learn
TypeScript

TypeScript in a React app and an API call

Typing props, state, events, a fetch hook and an API response.

Components are functions with typed inputs; API calls are promises with typed outputs. Interviewers ask you to type both live — props with an interface, fetch with a generic envelope, and state that cannot hold the wrong shape.

Real-life situation

A login form takes a title and an onLogin callback, fetches the user, and renders the name. Untyped, the callback receives whatever the form feels like and the render trusts whatever the network sent. Typed, props are checked at every usage, the response envelope is generic, and a runtime guard stands where the network ends and your code begins.

Visual

Typed component data
BrowserAPIRealtimeCacheDatabaseShareddurableephemeral

Try it

Fetch a user into a typed envelope

Checked with TypeScript 6.0.3, strict mode

typescriptTab moves to the next control
interface ApiUser {
id: number;
name: string;
email: string;
}
interface ApiResponse<T> {
data: T;
status: number;
}
async function loadUser(id: number): Promise<ApiResponse<ApiUser>> {
const response = await fakeFetch("https://api.example.com/users/" + id);
const data = (await response.json()) as ApiUser;
return { data: data, status: response.status };
}
function isUser(value: unknown): value is ApiUser {
return (
  typeof value === "object" &&
  value !== null &&
  "name" in value &&
  "email" in value
);
}
loadUser(1).then(function (res: ApiResponse<ApiUser>) {
console.log(res.status + " " + res.data.name);
console.log(isUser(res.data) ? "valid" : "invalid");
const bad: ApiUser = res.data.name;
console.log(bad);
});

Compiler not loaded. Check or Run loads it here (about 9 MB) — nothing downloads until you do.

Runner helpers

sleep(ms) waits; fakeFetch(url) returns deterministic sample data. TypeScript is stripped before running; imports and exports are not available in the sandbox.

Try fixing bad to res.data — clean. Props typing follows the same shape-first habit, shown statically below with small declared helpers standing in for React (no imports in the sandbox):

interface LoginProps {
  title: string;
  onLogin: (email: string) => void;
}
function useState<S>(initial: S): [S, (next: S) => void] {
  let current: S = initial;
  function set(next: S): void {
    current = next;
  }
  return [current, set];
}
function onSubmit(
  handler: (event: { preventDefault: () => void }) => void,
): void {
  handler({
    preventDefault: function (): void {},
  });
}
function LoginForm(props: LoginProps): string {
  const [email, setEmail] = useState("");
  setEmail("sam@example.com");
  onSubmit(function (event) {
    event.preventDefault();
    props.onLogin(email);
  });
  return props.title + ": " + email;
}

The event parameter needed no annotation — context provides it. State keeps the type of its initial value. And the guard (isUser) is the honest part: types describe your claims, but only a runtime check confronts the network.

Will it compile? — decide first, then reveal.

check.ts
interface CardProps {
title: string;
count: number;
}
function renderCard(props: CardProps): string {
return props.title + props.count;
}
renderCard({ count: 3 });

Interview questions

How do you type React component props?basic

You define a Props interface or type and annotate the component, like function Card({ title }: { title: string }). Required data stays required, optional UI gets ?, and mismatched usages fail at the call site instead of rendering broken output. Keep the Props type next to the component and name it after the component. For children, add children?: React.ReactNode explicitly.

Interviewers also ask

  • How do you type children?
How do you type useState in React?basic

You pass the value type as a generic, like useState<User | null>(null), so the state and setter stay checked. Inference handles the easy case — useState('') is already string — but empty or nullable initial values need the explicit argument to avoid widening to null or never[]. The generic documents what the state will become, not just what it starts as.

Interviewers also ask

  • When does useState need an explicit generic?
How do you type React event handlers?basic

You annotate the event with its React type, like (e: React.ChangeEvent<HTMLInputElement>). The generic names the element, so e.target.value is correctly a string. Inline handlers usually infer the event from the JSX prop, so explicit annotations matter most in extracted helpers. Use React.FormEvent for submits and React.MouseEvent for clicks, and call e.preventDefault() where the behavior needs it.

Interviewers also ask

  • How do you type an input change handler?
How do you type a fetch call with ApiResponse<T>?intermediate

You wrap the payload in a generic envelope like interface ApiResponse<T> { data: T; error: string | null }, then write fetchUser(): Promise<ApiResponse<User>>. The generic keeps every endpoint's data type precise while sharing one error and loading shape. Callers narrow on error before reading data. Keep the wrapper small — status, data, and error — so it fits every endpoint without sprouting per-route variants.

Interviewers also ask

  • How does the caller know whether data is safe to read?
Why must you validate API data at runtime even with TypeScript?intermediate

fetch returns any-shaped JSON the compiler cannot see, so asserting it with as User proves nothing — a changed backend still flows through. Add a small validator or schema check where the response enters your app and narrow to the typed shape only after it passes. That one boundary guard protects every downstream component. Treat network data as unknown until proven otherwise, no matter how precise your interfaces look.

Interviewers also ask

  • Where exactly should the validation live?
How do you type loading and error states for data fetching?intermediate

You model the states explicitly, often as a discriminated union like { status: 'loading' } | { status: 'ok'; data: User } | { status: 'error'; message: string }. The component switches on status and each branch sees only the fields that exist there. That removes impossible states — no data access while loading. Keep the union next to the fetch helper so UI and data logic share one definition of what can happen.

Interviewers also ask

  • Why is a status union safer than separate booleans?
How do you type a ref to a DOM element?intermediate

You parameterize the ref with the element type, like useRef<HTMLInputElement>(null), so .current is checked on every use. Because the ref starts empty, its type includes null and you must guard before calling methods like .focus(). Callback refs and non-null assertions have their place, but an explicit guard reads best. Use refs for focus, measurement, and uncontrolled inputs — not as a second state system.

Interviewers also ask

  • Why must you check ref.current for null?

Key takeaways

  • Props are interfaces; events and state inherit types from context.
  • Wrap fetch in a generic envelope, then guard the payload at runtime.
  • A green check never proves the network behaved — validate at the edge.

References

On this page