Y
Published on

Mastering Advanced Generic Types in TypeScript

Authors
  • avatar
    Name
    Yinhuan Yuan
    Twitter

Introduction

TypeScript's type system is one of its most powerful features, and generics are at the heart of writing reusable, type-safe code. While basic generics are relatively straightforward, TypeScript offers a range of advanced generic techniques that can help solve complex typing problems. In this post, we'll explore some of these advanced concepts.

1. Conditional Types

Conditional types allow you to create types that depend on other types. They're defined using a structure similar to ternary operators in JavaScript.

type IsArray<T> = T extends any[] ? true : false

type WithArray = IsArray<string[]> // true
type WithoutArray = IsArray<number> // false

Conditional types become particularly powerful when combined with infer:

type UnpackArray<T> = T extends Array<infer U> ? U : T

type Unpacked1 = UnpackArray<string[]> // string
type Unpacked2 = UnpackArray<number> // number

2. Mapped Types

Mapped types allow you to create new types based on old ones by transforming properties.

type Readonly<T> = {
  readonly [P in keyof T]: T[P]
}

interface Mutable {
  x: number
  y: string
}

type Immutable = Readonly<Mutable>
// Equivalent to: { readonly x: number; readonly y: string; }

You can also remove properties or change their optionality:

type Optional<T> = {
  [P in keyof T]?: T[P]
}

type Required<T> = {
  [P in keyof T]-?: T[P]
}

3. Template Literal Types

Template literal types allow you to manipulate string-based types:

type World = 'world'
type Greeting = `hello ${World}` // "hello world"

type EmailLocaleIDs = 'welcome_email' | 'email_heading'
type FooterLocaleIDs = 'footer_title' | 'footer_sendoff'

type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`
// "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"

4. Recursive Types

TypeScript allows you to create recursive type aliases, which can be useful for defining tree-like structures:

type TreeNode<T> = {
  value: T
  left?: TreeNode<T>
  right?: TreeNode<T>
}

let tree: TreeNode<number> = {
  value: 1,
  left: {
    value: 2,
    left: {
      value: 4,
    },
  },
  right: {
    value: 3,
  },
}

5. Higher-Order Types

You can create "higher-order types" that operate on other types, similar to how higher-order functions operate on other functions:

type ArrayType<T> = T[]
type StringArray = ArrayType<string> // string[]

type Func<T> = (arg: T) => T
type StringFunc = Func<string> // (arg: string) => string

6. Variadic Tuple Types

TypeScript 4.0 introduced variadic tuple types, allowing you to work with tuples of arbitrary length:

type Tail<T extends any[]> = T extends [any, ...infer U] ? U : never

type TailResult = Tail<[string, number, boolean]> // [number, boolean]

7. Key Remapping in Mapped Types

TypeScript 4.1 added the ability to remap keys in mapped types:

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}

interface Person {
  name: string
  age: number
}

type PersonGetters = Getters<Person>
// { getName: () => string; getAge: () => number; }

8. Generic Type Inference in Conditional Types

You can use the infer keyword to infer types within conditional types:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any

function add(a: number, b: number): number {
  return a + b
}

type AddReturnType = ReturnType<typeof add> // number

9. Distributive Conditional Types

Conditional types are distributive over union types:

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

type StrNumBool = string | number | boolean
type StrNumBoolArray = ToArray<StrNumBool> // string[] | number[] | boolean[]

10. Indexed Access Types

You can use indexed access types to look up a specific property on another type:

type Person = { age: number; name: string; alive: boolean }
type Age = Person['age'] // number

type I1 = Person['age' | 'name'] // string | number

type I2 = Person[keyof Person] // string | number | boolean

Conclusion

These advanced generic techniques in TypeScript provide powerful tools for creating flexible, reusable, and type-safe code. While they can be complex, mastering these concepts allows you to express intricate type relationships and create highly abstract, yet type-safe libraries and applications.

Remember, with great power comes great responsibility. While these techniques are powerful, they can also make your code harder to understand if overused. Always strive for a balance between type safety and code readability.

Happy TypeScripting!