- Published on
Mastering Advanced Generic Types in TypeScript
- Authors
- Name
- Yinhuan Yuan
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
- 2. Mapped Types
- 3. Template Literal Types
- 4. Recursive Types
- 5. Higher-Order Types
- 6. Variadic Tuple Types
- 7. Key Remapping in Mapped Types
- 8. Generic Type Inference in Conditional Types
- 9. Distributive Conditional Types
- 10. Indexed Access Types
- Conclusion
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!