TypeScript Advanced Types

Overview

TypeScript’s type system is one of the most powerful in mainstream programming languages. This note covers advanced type features that can make your code safer and more expressive.

Conditional Types

What Are Conditional Types?

Conditional types allow you to create types that depend on other types.

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

Real-World Use

type ApiResponse<T> = T extends "user" 
  ? UserData 
  : T extends "post" 
    ? PostData 
    : never;

Mapped Types

Transforming Types

Mapped types let you create new types by transforming existing ones.

// Make all properties optional
type Partial<T> = {
  [P in keyof T]?: T[P];
};
 
// Make all properties required
type Required<T> = {
  [P in keyof T]-?: T[P];
};
 
// Make all properties readonly
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

Practical Example

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}
 
type UserUpdate = Partial<User>;  // All fields optional
type UserPreview = Pick<User, "id" | "name">;  // Only id and name
type UserWithoutEmail = Omit<User, "email">;  // All except email

Don't Overuse

Complex mapped types can be hard to read. Use them judiciously.

Template Literal Types

String Manipulation

TypeScript can manipulate string types at the type level.

type EventName = "click" | "focus" | "blur";
type HandlerName = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"
 
type CSSProperty = "margin" | "padding";
type CSSDirection = "top" | "right" | "bottom" | "left";
type CSSSpacing = `${CSSProperty}-${CSSDirection}`;
// "margin-top" | "margin-right" | ... | "padding-left"

Real-World Use

type Route = "/users" | "/posts" | "/comments";
type ApiRoute = `/api${Route}`;
// "/api/users" | "/api/posts" | "/api/comments"

Discriminated Unions

Pattern Matching

Discriminated unions enable exhaustive pattern matching.

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle"; base: number; height: number };
 
function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "rectangle":
      return shape.width * shape.height;
    case "triangle":
      return (shape.base * shape.height) / 2;
  }
}

Exhaustiveness Checking

function assertNever(x: never): never {
  throw new Error("Unexpected value");
}
 
function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    // ... other cases
    default:
      return assertNever(shape); // Compile error if case missing
  }
}

Utility Types

Built-in Helpers

TypeScript provides many utility types:

TypePurposeExample
Partial<T>All optionalPartial<User>
Required<T>All requiredRequired<Config>
Readonly<T>All readonlyReadonly<State>
Pick<T, K>Select fields`Pick<User, “id"
Omit<T, K>Exclude fieldsOmit<User, "password">
Record<K, V>Key-value mapRecord<string, number>
Exclude<T, U>Remove types`Exclude<“a"
Extract<T, U>Keep types`Extract<“a"
NonNullable<T>Remove null`NonNullable<string
ReturnType<T>Function returnReturnType<typeof fn>
Parameters<T>Function paramsParameters<typeof fn>

Infer Keyword

Type Inference

The infer keyword lets you extract types from other types.

// Extract array element type
type ElementOf<T> = T extends (infer E)[] ? E : never;
 
type A = ElementOf<string[]>;  // string
type B = ElementOf<number[]>;  // number
 
// Extract function return type
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
 
// Extract promise type
type Unwrap<T> = T extends Promise<infer U> ? U : T;
 
type C = Unwrap<Promise<string>>;  // string
type D = Unwrap<number>;           // number

Branded Types

Type Safety for Primitives

Branded types prevent mixing up similar primitive types.

type UserId = string & { __brand: "UserId" };
type PostId = string & { __brand: "PostId" };
 
function createUserId(id: string): UserId {
  return id as UserId;
}
 
function getUser(id: UserId): User {
  // ...
}
 
const userId = createUserId("123");
const postId = "456" as PostId;
 
getUser(userId);  // ✅ Works
getUser(postId);  // ❌ Compile error

Type Assertions

Avoid as assertions when possible. Branded types are safer.

Real-World Patterns

API Response Pattern

type SuccessResponse<T> = {
  status: "success";
  data: T;
};
 
type ErrorResponse = {
  status: "error";
  message: string;
  code: number;
};
 
type ApiResponse<T> = SuccessResponse<T> | ErrorResponse;
 
function handleResponse<T>(response: ApiResponse<T>) {
  if (response.status === "success") {
    // TypeScript knows response.data exists
    console.log(response.data);
  } else {
    // TypeScript knows response.message exists
    console.error(response.message);
  }
}

See Also


*Tags: typescript javascript programming types