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.

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