⚡ TECH BLOG
Home
Blog
Tags
About
⚡

Powered by Next.js 15 & Modern Web Tech ⚡

Back to Home

TypeScript Advanced Types: Mapped Types, Conditionals, and More

June 15, 2021
typescriptjavascriptfrontendprogramming
TypeScript Advanced Types: Mapped Types, Conditionals, and More

TypeScript Advanced Types: Mapped Types, Conditionals, and More

TypeScript's type system is incredibly powerful. Let's explore advanced features that will help you write more robust and expressive code.

Mapped Types

Mapped types allow you to create new types based on existing ones.

Basic Mapped Types

// Make all properties readonly
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

// 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 mutable
type Mutable<T> = {
  -readonly [P in keyof T]: T[P];
};

interface User {
  readonly id: number;
  name: string;
  age?: number;
}

type ReadonlyUser = Readonly<User>;
type PartialUser = Partial<User>;
type RequiredUser = Required<User>;
type MutableUser = Mutable<User>;

Custom Mapped Types

// Get the type of a specific property
type PropertyType<T, K extends keyof T> = T[K];

// Create a type with only string properties
type StringProperties<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

interface Config {
  host: string;
  port: number;
  debug: boolean;
  apiKey: string;
}

type StringConfig = StringProperties<Config>;
// { host: string; apiKey: string; }

// Rename properties
type GetterNames<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type UserGetters = GetterNames<User>;
// { getName: () => string; getAge: () => number | undefined; ... }

Conditional Types

Conditional types select types based on conditions.

Basic Conditional Types

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

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

// Check for array
type IsArray<T> = T extends any[] ? true : false;

type C = IsArray<string[]>; // true
type D = IsArray<string>;   // false

infer Keyword

The infer keyword allows you to extract types:

// Extract return type of a function
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function greet(): string { return 'hello'; }
type GreetReturn = ReturnType<typeof greet>; // string

// Extract function parameters
type Parameters<T> = T extends (...args: infer P) => any ? P : never;

function add(a: number, b: string): void {}
type AddParams = Parameters<typeof add>; // [number, string]

// Extract array element type
type ElementType<T> = T extends (infer E)[] ? E : never;

type E = ElementType<string[]>; // string
type F = ElementType<number[]>; // number

// Extract Promise value
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;

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

Distributive Conditional Types

type ToArray<T> = T extends any ? T[] : never;

type StrArr = ToArray<string>; // string[]
type NumArr = ToArray<number>; // number[]

// When T is a union, it distributes
type StrOrNumArr = ToArray<string | number>; // string[] | number[]

// Non-distributive with tuple
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type StrOrNumArr2 = ToArrayNonDist<string | number>; // (string | number)[]

Template Literal Types

Template literal types manipulate string types:

// Basic template literal
type Greeting = `hello ${string}`;
const a: Greeting = 'hello world'; // OK
const b: Greeting = 'hi there';    // Error!

// String manipulation
type UppercaseName = Uppercase<'hello'>;    // 'HELLO'
type LowercaseName = Lowercase<'HELLO'>;    // 'hello'
type CapitalizeName = Capitalize<'hello'>;  // 'Hello'
type UncapitalizeName = Uncapitalize<'Hello'>; // 'hello'

// Combine with mapped types
type EventName<T extends string> = `on${Capitalize<T>}`;

type MouseEvents = EventName<'click' | 'mousedown' | 'mouseup'>;
// 'onClick' | 'onMousedown' | 'onMouseup'

// Get all possible routes
type Routes = '/home' | '/about' | '/contact';
type ApiRoutes = `/api${Routes}`;
// '/api/home' | '/api/about' | '/api/contact'

Advanced Template Literals

// Extract part of a string
type ExtractPath<S extends string> = S extends `/${infer Path}` ? Path : never;

type P = ExtractPath<'/users/123'>; // 'users/123'

// Parse route parameters
type RouteParams<Route extends string> = 
  Route extends `${string}:${infer Param}/${infer Rest}`
    ? Param | RouteParams<`/${Rest}`>
    : Route extends `${string}:${infer Param}`
    ? Param
    : never;

type Params = RouteParams<'/users/:id/posts/:postId'>;
// 'id' | 'postId'

Utility Types

Built-in Utilities

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

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

