Difficulty: Advanced
Typing component props is the most fundamental skill when using TypeScript with React. Props define the contract between a parent component and its child, and TypeScript enforces that contract at compile time. The most common approach is to define a props interface and annotate the component parameter, giving you autocomplete, refactoring support, and compile-time error detection.
There are two main ways to type a React function component. The first is the explicit `React.FC<Props>` (or `React.FunctionComponent<Props>`) generic type, which automatically includes the `children` prop in older React types. The second and now more recommended approach is to simply type the props parameter directly: `function MyComponent(props: MyProps)`. The direct typing approach is preferred in modern React because it gives you more control over whether `children` is accepted and avoids some subtle issues with FC's implicit children and return type.
The `children` prop is typed as `React.ReactNode`, which covers strings, numbers, JSX elements, arrays, fragments, portals, null, and undefined - essentially anything React can render. When using the direct typing approach, you must explicitly add `children` to your props interface. You can also use the utility type `React.PropsWithChildren<Props>` to add a typed `children` prop to any interface.
Optional props are defined with the `?` modifier, and default values can be provided via destructuring defaults in the function parameter. Required props without `?` will cause a compile error if omitted by the parent. This makes it impossible to forget a required prop, catching bugs at development time rather than in production.
// Defining and using typed props
interface ButtonProps {
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary' | 'danger';
disabled?: boolean;
}
// Direct typing approach (recommended)
function Button({ label, onClick, variant = 'primary', disabled = false }: ButtonProps) {
return `<button class="${variant}" disabled={${disabled}}>${label}</button>`;
}
// Simulating component usage
const result1 = Button({ label: 'Submit', onClick: () => {}, variant: 'primary' });
const result2 = Button({ label: 'Cancel', onClick: () => {}, variant: 'danger', disabled: true });
const result3 = Button({ label: 'Save', onClick: () => {} }); // uses defaults
console.log(result1);
console.log(result2);
console.log(result3);
Props with '?' are optional. Default values are provided via destructuring. TypeScript ensures the parent passes all required props and that values match the expected types.
// Typing the children prop explicitly
interface CardProps {
title: string;
children: string; // In real React, this would be React.ReactNode
}
function Card({ title, children }: CardProps): string {
return `<div class="card"><h2>${title}</h2><div>${children}</div></div>`;
}
// PropsWithChildren utility type pattern
// type PropsWithChildren<P> = P & { children?: React.ReactNode };
interface PanelProps {
heading: string;
}
// Simulating PropsWithChildren
type WithChildren<P> = P & { children?: string };
function Panel({ heading, children }: WithChildren<PanelProps>): string {
return `<section><h3>${heading}</h3>${children || 'No content'}</section>`;
}
console.log(Card({ title: 'Welcome', children: 'Hello there!' }));
console.log(Panel({ heading: 'Info', children: 'Details here' }));
console.log(Panel({ heading: 'Empty' }));
Children can be typed as React.ReactNode (accepting anything renderable) or more narrowly. PropsWithChildren<P> adds an optional children prop to any props type.
// Using discriminated unions for polymorphic component props
type AlertProps =
| { type: 'success'; message: string }
| { type: 'error'; message: string; retryable: boolean }
| { type: 'loading' };
function Alert(props: AlertProps): string {
switch (props.type) {
case 'success':
return `[SUCCESS] ${props.message}`;
case 'error':
return `[ERROR] ${props.message} (retryable: ${props.retryable})`;
case 'loading':
return '[LOADING] Please wait...';
}
}
console.log(Alert({ type: 'success', message: 'Saved!' }));
console.log(Alert({ type: 'error', message: 'Failed', retryable: true }));
console.log(Alert({ type: 'loading' }));
Discriminated union props let you model components where different prop combinations are valid depending on a discriminant field. TypeScript narrows the type inside each case.
FC type, props interfaces, children prop, optional props, default props, React.PropsWithChildren