• Labs icon Lab
  • Core Tech
Labs

Guided: Using TypeScript with React

Many developers know React and TypeScript separately but struggle to use them together effectively. In this guided lab, you’ll learn how to type React components, manage state safely, handle events, and work with hooks and context—all with TypeScript. By the end, you’ll be able to write more maintainable, type-safe React applications confidently.

Labs

Path Info

Level
Clock icon Beginner
Duration
Clock icon 56m
Published
Clock icon Mar 06, 2025

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Table of Contents

  1. Challenge

    Introduction

    In this guided lab, you will migrate a JavaScript React app to TypeScript. Along the way, you'll learn how to type different aspects of a React app and see it in action.

    What to Expect

    You should have foundational experience with React and have built a full application before.

    info> The React Foundations lab series will teach you the fundamentals and uses the same Helpdesk demo application.

    You should also be familiar with TypeScript fundamentals such as adding type annotations, interfaces, type aliases, and unions.

    info> Stuck on a task? Find solution files in the solutions/ folder, organized by step. Solution files look like {step.task}-{file path}, like 3.2-components~TicketPager.tsx which corresponds to Step 3, Task 2 in the src/components folder and TicketPager.tsx file. Copying and pasting the contents of that file to the equivalent file in the Filetree will make the check pass and you can examine it for details. Some tasks require you to update multiple files. The final outcome of the lab is in the final folder you can download.

    Getting Started

    To make sure the lab environment is set up and working, you'll start by running the Globomantics Helpdesk demo application. Once set up, you can visit {{localhost:5173}} to view the app in your browser, or in the Web Browser tab in the editor to the right.

    info> Web browser not working? Sometimes while reading through the lab, the dev server will shut down and you'll have to reconnect to the Terminal. Just run npm run dev again to start the server back up. Other times, the web browser may not always reflect the latest code due to the auto-save feature of the lab. Just click the refresh button next to the address bar in the tab to force-refresh the page.

    How is the app hosted? You may wonder what "VITE" means and how the app is being hosted on the above URL. This comes from the JavaScript development tool Vite.js.

    Vite is a frontend tool that bundles and builds JavaScript applications, including React. There are other options available depending to build and host React applications depending on what you need, such as Next.js.

    Vite is versatile and supports many other frameworks and libraries, including plain HTML and CSS with very little configuration.

    A Simple Helpdesk App

    The application is a simple helpdesk where you can submit and view support tickets related to fictitious Globomantics products.

    A quick tour around the codebase:

    • src/components holds different components that are on the page
    • src/hooks holds a custom hook to work with IndexedDB, the web-based storage engine and the idb package
    • src/contexts hold a shared context to share state in the app
    • App.jsx is the main component that renders the page
    • The TicketForm component encapsulates the form fields and handling form submission
    • The TicketDisplay component handles displaying ticket information to the user
    • The TicketList component handles displaying the list of tickets in the database
    • Additionally, the file tree has been organized into modules within the src folder so that it's easier to focus on the code you'll write throughout the lab.

    Benefits of Using TypeScript with React

    One of the major benefits TypeScript provides is adding more context to the code for readability. It helps you "explain" the system better.

    You probably know that TypeScript can infer types based on how they are used. We humans do the exact same thing without thinking. One way to think about TypeScript is that it forces you to explicitly think about types instead of only relying on inference, which may be an incomplete picture of your system.

    To emphasize this benefit, as you work through each example in the lab, it will not tell you what types to use. Instead, you will need to determine the correct typings based on how the code is used, which should be unfamiliar to you.

    This should help you understand why TypeScript is useful -- it is not just about catching errors.

  2. Challenge

    Configure TypeScript

    Currently the lab is set up with Vite and JavaScript for React development. On a new project, you can run the following command to initialize a template with TypeScript support:

    npm create vite@latest my-react-app -- --template react-ts
    

    This scaffolds a blank Vite project with TypeScript pre-configured with the latest React installed.

    However, when migrating an existing app, you'll need to configure TypeScript manually.

    Configuring TypeScript

    TypeScript can be configured using a tsconfig.json file at the root of your project.

    A tsconfig.json follows a schema like this:

    {
      "compilerOptions": {
        // config options here
      }
    }
    

    Most compiler options will be set under the compilerOptions section.

    Here are the recommended options to set for TypeScript and React support when using Vite:

    "target": "ESNext",
    "moduleResolution": "bundler",
    "moduleDetection": "force",
    "verbatimModuleSyntax": true,
    "useDefineForClassFields": true,
    "allowImportingTsExtensions": true,
    "jsx": "react-jsx",
    "lib": ["ESNext", "DOM"],
    "skipLibCheck": true,
    "noEmit": true
    
    What do these options do?

    Vite and several other build tools require TypeScript to be in "isolated modules" mode to enable proper JavaScript transpilation. This is configured with the verbatimModuleSyntax, useDefineForClassFields, allowImportingTsExtensions, and moduleDetection options.

    Since this is a React project, JSX needs to be configured and TypeScript supports the react-jsx option out-of-the-box.

    The noEmit flag ensures no actual TypeScript code is output when running the compiler, since the build tool will take care of bunding for production using the moduleResolution mode of bundler.

    The target and lib fields dictate what syntax is supported and which typings are included. Since this is a browser-based app, dom needs to be included and esnext is recommended for supporting the latest ECMAscript features.

    skipLibCheck ignores type errors if they come from your npm packages, which limits errors to "just your code."

    info> If you are using a different bundler/build tool, you will want to reference their documentation on the compiler options to use. These are the options Vite will set for you when using the Vanilla React TS template. ## Adding React Typings

    The official React npm packages do not come with TypeScript declarations. They are provided separately using @types packages and you will need to install them manually in your project.

    The lab has the packages pre-installed but in your own project you would install them using your package manager:

    npm install @types/react @types/react-dom --save
    

    The versions of the packages correspond to the major React versions (17.x, 18.x, 19.x, etc.) ## Language Server

    Frontend build tools like Vite do not perform type checking on your code since it can be a slow process, and typically your IDE provides that capability through the TypeScript Language Server.

    The TypeScript language server is provided through the official Microsoft TypeScript Compiler (tsc), which can compile your code and runs type checking.

    The lab has the compiler installed through the typescript npm package, but the file editor does not feature TypeScript language server integration.

    You will need a way run type checking to ensure everything works as expected at runtime.

    To do that, you can run the native TypeScript compiler (tsc). By default, it will use the nearest tsconfig.json for configuration which includes the noEmit option. When using a bundler like Vite, this prevents the compiler from emitting outputs and only runs the type check. It is common to run the type check in a CICD environment like GitHub Actions but you can also run it manually before committing code.

    You can run the type check anytime during the lab to see what errors you have left to fix using:

    npm run check
    ``` You should see an error:
    
    > error TS18003: No inputs were found in config file 'tsconfig.json'. Specified 'include' paths were `'["src"]'` and 'exclude' paths were `'["checks"]'`.
    
    This is expected since right now the project only contains JavaScript files and `tsc` expects to find TypeScript files.
    
    Time to start the migration process!
    
    info> From now on the lab will run a type check when verifying your tasks to make sure your code is valid TypeScript.
  3. Challenge

    Typing Components and Props

    To begin the migration to TypeScript, you will first need to change file extensions. Currently the project uses the extensions .js and .jsx so you'll need to rename them to .ts and .tsx respectively.

    info> TypeScript can support JavaScript extensions through the allowJs config flag but it is recommended to use language-specific file extensions to be clearer. However, you may want to use that option when migrating a big project to TypeScript to track your progress. info> After running the script, the App.jsx file tab may disappear. You can expand the filetree and open the new files as you work through the lab.

    Strict vs. Non-strict Mode

    If you run the type check, there should not be any compiler errors.

    By default, TypeScript will infer types as best it can when there are no type annotations. When it cannot infer a type, it falls back to the any type.

    This implicit typing may be desirable for a migration project but it will not catch many classes of errors. If you enable the strict: true config option, this will disable implicit type inference and catch more errors but the trade-off is that it requires you to be more explicit.

    info> To ease the lab migration process, strict mode is turned off but it is on by default when creating a new Vite React/TypeScript project.

    Common Prop Types

    In React, you can pass data to components through props, including primitive types like string, number, and boolean.

    It's also common to pass callback functions like onSelected. In TypeScript, you can type callbacks using the function notation:

    interface Props {
      name: string;
      isActive: boolean;
      onSelected: (index: number) => void;
    }
    

    Functional components accept props as the first argument and can be typed inline or separated into an interface or type alias.

    Typing Inline Props

    The simplest method of typing React component props is to inline the type annotation on the props object.

    For example, this component accepts two props:

    function UserAvatar({ imageUrl, title }: { 
      imageUrl: string;
      title: string;
    }) {
      return (
        <img src={imageUrl} alt={title} />
      )
    } 
    

    Based on the how each prop is used, you can infer they are string types since both img.src and img.alt are both typed as string.

    The type annotation is added inline after the destructuring expression.

    This approach is good for simple components that don't have a lot of props. ## Typing Prop Interfaces

    For components with more than a few props, it can be easier to read if you declare an interface or a type alias.

    What's the difference between an interface and type alias?

    Practically, there's not much of a difference. However, you will usually get better error messages when using interfaces. Interfaces are preferred for most cases but type aliases can be useful when doing "mapped types" which will be covered soon.

    Extracting the inline props in the example above might look like this:

    interface UserAvatarProps { 
      imageUrl: string;
      title: string;
    }
    
    function UserAvatar({ imageUrl, title }: UserAvatarProps) {
      return (
        <img src={imageUrl} alt={title} />
      )
    } 
    

    In many React projects, you can declare prop interfaces or types next to the component in the same module and they do not usually need to be exported (unless referenced directly). ## Typing Optional or Null Props

    Sometimes props can be optional when using a component, and you can declare that with the ? optional operator:

    interface Props {
      name?: string;
    }
    

    In this case, name will be typed as a union of string | undefined.

    Optional props are different than allowing a prop to be null. In that case, you can explicitly allow null values in addition to the regular type:

    interface Props {
      name: string | null;
    }
    

    name must be passed when using the component, but its value can be null (but never undefined). ## Importing React Typings

    The React typings packages have utility types that may be helpful for your project.

    When importing types only, in TypeScript you can use the type import keyword, which is sometimes required by your build tooling:

    import type { ReactNode } from 'react'
    

    This signals to your bundler/transpiler that its safe to strip those imports from the production code.

    info> You may receive a compiler or build error if you forget to use the type keyword for type-only imports. Some tools also require you to use the full .ts extension when importing from TypeScript ES modules.

    Global Typings

    React typings are global, so an alternative is that you can also reference utility types using the React global, like this:

    interface Props {
      heading: React.ReactNode;
    }
    

    Typing Children

    In React, all components can accept a children prop that represents other React components or JSX elements underneath them:

    <Layout>
      <Header />
      <Body />
      <Footer />
    </Layout>
    

    React children can be multiple types: an array of elements, components, a single component, a string literal, and more.

    React provides a type you can import named ReactNode which represents the valid children a component can accept.

    import type { ReactNode } from "react";
    
    interface Props {
      name: string;
      children: ReactNode;
    }
    
    function MyComponent(props: Props) {
     /* code */
    }
    

    This is an explicit way to type children on your own props type.

    Optional Children Prop

    React also provides a helper you can use, the PropsWithChildren<T> generic type. This will perform a type intersection and add the children prop to your typing automatically.

    You can use it without passing a props type to it, or pass your own prop interface/type alias:

    import type { PropsWithChildren } from "react";
    
    interface Props {
      name: string;
    }
    
    function MyComponent(props: PropsWithChildren<Props>) {
     /* props will contain `name` and `children` */
    }
    

    However, the children prop will be optional. This can be desirable in most situations but you may want to use the explicit method when you require children to be passed.

  4. Challenge

    Typing Async Hooks, Data, and APIs

    The app stores and fetches tickets asynchronously using IndexedDB, a native browser storage engine. The package it uses is idb which provides a nicer Promise-based wrapper around the IndexedDB web API.

    Start With the Data

    When migrating a JavaScript project to TypeScript, it is helpful to begin at the data or API layer and work your way down. This is because often data from APIs are passed down to components, and understanding the data layer gives you more insight into how the app works.

    The code to interface with IndexedDB is contained in the useTicketDb custom hook, which provides a set of async APIs components can call to create, get, or list tickets.

    Creating Data Types

    You will often need to create custom types that represent the business "domain objects" being sent to or received from an API. To understand what the types are, you will need to inspect how the code interacts with an API.

    It's common to use TypeScript interfaces to model the domain typings. For example, if you wanted to store and manage users, you may have a domain model like this:

    // api-types.ts
    
    export interface User {
      id?: number;
      username: string;
      email: string;
      hobbies: Hobby[];
    }
    
    export interface Hobby {
      id?: string;
      name: string;
    }
    

    You can put these types anywhere in your React app, often in a types folder or module and then export them for shared usage.

    info> In IndexedDB, IDs may be optional on the typings because they can be auto-generated during creation. You can denote this with the ? optional typing operator. ## Typing Third-Party APIs

    Unlike React, idb provides TypeScript typings as part of the package so there's no additional @types package to install.

    The main interface representing a database is IIDBPDatabase<T> where T represents your database schema typing.

    Continuing with the example of managing users, a fully typed IndexedDB API might look like this:

    import { openDB, type DBSchema, type IDBPDatabase } from "idb";
    import type { User, Hobby } from './api-types';
    
    interface AppDB extends DBSchema {
      users: {
        key: number;
        value: User
      }
      hobbies: {
        key: string;
        value: Hobby;
      }
    }
    
    let db: IDBPDatabase<AppDB>;
    
    async function openDatabase() {
      if (db) return db;
      db = await openDB<AppDB>("app-db", 1, {
        upgrade(db) {
            db.createObjectStore("users", {
              keyPath: 'id',
              autoIncrement: true,
            });
            db.createObjectStore("hobbies", { keyPath: 'id' });
          }
      );
      return db;
    }
    

    Here the example defines two IndexedDB object stores, users and hobbies. Every IndexedDB collection has a key to retrieve objects, which can be a string or auto-incrementing number. The typing indicates User objects are stored with a numbered ID, but hobbies are stored with string keys.

    Module-scope initialization

    It is best to manage a single IndexedDB instance at once, sometimes called the singleton pattern. The approach varies depending on the runtime environment. Here, db can be initialized once in the module-scope so that openDB is only called once per browser session.

    ## Suspense and Promises

    When loading the initial list of tickets, you will see a loading message. This is provided through React Suspense and the use API in React 19.

    The use API is not a hook, but instead can use a resource value like a Promise or Context. When used with Suspense and ErrorBoundary, React will show the Suspense fallback when a Promise is pending, or the error boundary fallback if it's rejected.

    Typing the use API

    This means you can pass a Promise from a top-level component down to a child within a Suspense boundary and it can show loading messages.

    You will need to type the Promise using the generic Promise<T> type where T represents what the Promise value is:

    import { use } from "react";
    import type { User } from "./api-types";
    
    function Users({ loadUsersPromise }: {
      loadUsersPromise: Promise<User[]>
    }) {
      const users = use(loadUsersPromise);
    
      /* render with users data */
    }
    

    Here users will be of type User[] as that's what the Promise value is wrapping. You do not need to explicitly type users since use will unwrap the Promise type for you and it will be inferred.

  5. Challenge

    Typing Events and Callbacks

    Synthetic Events

    React events are "synthetic" meaning they wrap native browser events, so there are some special cases to handle when it comes to onClick, onChange, and other event handlers with useCallback.

    If you use an inline callback, you do not need any typings:

    function Component() {
      const [name, setName] = useState('');
    
      return (
       <input type="text" onChange={e => setName(e.target.value)} />
      )
    }
    

    In this case, the onChange prop type will flow into the anonymous function so that e is typed as the correct React event type for the handler.

    However, for performance reasons you may decide to use the useCallback hook to memoize handlers, in which case you'll need to type them properly to strongly type the event object.

    There two ways to type events: by just typing the event argument or by typing the entire handler.

    React Event Typing Conventions

    Event handler typings follow a convention where the event type has an associated handler typing:

    • ChangeEvent and ChangeEventHandler for onChange
    • FocusEvent and FocusEventHandler for onBlur
    • MouseEvent and MouseEventHandler for onClick

    Depending on the event prop, the typing may be different.

    Typing Event Arguments

    For forms, the onChange prop is a common handler you'll need to type.

    The onChange prop invokes a callback and passes an event argument of type React.ChangeEvent<TElement> where TElement is the HTML element type (like HTMLInputElement) defined by the global DOM library typings (lib.dom).

    You can import this type helper to explicitly type event arguments in callbacks:

    import { useCallback } from "react";
    import type { ChangeEvent } from "react";
    
    function Component() {
      const [name, setName] = useState('');
      const handleChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
      setName(e.target.value);
      }), []);
    
      return (
       <input type="text" onChange={handleChange} />
      )
    }
    ``` Usually typing just the event argument is enough to satisfy TypeScript's inference when passing callbacks.
    
    However, there are other times where it may be necessary to explicitly type the handler instead of relying on inference.
    
    ## Typing Event Handlers
    
    In that case, you can import the `Handler` type and use it to annotate the callback variable declaration:
    
    ```tsx
    import { useCallback } from "react";
    import type { ChangeEvent } from "react";
    
    function Component() {
      const [name, setName] = useState('');
      const handleChange: ChangeEventHandler<HTMLInputElement> = useCallback(e => {
        setName(e.target.value);
      }), []);
    
      return (
       <input type="text" onChange={handleChange} />
      )
    }
    

    In this case, you do not need to explicitly type the e event argument as that is provided by the higher-order handler typing.

  6. Challenge

    Typing State, Context, and Providers

    Typing useState Hooks

    The React useState<S> hook accepts an optional generic type parameter representing the state value type. By default, the hook will infer the type based on what you pass as initial state:

    import { useState } from "react";
    
    const [selected, setSelected] = useState(false);
    // selected: boolean
    
    const [selected, setSelected] = useState([]);
    // selected: any[]
    
    const [selected, setSelected] = useState<number[]>([]);
    // selected: number[]
    

    Here you are defining the same state variable (selected) but there could be multiple meanings depending on how its used. For array state, it is common to explicitly pass the type in the generic so the state variable is typed properly.

    Typing null or undefined

    If state can be null, then you can pass null as union type:

    import { useState } from "react";
    
    const [selected, setSelected] = useState<boolean | null>(false);
    // selected: boolean | null
    

    If you do not pass an initial value or generic type, the state variable will be typed as undefined only, so you can use a type union if you want to allow undefined or another type. ## Typing Contexts and Providers

    A context is created using the React createContext API and it allows passing a generic type representing the context value type. The context value type will flow down into the Context.Provider and through use:

    import { createContext, use, type ReactNode } from "react";
    
    type AppTheme = 'light' | 'dark' | null;
    
    interface AppContextValue {
      theme: AppTheme;
    }
    
    const AppContext = createContext<AppContextValue>({
      theme: 'light'
    });
    
    export function AppContextProvider({ defaultTheme = 'light', children }: {
      defaultTheme?: AppTheme;
      children: ReactNode
    }) {
      return (
        <AppContext.Provider value={{ theme: defaultTheme }}>
          {children}
        </AppContext.Provider>
      )
    }
    
    export const useAppContext = () => use(AppContext);
    

    The object passed to the Context provider value prop will be typed as AppContextValue. ## Typing useMemo

    You can pass it inline as shown above or declare it as an object within the component and pass it down. It is common to memoize the context value using the useMemo API to reduce re-renders.

    Similar to useState, the useMemo API will infer the type based on the return type of the callback you pass but you can also explicitly pass a type in its generic parameter:

    export function AppContextProvider({ defaultTheme, children }: {
      defaultTheme?: AppTheme;
      children: ReactNode
    }) {
      const [theme, setTheme] = useState(defaultTheme ?? 'light');
      const value = useMemo<AppContextValue>(() => ({
        theme,
        setTheme
      }), [theme]);
    
      return (
        <AppContext.Provider value={value}>
          {children}
        </AppContext.Provider>
      )
    }
    

    Why is setTheme being passed to the app context value, and how is it typed?

    Typing State Actions

    It's not that common to pass set-state actions as props but in this demo app they are used to keep things simple.

    A set-state action is formally typed as React.Dispatch<React.SetStateAction<S>>, a nested generic type.

    The S generic type parameter is the same S as useState<S> and you can use this built-in React helper type to type set-state actions:

    import type { Dispatch, SetStateAction } from "react";
    
    interface AppContextValue {
      theme: AppTheme;
      setTheme: Dispatch<SetStateAction<AppTheme>>;
    }
    
    Simpler typings for state actions If you do not need to support the overload of set-state that passes the previous state, you can use a simpler type annotation like a regular callback function annotation, `(value: S) => void`. This is allowed by TypeScript since it matches the Dispatch interface.
    ## Flexibility of Implicit Typing

    Why is typing the useMemo hook within useContextValue optional?

    TypeScript uses a "structural" typing system, sometimes called "duck" typing. In other words, if it looks like a duck and quacks like a duck, TypeScript believes it's a duck.

    useMemo callback returns an object that matches your context interface and this is passed as the return type of the function, so TypeScript considers the type "assignable" and does not throw a compiler error.

    info> Try adding a new property in the useMemo callback and run the task check above again. Observe whether it fails or passes. Then, try changing one of the existing properties to a different value. Does it behave the same? Can you explain why or why not?

    Implicit types allow for flexibility. If you prefer strictness over flexibility, you can be explicit, and either pass a type to the useMemo<T> generic parameter or explicitly annotate the return type of the function.

  7. Challenge

    Typing Refs and the DOM

    Typing DOM Props

    All JSX elements support common HTML properties, like className, onClick, etc.

    React provides several type helpers to represent these depending on whether you want to include a ref prop or not:

    • React.ComponentPropsWithoutRef<T>
    • React.ComponentPropsWithRef<T>

    The T generic type is the component type, which in the case of HTML elements would be the literal string tag name:

    interface AutocompleteProps {
      htmlProps?: React.ComponentPropsWithoutRef<"div">;
    }
    
    export function Autocomplete({ htmlProps }: AutocompleteProps) {
      return (
        <div 
          {...htmlProps} 
          className={`autocomplete ${htmlProps?.className ?? ''}`
        >
        </div>
      )
    }
    

    In general, it's usually better to hide HTML from consuming components through a prop but occasionally it makes sense to allow passing in any HTML prop to the underlying JSX.

    You just want to be careful not to allow consumers to override HTML props you set yourself. To do that, you can spread the HTML props and then override specific ones based on the component behavior. The example above ensures the autocomplete CSS class is always present, in addition to whatever the caller passed (if any). When a new ticket is created, the app displays a popover using the native browser Popover API.

    The Popover API is available only on native DOM elements. Since React is an abstraction over the native DOM, the only way to access the native Popover API is through a Ref and the useRef hook.

    DOM Typings

    A ref holds a reference to a value. This can be any value but in the case of DOM refs, the reference is to a native DOM element. DOM element typings are available globally in all TypeScript apps that use the lib.dom.d.ts typings (the default for browser-based apps).

    You used these elements previously when typing event callbacks, for example:

    • HTMLDivElement
    • HTMLInputElement
    • HTMLSelectElement

    Each HTML element has an associated typing, and HTMLElement and Element are the base interfaces each one inherits. This is how the Popover API is made strongly-typed through TypeScript:

    • HTMLElement.showPopover()
    • HTMLElement.hidePopover()
    • HTMLElement.togglePopover()

    Typing the useRef Hook

    The useRef<T> hook accepts a generic type parameter representing the reference type you are holding, in this case a DOM element reference:

    const inputRef = useRef<HTMLInputElement>(null);
    
    return (
      <input ref={inputRef} {...props} />
    )
    

    By default, DOM refs are initialized to null because they are populated when the component is rendered. ## Typing CSS Styles

    If you only want to expose the style prop to consumers, React provides a simpler utility type, React.CSSProperties meant for that purpose:

    interface AutocompleteProps {
      containerStyle?: React.CSSProperties;
    }
    
    export function Autocomplete({ containerStyle }: AutocompleteProps) {
      return (
        <div 
          style={containerStyle}
          className="autocomplete"
        >
        </div>
      )
    }
    
  8. Challenge

    Typing Forms and Actions

    HTML forms in React 19 allow you to pass a "form action" callback in their action prop.

    const handleFormAction = (formData) => {
      const myData = {
        name: formData.get("name"),
        uploadedFile: formData.get("phone-number")
      };
    };
    
    return (
      <form action={handleFormAction}>
        {/* form code */}
      </form>
    )
    

    Form actions can be void functions or they can return a Promise:

    const handleFormAction = async (formData) => {
      /* await some API call */
    };
    

    Additionally, form actions may be marked as server functions with the "use server"; directive in supported frameworks like Next.js which will POST data back to the server and re-render the component. However, for this lab the code only runs on the client browser.

    Typing useActionState

    React also provides a useActionState hook that can create a form action whose pending state can be tracked for async or server-based rendering.

    There is no special typing required as long as you type the form action callback itself (the types will be inferred properly by TypeScript):

    function Component() {
      const handleFormAction = async (previousState: string, formData: FormData) => {
      const myData = {
        name: formData.get("name"),
        uploadedFile: formData.get("phone-number")
      };
    
        const req = await doSomething(myData);
    
        return req.message;
      };
      const [message, formAction, isPending] = useActionState(handleFormAction, null);
    
      return (
        <form action={formAction}>
          {/* form code */}
        </form>
      )
    }
    

    Here message is a type string presumably returned from an API doSomething(...).

    info> Note that the previous state is always returned as the first argument to the action callback, which is different when not using useActionState.

    Typing Form Data

    The FormData class has a get method that retrieves the form values for the given field name, which could be null if the field doesn't exist, a single string or even a File object (such as from a file input). The typing of FormData.get is a union between these different types. The FormData.getAll method is the same, except it could be an array of those values.

    If you want to specify a certain type for a form field, you may need to perform a type cast to cast the form data value to your expected type, like this:

    const myData = {
      name: formData.get("name") as string,
      hobbies: formData.getAll("hobby") as string[]
    };
    
    Narrowing vs. casting

    Instead of type casting, you can narrow the type through a type guard. In that case, you would need a conditional typeof check or a helper function that returns the correct type if you pass in the return value of formData.get/getAll. However, this can be more verbose and it is easier to cast directly in most cases.

  9. Challenge

    Recap and What's Next

    Using TypeScript with React seems fairly straightforward from the outside but once you get into the details, that's when things start to require more thought. In a real-world project, the hardest part is understanding how data flows through the application tree from the API layer down into components.

    What You Learned

    Congratulations on successfully completing this lab. Here are your takeaways:

    • How hard was it to type code you never saw before? Did you need to rely on hints and solutions/check files? Now imagine onboarding onto a 10k line project, or a 1MM line project. TypeScript helps make big systems more explainable.
    • Start with the data when typing a project using TypeScript. That often leads to an understanding of the domain you're working with.
    • Implicit type inference is more flexible than being explicit, but being explicit catches more potential errors before runtime.
    • "Let the types flow" to lessen the need for explicit typings. When you add typings at a higher-level, they can flow down so you aren't required to type as much lower-level code.

    Try the Lab Again on Hard Mode

    If you run:

    npm run check
    

    There should be no compiler errors. That's because the compiler is not strict by default, allowing the any type which disables type checking.

    For production projects, it's recommended to enable strict mode. If you want to stretch your TypeScript muscles, you check if there any strict errors by running:

    npm run check -- --strict
    

    There should be around 12 errors to fix. Can you solve them all?

    info> You can also set the "strict": true compiler option in the tsconfig.json file.

Kamran Ayub is a technologist specializing in full-stack web solutions. He's a huge fan of open source and of sharing what he knows.

What's a lab?

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Provided environment for hands-on practice

We will provide the credentials and environment necessary for you to practice right within your browser.

Guided walkthrough

Follow along with the author’s guided walkthrough and build something new in your provided environment!

Did you know?

On average, you retain 75% more of your learning if you get time for practice.