// Omit specific properties
type UserWithoutEmail = Omit<User, 'email'>;
// { id: number; name: string; age: number; }

// Extract from union
type T = Extract<'a' | 'b' | 'c', 'a' | 'b'>; // 'a' | 'b'

// Exclude from union
type U = Exclude<'a' | 'b' | 'c', 'a' | 'b'>; // 'c'

// Non-nullable
type V = NonNullable<string | null | undefined>; // string

// Record type
type UserRoles = Record<'admin' | 'user' | 'guest', { permissions: string[] }>;

Custom Utility Types

// Deep partial
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

interface NestedConfig {
  server: {
    host: string;
    port: number;
  };
  database: {
    url: string;
    pool: {
      min: number;
      max: number;
    };
  };
}

type PartialConfig = DeepPartial<NestedConfig>;

// Deep required
type DeepRequired<T> = {
  [P in keyof T]-?: T[P] extends object ? DeepRequired<T[P]> : T[P];
};

// Merge types
type Merge<A, B> = Omit<A, keyof B> & B;

type Base = { id: number; name: string };
type Extra = { name: string; email: string };
type Merged = Merge<Base, Extra>;
// { id: number; name: string; email: string }

// Make specific keys required
type RequireKeys<T, K extends keyof T> = T & Required<Pick<T, K>>;

interface OptionalUser {
  id?: number;
  name?: string;
  email?: string;
}

type UserWithId = RequireKeys<OptionalUser, 'id'>;
// { id: number; name?: string; email?: string; }

Type Guards and Narrowing

Type Guards

// typeof guard
function process(value: string | number) {
  if (typeof value === 'string') {
    return value.toUpperCase(); // string
  }
  return value * 2; // number
}

// instanceof guard
class Dog { bark() {} }
class Cat { meow() {} }

function makeSound(animal: Dog | Cat) {
  if (animal instanceof Dog) {
    animal.bark();
  } else {
    animal.meow();
  }
}

// in operator guard
interface Bird { fly(): void; }
interface Fish { swim(): void; }

function move(animal: Bird | Fish) {
  if ('fly' in animal) {
    animal.fly();
  } else {
    animal.swim();
  }
}

Custom Type Guards

interface User {
  id: number;
  name: string;
}

function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    'id' in value &&
    'name' in value &&
    typeof (value as User).id === 'number' &&
    typeof (value as User).name === 'string'
  );
}

function processValue(value: unknown) {
  if (isUser(value)) {
    console.log(value.name); // TypeScript knows it's User
  }
}

// Asserts functions
function assertDefined<T>(value: T | null | undefined): asserts value is T {
  if (value === null || value === undefined) {
    throw new Error('Value is null or undefined');
  }
}

function processString(value: string | null) {
  assertDefined(value);
  console.log(value.toUpperCase()); // value is string
}

Practical Examples

API Response Types

type ApiResponse<T> = {
  data: T;
  status: 'success' | 'error';
  message: string;
  timestamp: number;
};

type PaginatedResponse<T> = ApiResponse<T[]> & {
  pagination: {
    page: number;
    limit: number;
    total: number;
  };
};

// Type-safe fetch
async function fetchApi<T>(url: string): Promise<ApiResponse<T>> {
  const response = await fetch(url);
  return response.json();
}

const users = await fetchApi<User[]>('/api/users');

Type-safe Event Emitter

type EventMap = {
  click: { x: number; y: number };
  focus: { element: HTMLElement };
  change: { value: string };
};

class TypedEventEmitter<T extends Record<string, any>> {
  private listeners = new Map<keyof T, Set<Function>>();

  on<K extends keyof T>(event: K, listener: (data: T[K]) => void) {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }
    this.listeners.get(event)!.add(listener);
  }

  emit<K extends keyof T>(event: K, data: T[K]) {
    this.listeners.get(event)?.forEach(listener => listener(data));
  }
}

const emitter = new TypedEventEmitter<EventMap>();
emitter.on('click', (data) => console.log(data.x)); // Type-safe!
emitter.emit('click', { x: 100, y: 200 });

Conclusion

TypeScript's advanced type system provides powerful tools for creating robust, type-safe applications. Mapped types, conditional types, and template literal types enable you to build complex type relationships while maintaining type safety. Practice these patterns to write more expressive and safer code!

Share:

💬 Comments