类型体操
两数之和
包括定义加法 减法 获取数组除了第一个元素尾部 数组转联合类型
// ts => LeetCode第一题: 两数之和
type TwoSum<A extends number[], T extends number> =
A['length'] extends 0
? false
: Sub<T, A[0]> extends Arr2Union<Tail<A>>
? true
: TwoSum<Tail<A>, T>
// 生成长度为 N 的数组Arr
type toArray<L, Arr extends any[] = []> =
Arr['length'] extends L
? Arr
: toArray<L, [...Arr, any]>
type Add<T extends number, K extends number> =
[...toArray<T>, ...toArray<K>]['length']
type Sub<T extends number, K extends number> =
toArray<T> extends [...toArray<K>, ...infer R]
? R['length']
: never
type Tail<N extends any[]> = N extends [infer F, ...infer Tail] ? Tail : []
type Arr2Union<T extends any[]> = T[number]
type res = TwoSum<[1,2,3], 5>
获取元祖第一个元素
type First<T extends any[]> = T extends [infer First, ...infer Tail] ? First : never
元组转对象
type TupleToObject<T extends readonly any[]> = {
[K in T[number]]: K
}
const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const
type cases = [
Expect<Equal<TupleToObject<typeof tuple>, { tesla: 'tesla'; 'model 3': 'model 3'; 'model X': 'model X'; 'model Y': 'model Y' }>>,
]
类型体操练习题系列(持续更新)
1
type User = {
id: number;
kind: string;
};
function makeCustomer<T extends User>(u: T): T {
// Error(TS 编译器版本:v4.4.2)
// Type '{ id: number; kind: string; }' is not assignable to type 'T'.
// '{ id: number; kind: string; }' is assignable to the constraint of type 'T',
// but 'T' could be instantiated with a different subtype of constraint 'User'.
return {
id: u.id,
kind: 'customer'
}
}
// 解决
function makeCustomer<T extends User>(u: T): T {
return {
...u,
id: u.id,
kind: 'customer'
}
}
2
我们希望参数 a 和 b 的类型都是一致的,即 a 和 b 同时为 number 或 string 类型。当它们的类型不一致的值,TS 类型检查器能自动提示对应的错误信息。
function f(a: string | number, b: string | number) {
if (typeof a === 'string') {
return a + ':' + b; // no error but b can be number!
} else {
return a + b; // error as b can be number | string
}
}
f(2, 3); // Ok
f(1, 'a'); // Error
f('a', 2); // Error
f('a', 'b') // Ok
// 解决
// 函数重载
function f(a: string, b: string): string
function f(a: number, b: number): number
function f(a: string | number, b: string | number) {
if (typeof a === 'string' || typeof b === 'string') {
return a + ':' + b;
} else {
return a + b
}
}
f(2, 3); // Ok
f(1, 'a'); // Error
f('a', 2); // Error
f('a', 'b') // Ok
3
TS 内置的工具类型:Partial,它的作用是将某个类型里的属性全部变为可选项
// lib.es5.d.ts
type Partial<T> = {
[P in keyof T]?: T[P];
};
那么如何定义一个 SetOptional 工具类型,支持把给定的 keys 对应的属性变成可选的
对应的使用示例如下所示:
type Foo = {
a: number;
b?: string;
c: boolean;
}
// 测试用例
type SomeOptional = SetOptional<Foo, 'a' | 'b'>;
// type SomeOptional = {
// a?: number; // 该属性已变成可选的
// b?: string; // 保持不变
// c: boolean;
// }
在实现 SetOptional 工具类型之后,如果你感兴趣,可以继续实现 SetRequired 工具类型,利用它可以把指定的 keys 对应的属性变成必填的。对应的使用示例如下所示:
type Foo = {
a?: number;
b: string;
c?: boolean;
}
// 测试用例
type SomeRequired = SetRequired<Foo, 'b' | 'c'>;
// type SomeRequired = {
// a?: number;
// b: string; // 保持不变
// c: boolean; // 该属性已变成必填
// }
答案SetOptional
type Foo = {
a: number;
b?: string;
c: boolean;
}
type Simplify<T> = {
[P in keyof T]: T[P]
}// 用来对交叉类型进行扁平化处理
type SetOptional<T, K extends keyof T> =
Simplify<Partial<Pick<T, K>> & Pick<T, Exclude<keyof T, K>>>
// Simplify<
// Partial<Pick<T, K>> /* Partial<A>作用是把 A 所有类型变为可选 这里的A是后面从T中挑出所有属于K*/
// &
// Pick<T, Exclude<keyof T, K>> /* 从T中挑出不属于K的 这部分不需要变成可选*/
// >
// Simplify作用是把这个交叉类型整合到一起
type SomeOptional = SetOptional<Foo, 'a' | 'b'>;
const obj: SomeOptional = {
c: false
}
答案SomeRequired
// 同上
type SomeRequired =
Simplify<Required<Pick<T, K>> & Pick<T, Exclude<keyof T, K>>>
4
Pick<T, K extends keyof T>
的作用是将某个类型中的子属性挑出来,变成包含这个类型部分属性的子类型。
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = Pick<Todo, "title" | "completed">;
const todo: TodoPreview = {
title: "Clean room",
completed: false
};
那么如何定义一个 ConditionalPick 工具类型,支持根据指定的 Condition 条件来生成新的类型,对应的使用示例如下:
interface Example {
a: string;
b: string | number;
c: () => void;
d: {};
}
// 测试用例:
type StringKeysOnly = ConditionalPick<Example, string>;
// => {a: string}
// 答案
type ConditionalPick<T, V> = {
[K in keyof T as T[K] extends V ? K : never]: T[K];
}
// as 我理解成等于号 = ,后面是hi三目运算
// 如果K in keyof T 获取T所有键 并遍历
// 如果T[K]即属性的value值 符合V的约束的挑选出来
// 不符合的杀掉 变成never
5
定义一个工具类型 AppendArgument,为已有的函数类型增加指定类型的参数,新增的参数名是 x,将作为新函数类型的第一个参数。具体的使用示例如下所示:
type Fn = (a: number, b: string) => number
type AppendArgument<F, A> = // 你的实现方式
type AppendArgument<F extends (...args: any) => any, A>
= (x: A, ...args: Parameters<F>) => ReturnType<F>
type AppendArgument<F, A> = F extends (...args: infer Args) => infer Return ?
(x: A, ...args: Args) => Return : never
type FinalFn = AppendArgument<Fn, boolean>
// (x: boolean, a: number, b: string) => number
6
定义一个 NativeFlat 工具类型,支持把数组类型拍平(扁平化)。具体的使用示例如下所示:
type NaiveFlat<T extends any[]> = {
[P in keyof T]: T[P] extends any[] ? T[P][number] : T[P]
}[number]
// 测试用例:
type NaiveResult = NaiveFlat<[['a'], ['b', 'c'], ['d']]>
// NaiveResult的结果: "a" | "b" | "c" | "d"
在完成 NaiveFlat 工具类型之后,在继续实现 DeepFlat 工具类型,以支持多维数组类型:
type DeepFlat<T extends any[]> = {
[K in keyof T]: T[K] extends any[] ? DeepFlat<T[K]> : T[K]
}[number]
// 测试用例
type Deep = [['a'], ['b', 'c'], [['d']], [[[['e']]]]];
type DeepTestResult = DeepFlat<Deep>
// DeepTestResult: "a" | "b" | "c" | "d" | "e"