In one minute
A component is a function, so typing it is typing a function’s parameter: describe the props object. React adds only a few types of its own (ReactNode, ComponentProps<'span'>, event types). The rest is everyday TypeScript, and a few techniques make it much stronger: literal unions instead of string, deriving types from values with typeof/keyof, and satisfies to check an object without losing its exact keys.
You’ll be able to: type a component’s props, keep one source of truth for allowed values, and read the errors tsc gives you.
1const formatters = {2 USD: (cents: number) => `$${cents / 100}`,3 EUR: (cents: number) => `€${cents / 100}`,4 GBP: (cents: number) => `£${cents / 100}`,5} satisfies Record<string, Formatter>67type Currency = keyof typeof formatters89type PriceProps = { cents: number; currency?: Currency }1011<Price cents="89.00" /> // ✗12<Price currency="JPY" /> // ✗13<Price currency="EUR" /> // ✗} satisfies Record<string, Formatter>satisfieschecks that every value is a(cents: number) => string, but does not widen the object: its keys stay exactlyUSD,EUR,GBP.type Currency = keyof typeof formatterstypeofturns the value into its type, andkeyoftakes its keys as a union. Add a currency to the object and the type follows.type PriceProps = { cents: number; currency?: Currency }The props use the derived union.<Price cents="89.00" /> // ✗Three mistakes, three compile errors, before any code runs: a string for a number (TS2322), an unknown currency (TS2322), and a missingcents(TS2741).
The example: a Price that knows its currencies
type Formatter = (cents: number) => string
const formatters = {
USD: (cents: number) => `$${(cents / 100).toFixed(2)}`,
EUR: (cents: number) => `€${(cents / 100).toFixed(2)}`,
GBP: (cents: number) => `£${(cents / 100).toFixed(2)}`,
} satisfies Record<string, Formatter>
export type Currency = keyof typeof formatters // 'USD' | 'EUR' | 'GBP'
type PriceProps = {
cents: number
currency?: Currency
}
export function Price({ cents, currency = 'USD' }: PriceProps) {
return <p className="price">{formatters[currency](cents)}</p>
}
Three mistakes the compiler catches (the lesson’s 05-typescript.bad.tsx):
// Mistakes TypeScript catches before the code runs (not part of the app build).
import { Price } from './05-typescript'
export const examples = (
<>
<Price cents="89.00" />
<Price cents={8900} currency="JPY" />
<Price currency="EUR" />
</>
)
What tsc --noEmit reported:
05-typescript.bad.tsx(6,10): error TS2322: Type 'string' is not assignable to type 'number'.
05-typescript.bad.tsx(7,23): error TS2322: Type '"JPY"' is not assignable to type '"USD" | "EUR" | "GBP" | undefined'.
05-typescript.bad.tsx(8,4): error TS2741: Property 'cents' is missing in type '{ currency: "EUR"; }' but required in type 'PriceProps'.
None of these would have failed loudly at runtime: "89.00" / 100 quietly becomes 0.89, and formatters["JPY"] is undefined, which crashes only when that price is shown.
How it works
- Type the parameter, not the function.
function Price({ cents, currency = 'USD' }: PriceProps). Prefer this toReact.FC<PriceProps>, which historically added achildrenprop you didn’t ask for and makes generic components awkward. - Literal unions beat
string.'USD' | 'EUR' | 'GBP'rejects typos and gives autocomplete. - Derive instead of repeating.
typeof formattersreads the object’s type;keyoftakes its keys. Add a currency to the object, andCurrencyupdates itself. satisfieschecks without widening.const formatters: Record<string, Formatter> = …would widen the keys tostring(sokeyofgivesstring).satisfies Record<string, Formatter>checks every value but keeps the exact keys.- Optional + default.
currency?: Currencylets callers omit it; the destructuring default= 'USD'means the function body sees aCurrency, neverundefined. - Wrapping a native element:
ComponentProps<'span'>gives every prop a<span>accepts. Styling uses it for aTagcomponent. In React 19,refis a regular prop for function components, so it’s included and simply forwarded when you spread...rest.
Common mistakes
operator: string/currency: stringwhen only a few values are valid.- Writing the allowed values twice (a union and an object) and letting them drift apart.
- Annotating with
: Record<…>when you need the exact keys later; usesatisfies. - Marking a prop optional but forgetting the default, then handling
undefinedeverywhere.
Interview Q&A
Should you type components with React.FC?
Usually not. Type the props parameter directly: it covers everything React.FC does, works naturally for generic components (function List<T>(props: ListProps<T>)), and doesn’t imply props you didn’t declare.
Why a union of string literals instead of string?
string accepts any value, including ones nothing handles. A union like 'USD' | 'EUR' | 'GBP' turns a runtime bug into a compile error: the recorded tsc output rejects currency="JPY" with TS2322.
What do typeof and keyof do when combined?
In a type position, typeof formatters gives the type of the runtime value; keyof then gives the union of its keys. keyof typeof formatters is 'USD' | 'EUR' | 'GBP', derived from the object so there’s one source of truth.
What does satisfies give you that an annotation doesn't?
An annotation (const x: T = …) makes the variable’s type exactly T, losing detail such as the exact keys. satisfies T checks the value against T but keeps the value’s own inferred type, so keyof typeof formatters still lists the three currencies.
How do you type a component that wraps a native element?
Intersect the element’s props with your own: ComponentProps<'span'> & { tone?: Tone }, destructure what you use, and spread the rest onto the element.
Related
- Custom Components: what you’re typing.
- Styling:
ComponentProps<'span'>in practice.
Sources
- react.dev: Using TypeScript
- TypeScript handbook: Literal types,
keyof,typeof,satisfies - Topic order inspired by Kent C. Dodds’ EpicReact React Fundamentals workshop; the example app and code here are this handbook’s own.
Primary source: react.dev/learn/typescript