Typing props, children, state, refs and events, why React.FC fell out of favour, generic components and typed reducers.
11 questions3 fresher, 5 mid-level, 3 senior
Most React jobs are TypeScript jobs now, so these come up even when the advert does not mention them. The good news is that the questions are narrow: interviewers want to see that you can type a component, a hook and an event handler without reaching for any, and that you know why the ergonomics are the way they are.
1.How do you type a component's props?
Fresher
Declare a type or interface for the props object and annotate the destructured parameter. That is all React needs — the return type is inferred.
Either works. Interfaces can be merged and extended and give slightly nicer errors for object shapes; type aliases can express unions, intersections and mapped types. Most teams pick one for consistency, and type is the more common default in React code.
interface supports declaration merging, which is useful for augmenting a library's types and unwelcome in application code where two definitions silently combining is a bug.
type is required when the props are a union, for example a discriminated union of two variants of the same component.
Both support extension: interface A extends B and type A = B & { ... }.
A shape a plain interface cannot express.
type InputProps =
| { multiline: true; rows: number }
| { multiline?: false; rows?: never };
Likely follow-up questions
When could you not use an interface?
3.How do you type children?
Fresher
Use React.ReactNode, which covers elements, strings, numbers, arrays, null and undefined. Use React.ReactElement only when the child must be a single element.
type PanelProps = {
title: string;
children: React.ReactNode; // anything renderable
};
type TabsProps = {
children: React.ReactElement<TabProps>[]; // must be Tab elements
};
type ToggleProps = {
children: (isOpen: boolean) => React.ReactNode; // render prop
};
ReactNode is the right default. JSX.Element is narrower than people expect and rejects a string child, which is usually not what you want.
Likely follow-up questions
What is the difference between ReactNode and JSX.Element?
4.What is React.FC and why do many teams avoid it?
Mid-level
React.FC is a type for function components that annotates the whole function rather than its props. Teams moved away from it because it used to add an implicit children prop, it complicates generic components, and annotating the parameter is simpler.
// Common today
function Card({ title }: CardProps) { ... }
// The older style
const Card: React.FC<CardProps> = ({ title }) => { ... };
Before React 18's types, React.FC added children to every component, so a component that accepted no children still typed as if it did. That is fixed, but the habit stuck.
A generic component cannot be expressed cleanly with React.FC without awkward casts.
Annotating the parameter gives the same safety with less indirection.
It is not wrong to use it, and a codebase that already does should stay consistent. This question is really about whether you can explain a convention rather than just follow one.
Likely follow-up questions
What changed in the React 18 type definitions?
5.How do you type useState, and when is the type argument needed?
Fresher
TypeScript infers the type from the initial value, so most calls need no annotation. You need the type argument when the initial value does not represent the full range, typically null or an empty array.
const [count, setCount] = useState(0); // number, inferred
const [user, setUser] = useState<User | null>(null); // otherwise it is just null
const [items, setItems] = useState<Item[]>([]); // otherwise never[]
// A state machine, typed as a union
const [status, setStatus] = useState<'idle' | 'loading' | 'error'>('idle');
Likely follow-up questions
Why does an empty array infer never[]?
6.How do you type an event handler?
Mid-level
Use React's generic event types parameterised by the element: React.ChangeEvent<HTMLInputElement>, React.MouseEvent<HTMLButtonElement>, React.FormEvent<HTMLFormElement>. Inline handlers usually infer these for you.
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
setValue(event.target.value); // typed as string
}
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
}
// Inline: no annotation needed, the type flows from the element
<input onChange={(event) => setValue(event.target.value)} />
The element type argument is what makes event.target.value type as string rather than erroring. Getting this wrong is the reason people reach for any here, so being specific about it is a good signal.
Likely follow-up questions
Why is event.target sometimes typed as EventTarget?
7.How do you type useRef?
Mid-level
For a DOM node use useRef<HTMLInputElement>(null), which gives a read-only current that may be null. For a mutable value use useRef<number | undefined>(undefined), which gives a writable current.
// DOM ref: React sets it, so current is typed as possibly null
const inputRef = useRef<HTMLInputElement>(null);
inputRef.current?.focus();
// Mutable instance value: you set it, so current is writable
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
timerRef.current = setTimeout(save, 500);
The two overloads behave differently on purpose: passing null as the initial value and a DOM type produces a RefObject, whose current React owns; anything else produces a MutableRefObject you can assign to.
Likely follow-up questions
Why is current read-only in one case and not the other?
8.How do you type a component that accepts all the native props of an element?
Senior
Extend the element's prop type from React's JSX intrinsic elements, or use ComponentPropsWithoutRef, then add your own props on top and spread the rest onto the element.
Use ComponentPropsWithRef when the component forwards a ref. The pattern matters for design systems: consumers get aria-*, data-*, type, disabled and every other native attribute without you enumerating them.
Likely follow-up questions
How would you type this if the component could render as an anchor instead?
9.How do you write a generic component?
Senior
Declare a type parameter on the function and use it in the props, so the caller's data type flows through to the callbacks and the rendered items.
type ListProps<T> = {
items: T[];
getKey: (item: T) => string;
renderItem: (item: T) => React.ReactNode;
};
function List<T>({ items, getKey, renderItem }: ListProps<T>) {
return <ul>{items.map((item) => <li key={getKey(item)}>{renderItem(item)}</li>)}</ul>;
}
// T is inferred as User, so item is typed inside renderItem
<List items={users} getKey={(u) => u.id} renderItem={(u) => u.name} />
Likely follow-up questions
Why does the trailing comma matter?
10.How do you type useReducer and its actions?
Senior
Model the actions as a discriminated union on a type field and type the reducer as (state: State, action: Action) => State. TypeScript then narrows the payload inside each case and errors on an unhandled action.
type State = { items: Item[]; status: 'idle' | 'saving' };
type Action =
| { type: 'added'; item: Item }
| { type: 'removed'; id: string }
| { type: 'saving' };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'added':
return { ...state, items: [...state.items, action.item] }; // action.item is typed
case 'removed':
return { ...state, items: state.items.filter((i) => i.id !== action.id) };
case 'saving':
return { ...state, status: 'saving' };
}
}
With the return type annotated and no default case, TypeScript reports a missing branch when a new action is added, which turns the reducer into a checklist that maintains itself.
11.How should you type data coming back from an API?
Mid-level
Do not assert it. A response is unknown until something checks it, so validate it at the boundary with a schema library such as Zod and let the validated type flow inwards.
const UserSchema = z.object({ id: z.string(), name: z.string() });
type User = z.infer<typeof UserSchema>;
async function getUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return UserSchema.parse(await response.json()); // throws if the shape is wrong
}