JavaScript & TypeScript Mastery

TypeScript Interview Patterns

4 min read

TypeScript is now a hard requirement for most frontend roles. Interviews test your ability to use the type system to write safer, more maintainable code — not just adding : string annotations.

Before the utility types, the question you are most likely to be asked first, and the one most candidates answer with a shrug:

`interface` or `type` — the answer that is not 'they're basically the same'

extensible by design

interface

DescribesObject shapes and class contracts only
MergesYes — declaration merging
Reach for it whenPublishing a type others extend
Pros
  • Declaration merging lets consumers augment a shape you shipped — this is how you add fields to a library's types without forking it
  • `implements` on a class reads naturally, and the error messages when a class breaks the contract are clearer
  • Extending several interfaces is checked eagerly, so conflicts surface at the declaration rather than at the use site
Cons
  • Cannot express a union, a tuple, a mapped type, or a conditional type at all
  • Declaration merging is silent — two files can widen the same interface with nobody noticing, which is a feature in a library and a hazard in an app
  • No way to alias a primitive, so `type UserId = string` has no interface equivalent
the whole type system

type

DescribesAnything a type expression can produce
MergesNo — redeclaring is an error
Reach for it whenDeriving a type from another type
Pros
  • Unions, intersections, tuples, mapped and conditional types are only expressible here — every utility type in the next section is a `type`
  • Redeclaration is a compile error, so a type cannot be widened behind your back
  • Composes with `keyof`, `typeof`, and template literals, which is where the interesting type-level work happens
Cons
  • No declaration merging, so consumers of a published type cannot augment it — they must wrap it
  • Deeply composed aliases produce error messages that expand into unreadable structural output
  • Nothing stops you writing a clever type that nobody on the team can modify six months later

The sentence to say out loud: "Interface when the shape is a contract others might extend, type when I am computing a type from another type. In application code the distinction rarely bites, so I follow whatever the codebase already does." That last clause is what separates an answer from a recital.

Utility Types

These built-in types appear constantly in interviews. Know them by heart:

interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user' | 'guest';
  createdAt: Date;
}

// Pick: select specific properties
type UserPreview = Pick<User, 'id' | 'name'>;
// { id: number; name: string }

// Omit: exclude specific properties
type UserWithoutDates = Omit<User, 'createdAt'>;
// { id: number; name: string; email: string; role: ... }

// Partial: make all properties optional
type UpdateUser = Partial<User>;
// { id?: number; name?: string; email?: string; ... }

// Required: make all properties required
type CompleteUser = Required<Partial<User>>;
// Back to all required

// Record: create an object type with specific key-value types
type RolePermissions = Record<User['role'], string[]>;
// { admin: string[]; user: string[]; guest: string[] }

// Readonly: make all properties read-only
type FrozenUser = Readonly<User>;
// Cannot reassign any properties

Generics

Generics let you write reusable, type-safe code:

// Basic generic function
function getFirst<T>(arr: T[]): T | undefined {
  return arr[0];
}

getFirst([1, 2, 3]);      // number
getFirst(['a', 'b']);      // string

// Generic with constraints
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user: User = { id: 1, name: 'Alice', email: 'a@b.com', role: 'admin', createdAt: new Date() };
getProperty(user, 'name');  // string
getProperty(user, 'role');  // 'admin' | 'user' | 'guest'
// getProperty(user, 'age'); // Error: 'age' is not in keyof User

Conditional Types

These are powerful for building flexible type utilities:

// Basic conditional type
type IsString<T> = T extends string ? true : false;

type A = IsString<string>;  // true
type B = IsString<number>;  // false

// Extract non-nullable types
type NonNullable<T> = T extends null | undefined ? never : T;

type C = NonNullable<string | null | undefined>;  // string

// Infer keyword: extract types from other types
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type D = ReturnType<() => string>;  // string
type E = ReturnType<(x: number) => boolean>;  // boolean

// Practical example: extract Promise inner type
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;

type F = Awaited<Promise<string>>;  // string
type G = Awaited<Promise<Promise<number>>>;  // number

Discriminated Unions

A pattern that comes up frequently in React component props:

// Instead of optional props with complex conditions,
// use discriminated unions
type ButtonProps =
  | { variant: 'link'; href: string; onClick?: never }
  | { variant: 'button'; onClick: () => void; href?: never }
  | { variant: 'submit'; form: string; href?: never; onClick?: never };

function Button(props: ButtonProps) {
  switch (props.variant) {
    case 'link':
      return <a href={props.href}>Link</a>;
    case 'button':
      return <button onClick={props.onClick}>Click</button>;
    case 'submit':
      return <button type="submit" form={props.form}>Submit</button>;
  }
}

// TypeScript narrows the type in each case branch
// props.href is only accessible when variant is 'link'

Type Guards

Custom type guards refine types at runtime:

// Type predicate with `is`
function isString(value: unknown): value is string {
  return typeof value === 'string';
}

// Using `in` operator
interface Dog { bark(): void; }
interface Cat { meow(): void; }

function isDog(pet: Dog | Cat): pet is Dog {
  return 'bark' in pet;
}

// Exhaustive checking with `never`
type Shape = { kind: 'circle'; radius: number }
           | { kind: 'square'; side: number }
           | { kind: 'triangle'; base: number; height: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2;
    case 'square':
      return shape.side ** 2;
    case 'triangle':
      return 0.5 * shape.base * shape.height;
    default:
      // If a new shape is added but not handled,
      // TypeScript will error here at compile time
      const _exhaustive: never = shape;
      return _exhaustive;
  }
}

Template Literal Types

Useful for creating precise string types:

type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type APIPath = '/users' | '/posts' | '/comments';

// Combine into route strings
type APIRoute = `${HTTPMethod} ${APIPath}`;
// 'GET /users' | 'GET /posts' | ... (12 combinations)

// Practical: CSS property types
type CSSUnit = 'px' | 'rem' | 'em' | '%' | 'vh' | 'vw';
type CSSValue = `${number}${CSSUnit}`;

const width: CSSValue = '100px';   // valid
const height: CSSValue = '50rem';  // valid
// const bad: CSSValue = 'auto';   // Error

Interview tip: When asked to "improve the type safety" of a piece of code, look for opportunities to use discriminated unions, generics with constraints, and template literal types. These show deep TypeScript understanding beyond basic annotations.

Next, we'll cover async patterns and error handling — the other major JavaScript topic that trips up candidates. :::

Quiz

Module 2: JavaScript & TypeScript Mastery

Take Quiz
Was this lesson helpful?

Sign in to rate