Home › Technical › TypeScript

TypeScript

The JD calls out "deep expertise" in TypeScript. Stage 3 will probe this. The good news: most of it you already do. This page is the vocabulary check.

01The basics fast

type vs interface — when do you reach for each?

For most cases they're interchangeable. The differences:

  • interface is open — declarations with the same name merge. That makes it good for public API contracts (you might extend), bad if you want sealed shapes.
  • type is closed and more flexible — supports unions, intersections, mapped types, conditional types, tuples.

My rule: interface for object shapes that might be extended, type for everything else (unions, function types, complex compositions). Either is defensible if you're consistent.

any vs unknown vs never?

any turns off type checking. Anything goes. Avoid.

unknown is the type-safe "I don't know what this is yet" — you can hold any value but you can't do anything with it until you narrow it via a type guard.

never is the type that has no values. It's what you get from throw, infinite loops, and exhausted unions in switch. Useful for exhaustiveness checks.

function assertNever(x: never): never { throw new Error(`Unhandled: ${x}`); }

switch (kind) {
  case 'a': return ...;
  case 'b': return ...;
  default: return assertNever(kind); // compiler error if you add a 'c'
}
Generics — quick example?

Generics let you write code that works over a parameterized type. The classic example:

function identity<T>(x: T): T { return x; }

identity<string>('hi'); // T inferred or specified
identity(42);            // T inferred as number

Real use: a fetch wrapper, a state store, a list component. Anywhere you want to preserve the input's type through to the output.

02Useful built-in utilities

Walk me through the utility types you use most.

The ones I reach for daily:

  • Partial<T> — all keys optional. Useful for "update" payloads.
  • Required<T> — all keys required.
  • Pick<T, K> — subset of keys. Pick<User, 'id' | 'name'>
  • Omit<T, K> — everything except those keys.
  • Record<K, V> — object shape { [k in K]: V }.
  • Readonly<T> — all properties readonly.
  • ReturnType<F> — extract the return type of a function.
  • Awaited<T> — unwrap a Promise.
  • NonNullable<T> — strip null and undefined.
What's a discriminated union?

A union type where each variant has a literal-type field that lets the compiler narrow which one you're holding.

type Result<T>
  = { status: 'success'; data: T }
  | { status: 'error'; error: Error }
  | { status: 'loading' };

function render(r: Result<User>) {
  switch (r.status) {
    case 'success': return r.data.name;   // narrowed to success
    case 'error':   return r.error.message;
    case 'loading': return 'Loading…';
  }
}

This is the right way to model state machines, async results, and any "one of N shapes" pattern. Pairs perfectly with exhaustiveness checks.

What are conditional types? Mapped types?

Conditional types: T extends U ? X : Y. Lets the type system branch.

type IsString<T> = T extends string ? true : false;
type A = IsString<'hi'>;  // true
type B = IsString<42>;    // false

Mapped types: transform every key of a type.

type Optional<T> = { [K in keyof T]?: T[K] };
type Stringify<T> = { [K in keyof T]: string };

You don't write these every day, but you'll read them in libraries (e.g., react-hook-form, TanStack Query types). Worth being able to read fluently.

03React + TypeScript

How do you type props with children?
type Props = {
  title: string;
  children: React.ReactNode;
};

function Card({ title, children }: Props) { ... }

React.ReactNode is the right type — accepts strings, numbers, elements, fragments, arrays, nullish.

Avoid the old React.FC — it implicitly adds children, which is wrong for components that don't accept children, and the API has bounced around versions.

How do you type an event handler?
// Common ones
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => ...;
const onClick  = (e: React.MouseEvent<HTMLButtonElement>) => ...;
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => ...;

// In JSX, types are inferred
<input onChange={(e) => setName(e.target.value)} /> // e is correctly typed
How do you type a generic component?
type ListProps<T> = {
  items: T[];
  render: (item: T) => React.ReactNode;
};

function List<T>({ items, render }: ListProps<T>) {
  return <ul>{items.map((it, i) => <li key={i}>{render(it)}</li>)}</ul>;
}

// Usage — T inferred from `items`
<List items={users} render={(u) => u.name} />

Useful for Table, List, Select, anywhere the component shape doesn't depend on the data shape.

How do you type a useReducer?
type State = { count: number };
type Action =
  | { type: 'inc' }
  | { type: 'set'; value: number };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'inc': return { count: state.count + 1 };
    case 'set': return { count: action.value };
  }
}

const [state, dispatch] = useReducer(reducer, { count: 0 });
dispatch({ type: 'set', value: 5 });

04Common pitfalls

What's wrong with this?
function getName(user: { name?: string }) {
  return user.name.toUpperCase();
}

name is optional — could be undefined — so calling .toUpperCase() errors. Fix: narrow it (if (!user.name) return ''), provide a default (user.name ?? ''), or use the optional chain (user.name?.toUpperCase()).

This is exactly what TypeScript's strictNullChecks is supposed to catch. Make sure that's on.

Type assertion (as) — when is it OK?

When you genuinely have information the compiler can't know — narrowing the result of document.getElementById, deserializing an API response you've already validated, telling TS that an enum is exhaustive.

When it's NOT OK: silencing errors you should be fixing, casting around bad types because you're in a hurry. as any in production code is almost always a smell.

Prefer narrowing via type guards over assertions:

function isUser(x: unknown): x is User {
  return typeof x === 'object' && x !== null && 'id' in x;
}
What's satisfies?

Newer TypeScript operator that checks a value matches a type without widening it.

const colors = {
  red: '#f00',
  green: '#0f0',
} satisfies Record<string, string>;

colors.red.toUpperCase(); // OK — inferred as the literal '#f00'

Without satisfies, if you typed it as Record<string, string> directly, you'd lose the literal types and the specific keys. With satisfies, you get both the constraint check and the narrow inferred type.

05Project / config knowledge

What's in your tsconfig.json for a serious project?

The flags I always want on:

  • strict: true — turns on the strict family
  • noImplicitAny
  • strictNullChecks
  • noUncheckedIndexedAccess — array access returns T | undefined, prevents off-by-one bugs
  • noFallthroughCasesInSwitch
  • noUnusedLocals / noUnusedParameters
  • exactOptionalPropertyTypes — distinguishes {x?: T} from {x: T | undefined}

For a monorepo: project references and composite mode for incremental builds. paths for clean imports.

06Worth practicing live

Stage 3 may give you a small typed-API exercise. The most likely shape:

  • Type a generic useFetch hook
  • Build a discriminated union for an async result
  • Type a Table component that infers column accessors from row data
  • Convert a JS file to TypeScript and explain your decisions