Skip to main content

Generic Types

Generic Function

function reverse<T>(items: T[]): T[] {
const toReturn = []
for (let i = items.length - 1; i >= 0; i--)
toReturn.push(items[i])

return toReturn
}

Generic Parameters

type Event
= | {
type: 'LogIn'
payload: {
userId: string
}
}
| {
type: 'SignOut'
}

function sendEvent<Type extends Event['type']>(
...args: Extract<Event, { type: Type }> extends { payload: infer Payload }
? [type: Type, payload: Payload]
: [type: Type]
) {
// Send event ...
}

Generic Class

// 创建一个泛型类
class Queue<T> {
private data = []
push = (item: T) => this.data.push(item)
pop = (): T => this.data.shift()
}

// 简单的使用
const queue = new Queue<number>()
queue.push(0)
queue.push('1') // Error:不能推入一个 `string`,只有 number 类型被允许
interface Listener<T> {
(event: T): any
}

interface Disposable {
dispose: () => any
}

class TypedEvent<T> {
private listeners: Listener<T>[] = []
private listenersOnce: Listener<T>[] = []

public on = (listener: Listener<T>): Disposable => {
this.listeners.push(listener)

return {
dispose: () => this.off(listener),
}
}

public once = (listener: Listener<T>): void => {
this.listenersOnce.push(listener)
}

public off = (listener: Listener<T>) => {
const callbackIndex = this.listeners.indexOf(listener)
if (callbackIndex > -1)
this.listeners.splice(callbackIndex, 1)
}

public emit = (event: T) => {
this.listeners.forEach(listener => listener(event))

this.listenersOnce.forEach(listener => listener(event))
this.listenersOnce = []
}

public pipe = (te: TypedEvent<T>): Disposable => {
return this.on(e => te.emit(e))
}
}

Generic Type Alias

type CreatesValue<Input, Output> = (input: Input) => Output

// Type: (input: string) => number
let creator: CreatesValue<string, number>

creator = text => text.length // Ok

creator = text => text.toUpperCase()
// ~~~~~~~~~~~~~~~~~~
// Error: Type 'string' is not assignable to type 'number'.

Generic Discriminated Union

type Result<Data> = FailureResult | SuccessfulResult<Data>

interface FailureResult {
error: Error
succeeded: false
}

interface SuccessfulResult<Data> {
data: Data
succeeded: true
}

function handleResult(result: Result<string>) {
if (result.succeeded) {
// Type of result: SuccessfulResult<string>
console.log(`We did it! ${result.data}`)
} else {
// Type of result: FailureResult
console.error(`Em... ${result.error}`)
}

console.log(result.data)
// ~~~~
// Error: Property 'data' does not exist on type 'Result<string>'.
// Property 'data' does not exist on type 'FailureResult'.
}

Explicit Generic Types

class Foo<T> {
foo: T
}

const FooNumber = Foo as { new (): Foo<number> } // ref 1

function id<T>(x: T) {
return x
}

const idNum = id as { (x: number): number }

Default Generic Types

interface Quote<T = string> {
value: T
}

const explicit: Quote<number> = { value: 123 }
const implicit: Quote = {
value: 'Be yourself. The world worships the original.',
}
const mismatch: Quote = { value: 123 }
// ~~~
// Error: Type 'number' is not assignable to type 'string'.

interface KeyValuePair<Key, Value = Key> {
key: Key
value: Value
}

// Type: KeyValuePair<string, number>
const allExplicit: KeyValuePair<string, number> = {
key: 'rating',
value: 10,
}

// Type: KeyValuePair<string>
const oneDefaulting: KeyValuePair<string> = {
key: 'rating',
value: 'ten',
}

const firstMissing: KeyValuePair = {
// ~~~~~~~~~~~~
// Error: Generic type 'KeyValuePair<Key, Value>'
// requires between 1 and 2 type arguments.
key: 'rating',
value: 10,
}

Constrained Generic Types

Constrained union types:

interface Lengthwise {
length: number
}

function createList<T extends number | Lengthwise>(): T[] {
return [] as T[]
}

const numberList = createList<number>() // ok
const stringList = createList<string>() // ok
const arrayList = createList<any[]>() // ok
const boolList = createList<boolean>() // error

Constrained template literal types:

type RemoveMapsHelper<T> = T extends `maps:${infer U}` ? U : T
type RemoveMaps<T> = {
[K in keyof T as RemoveMapsHelper<K>]: T[K]
}

interface Data {
'maps:longitude': string
'maps:latitude': string
'awesome': boolean
}
type ShapedData = RemoveMaps<Data>
// type ShapedData = {
// longitude: string;
// latitude: string;
// awesome: boolean;
// }

Constrained index types:

function getValue<T, Key extends keyof T>(container: T, key: Key) {
return container[key]
}

const roles = {
favorite: 'Fargo',
others: ['Almost Famous', 'Burn After Reading', 'NorthLand'],
}

const favorite = getValue(roles, 'favorite') // Type: string
const others = getValue(roles, 'others') // Type: string[]
const missing = getValue(roles, 'extras')
// ~~~~~~~~
// Error: Argument of type '"extras"' is not assignable
// to parameter of type '"favorite" | "others"'.

function getDeepValue<
T,
FirstKey extends keyof T,
SecondKey extends keyof T[FirstKey],
>(target: T, firstKey: FirstKey, secondKey: SecondKey): T[FirstKey][SecondKey] {
return target[firstKey][secondKey]
}

const target = {
foo: {
a: true,
b: 2,
},
bar: {
c: 'cool',
d: 2,
},
}

const result1 = getDeepValue(target, 'foo', 'a') // boolean
const result2 = getDeepValue(target, 'bar', 'c') // string

Constrained default types:

interface Props {
a1: 'Foo'
a2: 'Bar'
a3: 'FooBar'
b1: 'b1'
b2: 'b2'
b3: 'b3'
}

type ExtractValues<T, Keys extends keyof T = Extract<keyof T, `a${string}`>> = {
[K in Keys]: T[K]
}[Keys]

type Values = ExtractValues<Props>
// type Values = 'Foo' | 'Bar' | 'FooBar'

Generic Types Programming

在类型编程里, 泛型就是变量:

function pick<T extends object, U extends keyof T>(obj: T, keys: U[]): T[U][] {
return keys.map(key => obj[key])
}
Generic Golden Rule

Type T parameters should appear twice:

If a type parameter only appears in one location, strongly reconsider if actually need it.