TypeScript for React Interview Questions: 10 Questions with Clear Answers
In front-end engineering interviews, proficiency in TypeScript within React codebases has become a fundamental baseline. Beyond primitive annotations, interviewers evaluate component contract design, elimination of impossible states with discriminated unions, generic type inference, and DOM ref typing.
This guide covers 10 high-frequency TypeScript for React interview questions tailored for developers with 0–5 years of experience. For each question, review the interviewer's focus, spoken answer, technical explanation, code example, common pitfalls, and follow-ups.
Question Index
| # | Question | Focus Area | Difficulty |
|---|---|---|---|
| 1 | ReactNode vs ReactElement vs JSX.Element | Component return and children typing | Junior |
| 2 | Typing Event Handlers and Synthetic Events | Form and mouse event safety | Junior |
| 3 | ComponentPropsWithoutRef for HTML Attributes | Reusable native HTML wrapper props | Junior-Mid |
| 4 | Discriminated Unions for Mutually Exclusive Props | Variant props and eliminating impossible states | Mid |
| 5 | Generic React Components for Reusable Lists | Reusable components with inferred item types | Mid |
| 6 | Typing useRef: DOM Elements vs Mutable Values | Ref object typing and read-only current semantics | Mid |
| 7 | Custom Hook Typing: Tuples vs Objects and as const | Return type inference and const assertions | Mid |
| 8 | Discriminated Unions for Async State Management | Status-based data/error state safety | Mid |
| 9 | Polymorphic Components with the as Prop | Dynamic HTML tags with matching native attributes | Senior |
| 10 | Utility Types for Prop Transformations | Composing external prop types with Pick and Omit | Mid-Senior |
Q1. What is the difference between ReactNode, ReactElement, and JSX.Element in React typing?
Level: Junior
What the interviewer is checking: Understanding how React models UI primitives in the type system and when to use each for props, children, and component returns.
Short answer to say out loud
"A ReactElement is an object representation of a virtual DOM node created by createElement or JSX, containing type, props, and key. JSX.Element is an alias for ReactElement with type arguments defaulted to any. ReactNode is the widest union type—it includes ReactElement, strings, numbers, booleans, null, undefined, and fragments. For component children, always prefer ReactNode. For elements that must be renderable objects, use ReactElement."
Detailed explanation
ReactElement models a virtual DOM element with type, props, and key. JSX.Element inherits from ReactElement<any, any>. ReactNode is the comprehensive union of anything renderable: elements, strings, numbers, fragments, portals, and null. For component children, always prefer ReactNode.
Example
import type { ReactNode, ReactElement } from 'react';
interface CardProps {
title: string;
children: ReactNode;
}
export function Card({ title, children }: CardProps): ReactElement {
return (
<div className="card">
<h2>{title}</h2>
<div>{children}</div>
</div>
);
}
Common mistakes
- Typing
childrenasJSX.Element, causing compiler errors whenever consumers pass plain text or conditional booleans. - Typing
childrenasany, losing type checking for component composition.
Likely follow-up questions
- Interviewer: "Why was the component return type broadened in React 18+?"
- Answer: "In TypeScript 5.1+ and React 18+, component returns were broadened to
ReactNodeto natively support returning strings, numbers, or promises in React Server Components without type errors."
Q2. How do you correctly type event handlers and synthetic events in React?
Level: Junior
What the interviewer is checking: Familiarity with React's synthetic event wrappers, generic event types, and differentiating inline vs separate handler functions.
Short answer to say out loud
"React wraps native DOM events in synthetic event wrappers like React.ChangeEvent and React.MouseEvent. When annotating an event parameter in a standalone function, specify the generic event type and the target element, such as React.ChangeEvent<HTMLInputElement>. For inline handlers, TypeScript infers the event parameter automatically."
Detailed explanation
React wraps native DOM events in synthetic event wrappers. Annotating the generic DOM element (e.g., ChangeEvent<HTMLInputElement>) ensures e.target and e.currentTarget expose native HTML properties like .value without manual type casting.
Example
import { useState, type ChangeEvent, type FormEvent } from 'react';
export function RegistrationForm() {
const [email, setEmail] = useState('');
const handleEmailChange = (e: ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value);
};
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log('Submitted email:', email);
};
return (
<form onSubmit={handleSubmit}>
<input type="email" value={email} onChange={handleEmailChange} />
<button type="submit">Submit</button>
</form>
);
}
Common mistakes
- Using native DOM
Eventinstead of React'sReact.ChangeEvent, causing JSX prop type mismatches. - Accessing
e.target.valuewhen the generic is typed asHTMLElementinstead ofHTMLInputElement.
Likely follow-up questions
- Interviewer: "What is the difference between
e.targetande.currentTargetin TypeScript?" - Answer: "
e.currentTargetis typed as the exact element where the listener was attached.e.targetrefers to the element where the event originated, which can be a nested child node."
Q3. How should you type a component that extends native HTML attributes using ComponentPropsWithoutRef?
Level: Junior-Mid
What the interviewer is checking: Understanding component composition, HTML attribute inheritance, avoiding conflicting props, and ref handling.
Short answer to say out loud
"To create a reusable wrapper component that accepts all standard HTML attributes of an underlying element, extend React.ComponentPropsWithoutRef<'element'>. For example, a custom button interface extends React.ComponentPropsWithoutRef<'button'>. This inherits native attributes like onClick, disabled, and aria-*. If you need ref forwarding, use React.ComponentPropsWithRef<'button'> instead."
Detailed explanation
To support native HTML attributes on custom elements without manual duplication, extend ComponentPropsWithoutRef<'button'> (or WithRef when forwarding refs). Your component can then define custom props alongside native attributes.
Example
import type { ComponentPropsWithoutRef } from 'react';
interface CustomButtonProps extends ComponentPropsWithoutRef<'button'> {
variant?: 'primary' | 'secondary' | 'danger';
isLoading?: boolean;
}
export function CustomButton({
variant = 'primary',
isLoading = false,
children,
disabled,
className = '',
...restProps
}: CustomButtonProps) {
return (
<button
type="button"
disabled={disabled || isLoading}
className={`btn btn-${variant} ${className}`}
{...restProps}
>
{isLoading ? <span>Loading...</span> : children}
</button>
);
}
Common mistakes
- Extending
HTMLAttributes<HTMLButtonElement>, which omits button-specific attributes likedisabled,type, andform. - Failing to omit conflicting props when a custom prop shares a name with a native HTML prop.
Likely follow-up questions
- Interviewer: "How do you override an existing HTML attribute's type, such as replacing native
onChangewith a custom signature?" - Answer: "Use TypeScript's
Omitutility type:interface CustomInputProps extends Omit<ComponentPropsWithoutRef<'input'>, 'onChange'> { onChange: (val: string) => void; }."
Q4. How do you implement discriminated unions for mutually exclusive component props?
Level: Mid
What the interviewer is checking: Ability to eliminate impossible component states at compile time using TypeScript union narrowing.
Short answer to say out loud
"Use a discriminated union to represent mutually exclusive prop configurations. By sharing a common discriminant literal property (like variant or mode), TypeScript prevents callers from providing conflicting props simultaneously. Inside the component, TypeScript automatically narrows available props based on that discriminant."
Detailed explanation
Declaring multiple optional props for mutually exclusive variants creates impossible states at runtime. Discriminated unions enforce valid configurations at compile time using a shared discriminant key like variant.
Example
type InfoAlert = {
variant: 'info';
message: string;
};
type ActionAlert = {
variant: 'action';
message: string;
actionLabel: string;
onAction: () => void;
};
type AlertProps = InfoAlert | ActionAlert;
export function Alert(props: AlertProps) {
if (props.variant === 'action') {
return (
<div className="alert alert-action">
<p>{props.message}</p>
<button type="button" onClick={props.onAction}>
{props.actionLabel}
</button>
</div>
);
}
return <div className="alert alert-info"><p>{props.message}</p></div>;
}
Common mistakes
- Defining all variants as optional fields on a single interface, failing to catch conflicting prop combinations at compile time.
- Destructuring discriminated union props before narrowing:
const { variant, actionLabel } = props;. Destructuring breaks union narrowing in older TypeScript versions because local variables lose their shared relationship.
Likely follow-up questions
- Interviewer: "How do you model mutually exclusive props without an explicit discriminant tag?"
- Answer: "Define two interfaces where exclusive keys are set to
never:{ href: string; onClick?: never } | { href?: never; onClick: () => void }."
Q5. How do you build and type a generic React component, such as a reusable data table or select list?
Level: Mid
What the interviewer is checking: Understanding generic type parameters in JSX, generic constraints (extends), and caller type inference.
Short answer to say out loud
"A generic React component defines a type parameter on the component function signature—for example, function SelectList<T>(props: SelectListProps<T>). When a parent renders <SelectList items={users} ... />, TypeScript infers T directly from the passed items prop, ensuring callbacks like onSelect receive the exact typed item without manual casting."
Detailed explanation
Generic components parameterize props over type T, allowing reusable UI (lists, dropdowns, tables) to infer caller types automatically without casting. In .tsx files, write <T,> in arrow signatures to disambiguate from JSX tags.
Example
import type { ReactNode } from 'react';
interface SelectListProps<T> {
items: T[];
renderItem: (item: T) => ReactNode;
onSelect: (item: T) => void;
keyExtractor: (item: T) => string | number;
}
export function SelectList<T>({
items,
renderItem,
onSelect,
keyExtractor,
}: SelectListProps<T>) {
return (
<ul className="select-list">
{items.map((item) => (
<li key={keyExtractor(item)}>
<button type="button" onClick={() => onSelect(item)}>
{renderItem(item)}
</button>
</li>
))}
</ul>
);
}
Common mistakes
- Writing
const List = <T>(props: ListProps<T>) => ...in.tsxfiles without a trailing comma (<T,>), triggering a JSX syntax error. - Over-constraining
Tunnecessarily, reducing component flexibility.
Likely follow-up questions
- Interviewer: "How do you forward refs with a generic React component?"
- Answer: "
React.forwardRefhistorically drops generic type parameters. To preserve generics, you either wrap the component in a typed helper or define an augmented type declaration forforwardRef."
Q6. How do you correctly type useRef for DOM element references versus mutable values?
Level: Mid
What the interviewer is checking: Understanding how initial values in useRef determine whether ref.current is mutable or read-only in @types/react.
Short answer to say out loud
"In TypeScript, useRef has distinct overloads. When referencing a DOM element, initialize it with null and pass the element type in the generic: useRef<HTMLInputElement>(null). This returns a RefObject where .current is read-only (managed by React) and typed as HTMLInputElement | null. When storing a mutable timer or value, pass the initial value directly: useRef<number>(0). This returns a MutableRefObject where .current is directly reassignable."
Detailed explanation
When initialized with null (useRef<HTMLInputElement>(null)), TypeScript infers RefObject<T>, whose .current is read-only and managed by React. When initialized with a value (useRef<number>(0)), it returns a mutable MutableRefObject<T>.
Example
import { useRef, useEffect } from 'react';
export function SearchWithTimer() {
const inputRef = useRef<HTMLInputElement>(null);
const timerRef = useRef<number | null>(null);
useEffect(() => {
inputRef.current?.focus();
timerRef.current = window.setInterval(() => console.log('Tick'), 1000);
return () => {
if (timerRef.current !== null) clearInterval(timerRef.current);
};
}, []);
return <input ref={inputRef} placeholder="Search..." />;
}
Common mistakes
- Declaring
useRef<HTMLInputElement>()withoutnull. This leaves initial value undefined, failing to match React's JSXrefattribute expectation. - Trying to manually reassign
inputRef.current = ...when typed as a DOM ref, causing a read-only property compiler error.
Likely follow-up questions
- Interviewer: "Why is
timerReftyped asnumber | nullrather thanNodeJS.Timeoutin browser apps?" - Answer: "In browser environments,
window.setIntervalreturns anumberID. In Node.js, it returns aNodeJS.Timeoutobject. Usingwindow.setIntervalexplicitly avoids typing conflicts between Node and DOM typings."
Q7. How do you type custom hooks returning tuples versus objects, and why is as const necessary for tuples?
Level: Mid
What the interviewer is checking: Understanding TypeScript array inference vs tuple inference, return type contracts, and ergonomics of custom hooks.
Short answer to say out loud
"When a custom hook returns an array like [state, setState], TypeScript infers a union array (T | U)[] by default. If a caller invokes the second item as a function, TypeScript throws an error because it might be the state value. To enforce that the return value is a fixed-length positional tuple, annotate the return statement with as const or explicitly annotate the hook's return type as [T, (val: T) => void]."
Detailed explanation
Returning an array without annotations causes TypeScript to infer a loose union array (T | U)[]. Appending as const enforces a strict, readonly positional tuple. For hooks exposing more than two values, returning a typed object provides better ergonomics.
Example
import { useState, useCallback } from 'react';
export function useToggle(initialValue = false) {
const [value, setValue] = useState<boolean>(initialValue);
const toggle = useCallback(() => setValue((prev) => !prev), []);
return [value, toggle] as const;
}
export function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = useCallback(() => setCount((c) => c + 1), []);
const reset = useCallback(() => setCount(initialValue), [initialValue]);
return { count, increment, reset };
}
Common mistakes
- Omitting
as conston a tuple return, resulting inCannot invoke an expression whose type lacks a call signatureerrors. - Returning tuples with more than 3 elements, which degrades API ergonomics compared to returning a typed object.
Likely follow-up questions
- Interviewer: "What is the difference between explicit return type annotations and
as const?" - Answer: "An explicit return type guarantees the hook conforms to a strict contract at the declaration.
as constis a concise inline assertion that instructs TypeScript to infer the narrowest literal tuple type."
Q8. How do you design a discriminated union for asynchronous state in React to eliminate impossible states?
Level: Mid
What the interviewer is checking: State management modeling skills, avoiding boolean flags (isLoading, isError, isSuccess), and enforcing exhaustive type narrowing.
Short answer to say out loud
"Instead of managing separate booleans like isLoading, isError, data, and error, model async state as a discriminated union keyed on a status field: 'idle' | 'loading' | 'success' | 'error'. Under 'success', data is guaranteed to be present and error is null. Under 'error', error is guaranteed and data is null. In the UI, a switch statement on status guarantees components only access data when it actually exists."
Detailed explanation
Separate boolean flags (isLoading, isError) allow contradictory states. Modeling state as a discriminated union ('idle' | 'loading' | 'success' | 'error') ensures data is only accessible when the request succeeds.
Example
type AsyncState<T> =
| { status: 'idle'; data: null; error: null }
| { status: 'loading'; data: T | null; error: null }
| { status: 'success'; data: T; error: null }
| { status: 'error'; data: null; error: Error };
interface User { id: string; name: string; }
export function UserView({ state }: { state: AsyncState<User> }) {
switch (state.status) {
case 'idle':
return <p>Press fetch to load.</p>;
case 'loading':
return <p>Loading...</p>;
case 'error':
return <p className="error">{state.error.message}</p>;
case 'success':
return <h3>{state.data.name}</h3>;
}
}
Common mistakes
- Using multiple independent
useStatehooks (const [isLoading, setIsLoading] = useState(false); const [data, setData] = useState(...)) which allow impossible combinations. - Failing to handle all union cases, which can be guarded using exhaustive checks with
never.
Likely follow-up questions
- Interviewer: "How do you enforce exhaustive checking at compile time in a switch statement?"
- Answer: "Add a
defaultcase that assigns the remaining state tonever:const _exhaustive: never = state; throw new Error('Unhandled case');. If a new status is added to the union, TypeScript throws a compilation error on that assignment."
Q9. How do you implement a polymorphic component with the 'as' prop in TypeScript?
Level: Senior
What the interviewer is checking: Mastery of advanced TypeScript generics, template literal types, and building flexible design-system primitives.
Short answer to say out loud
"A polymorphic component accepts an as prop allowing callers to render it as different HTML tags or components—such as <Text as='h1'> or <Button as='a' href='/login'>. To type it correctly, make the component generic over an element type E extends React.ElementType, and combine custom props with React.ComponentPropsWithoutRef<E> using Omit to avoid prop collisions. This ensures the component accepts only the HTML attributes valid for the specified tag."
Detailed explanation
Polymorphic components parameterize their tag via E extends React.ElementType. Combining custom props with Omit<ComponentPropsWithoutRef<E>, keyof CustomProps> ensures that native props (like href for <a>) are correctly validated for the rendered tag.
Example
import type { ElementType, ReactNode, ComponentPropsWithoutRef } from 'react';
interface TextOwnProps<E extends ElementType = ElementType> {
as?: E;
children: ReactNode;
variant?: 'body' | 'heading' | 'caption';
}
type TextProps<E extends ElementType> = TextOwnProps<E> &
Omit<ComponentPropsWithoutRef<E>, keyof TextOwnProps>;
export function Text<E extends ElementType = 'span'>({
as,
children,
variant = 'body',
className = '',
...restProps
}: TextProps<E>) {
const Component = as || 'span';
return (
<Component className={`text text-${variant} ${className}`} {...restProps}>
{children}
</Component>
);
}
Common mistakes
- Typing
asas a string (as?: string), which forfeits tag-specific prop validation (allowinghrefon<div>). - Forgetting to use
Omit, which causes compiler errors if a custom prop shares a name with a native HTML prop.
Likely follow-up questions
- Interviewer: "How do you add ref forwarding to a polymorphic component?"
- Answer: "Polymorphic ref forwarding requires typing the ref as
React.ComponentPropsWithRef<E>['ref']. Because TypeScript'sforwardRefdoes not easily support generic parameters out of the box, you typically cast the component through a dedicated polymorphic forward-ref helper type."
Q10. How do you leverage TypeScript utility types (Pick, Omit, Partial, Extract) for React prop transformations?
Level: Mid-Senior
What the interviewer is checking: Proficiency in composing and transforming existing component prop interfaces cleanly without duplication.
Short answer to say out loud
"TypeScript utility types allow you to derive new component prop interfaces from existing ones without duplicating code. Pick selects specific props from a larger interface, Omit removes unwanted props, Partial makes all props optional (useful for default prop overrides), and Extract pulls specific subtypes out of union types. In React, Omit and Pick are especially valuable when wrapping third-party components or native HTML elements."
Detailed explanation
Utility types derive prop interfaces cleanly: Pick<T, K> extracts specific properties for child views, Omit<T, K> removes properties to allow custom overrides, and Partial<T> marks fields optional for form draft states.
Example
import type { ComponentPropsWithoutRef } from 'react';
interface UserProfile {
id: string;
name: string;
email: string;
avatarUrl: string;
role: 'admin' | 'editor' | 'viewer';
}
// 1. Pick: AvatarCard only needs a subset of user fields
type AvatarCardProps = Pick<UserProfile, 'name' | 'avatarUrl' | 'role'>;
export function AvatarCard({ name, avatarUrl, role }: AvatarCardProps) {
return (
<div className="avatar-card">
<img src={avatarUrl} alt={name} />
<h4>{name}</h4>
<span>{role}</span>
</div>
);
}
// 2. Omit: CustomInput wraps native input but redefines value/onChange
interface TextInputProps
extends Omit<ComponentPropsWithoutRef<'input'>, 'value' | 'onChange'> {
value: string;
onChange: (newValue: string) => void;
label: string;
}
export function TextInput({ value, onChange, label, ...rest }: TextInputProps) {
return (
<label>
{label}
<input
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
{...rest}
/>
</label>
);
}
Common mistakes
- Redefining identical interfaces across multiple files instead of deriving them with
PickorOmit. - Overusing
Partialon component props, which can hide required properties and lead to runtime undefined errors.
Likely follow-up questions
- Interviewer: "What is the difference between
ExcludeandOmit?" - Answer: "
Exclude<Union, Members>operates on union types to remove matching members.Omit<Type, Keys>operates on object types to remove specific property keys."
Preparation Checklist
Before heading into your TypeScript for React interview, review this actionable preparation checklist:
- Master primitive React types: Differentiate
ReactNode,ReactElement, andJSX.Element. UseReactNodefor children. - Type event handlers accurately: Annotate synthetic events (
ChangeEvent<HTMLInputElement>,MouseEvent<HTMLButtonElement>). - Extend HTML props cleanly: Use
ComponentPropsWithoutRef<'button'>to build native element wrappers without prop duplication. - Eliminate impossible states: Implement discriminated unions with literal discriminant keys (
status: 'loading' | 'success' | 'error') rather than independent booleans. - Build generic components: Practise writing generic lists and tables with
<T,>syntax and type constraints. - Master useRef overloads: Initialize DOM refs with
null(useRef<HTMLDivElement>(null)) and value refs with the starting value. - Protect hook return tuples: Always append
as constto custom hook return arrays to preserve strict tuple inference. - Polymorphic component patterns: Understand the
asprop generic pattern (E extends React.ElementType) and whyOmitis needed. - Practice utility type derivations: Use
Pick,Omit, andPartialto compose prop types cleanly across component boundaries.
Conclusion
Demonstrating strong TypeScript fluency in a React technical interview shows that you can design durable, self-documenting component APIs that prevent runtime bugs before code is even deployed. By mastering discriminated unions, generic components, and precise DOM typing, you can speak confidently to trade-offs and showcase senior-level engineering rigor.