TypeScript 泛型从新手到入门
从一些简单的例子来学习类型体操,效率更高。
所有题目来自 type-challenges 的简单
挑战。
题目解答同步到 github
泛型可以理解成类型函数。<T>
就表示类型参数,和形参数一样,<>里写什么字母都可以。
实现 Pick
4 - 实现 Pick
by Anthony Fu (@antfu) #简单 #union #built-in
题目
实现 TS 内置的 Pick<T, K>
,但不可以使用它。
从类型 T
中选择出属性 K
,构造成一个新的类型。
例如:
interface Todo {
title: string
description: string
completed: boolean
}
type TodoPreview = MyPick<Todo, 'title' | 'completed'>
const todo: TodoPreview = {
title: 'Clean room',
completed: false,
}
知识点
keyof
keyof
的作用将一个 类型 映射为它 所有成员名称的联合类型
interface Todo {
title: string;
description: string;
completed: boolean;
}
type todoKey = keyof Todo // "title" | "description" | "completed"
- 实际使用
比如我们经常获取一个对象的某个属性,会获取到
undefined
function getProperty(obj, key) {
return obj[key];
}
const obj = {
foo: 1,
bar: 2,
baz: 3,
};
const foo = getProperty(obj, "foo");
const b = getProperty(obj, "b");
console.log(b); // undefined
这时,我们直接添加泛型,会报错。因为现在无法确定 K
是否是 T
的成员属性。
function getProperty<T, K>(obj: T, key: K) {
return obj[key]; // Error 类型“K”无法用于索引类型“T”。
}
用 keyof
来获取 T
的所有成员属性,用 extends
来判断 K
是否可以赋值它。
用上面的例子来分解:
// 这里泛型的意思就是,K 是 T 成员属性中的一个。如果 K 传的值不是 T 成员属性其中之一,就会报错。
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
const obj = {
foo: 1,
bar: 2,
baz: 3,
};
// keyof T => "foo" | "bar" | "baz"
const foo = getProperty(obj, "foo"); // "foo" extends "foo" | "bar" | "baz",正常
const b = getProperty(obj, "b"); // "b" extends "foo" | "bar" | "baz",报错
2.
extends
extends
有很多功能,这里只讲上面用到的功能。 判断是否能将左边的类型赋值给右边的类型- 实际使用
// 左边能赋值给右边
type trueType = "foo" extends "foo" | "bar" | "baz" ? "trueType" : "falseType"
// 左边不能赋值给右边
type falseType = "b" extends "foo" | "bar" | "baz" ? "trueType" : "falseType"
in
-
in
操作符用于遍历目标类型的公开属性名。类似for .. in
的机制。 -
实际使用
// 遍历枚举类型
enum Letter {
A,
B,
C,
}
type LetterMap = { [key in Letter]: string };
// 遍历联合类型
type Property = "name" | "age" | "phoneNum";
type PropertyObject = { [key in Property]: string };
解答
3 条测试用例
MyPick<Todo, 'title' | 'completed' | 'invalid'>
K extends keyof T,限制了 K 只能是 T 的成员属性,否则报错Expect<Equal<Expected1, MyPick<Todo, 'title'>>>
Expect<Equal<Expected2, MyPick<Todo, 'title' | 'completed'>>>
2 和 3 都是 U 是否是 K 的一个属性,如果是就添加。就达到了 Pick 的效果了。
type MyPick<T, K extends keyof T> = {
[U in K]: T[U]
}
实现 Readonly
7 - 实现 Readonly
by Anthony Fu (@antfu) #简单 #built-in #readonly #object-keys
题目
不要使用内置的Readonly<T>
,自己实现一个。
该 Readonly
会接收一个 泛型参数,并返回一个完全一样的类型,只是所有属性都会被 readonly
所修饰。
也就是不可以再对该对象的属性赋值。
例如:
interface Todo {
title: string
description: string
}
const todo: MyReadonly<Todo> = {
title: "Hey",
description: "foobar"
}
todo.title = "Hello" // Error: cannot reassign a readonly property
todo.description = "barFoo" // Error: cannot reassign a readonly property
知识点
readonly
type Foo = {
readonly bar: number;
readonly bas: number;
};
// 初始化
const foo: Foo = { bar: 123, bas: 456 };
// 不能被改变
foo.bar = 456; // Error: foo.bar 为只读属性
解答
这个用上面的 in
和 keyof
知识点就可以,遍历之后给所有属性加上 readonly
type MyReadonly<T> = {
readonly [P in keyof T]: T[P]
}
元组转换为对象
11 - 元组转换为对象
by sinoon (@sinoon) #简单
题目
传入一个元组类型,将这个元组类型转换为对象类型,这个对象类型的键/值都是从元组中遍历出来。
例如:
const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const
type result = TupleToObject<typeof tuple> // expected { tesla: 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'}
知识点
const
字面量,可以理解为不可修改。
let a1 = '123'
type a1 = typeof a1 // string
a1 = "321"
const a2 = '123'
type a2 = typeof a2 // '123'
a2 = "321" // 无法分配到 "a2" ,因为它是常数。
数组同样,不加 const
就是可以修改,加 const
不可以修改,在 TS
里叫作 元组
let arr1 = ['tesla', 'model 3', 'model X', 'model Y']
arr1[0] = 'test'
const arr2 = ['tesla', 'model 3', 'model X', 'model Y'] as const
arr2[0] = 'test' // 无法分配到 "0" ,因为它是只读属性。
// as 是断言,意思是告诉 TS 服务,它就是 const
typeof
在TS
里可以理解为有两部分,一部分是原来的JS
,一部分是类型
。如果要获取一个JavaScript
变量的类型就可以用typeof
。
const add = (a: number, b: number): number => {
return a + b
}
const obj = {
name: 'AAA',
age: 23
}
// 结果:(a: number, b:number) => number
type t1 = typeof add
// 结果:{ name: string; age: number; }
type t2 = typeof obj
有些简单的判断,系统可以自动推断类型,所以不用 typeof 也可以有类型约束。但复杂的泛型不行。
T[number]
表示返回所有数字型索引的元素,形成一个联合类型,例如:
const arr2 = ["tesla", "model 3", "model X", "model Y"] as const;
type Tuple = typeof arr2
type arr = Tuple[number]; // "tesla" | "model 3" | "model X" | "model Y"
解答
还和上面一样,只不过需要先用T[number]
获取到元组的所有元素,
然后遍历,
因为是获取到的元素,返回值直接返回 P 就行。
type TupleToObject<T extends readonly string[]> = {
[P in T[number]]: P
}
注意泛型里的 T extends readonly string[],如果使用 any 会导致 @ts-expect-error 这条不通过。
@ts-expect-error,如果我们把这个注释放在代码行前面,TypeScript 就会预期下面的代码会报错。就是报错才正常,如果不报错那这行注释会提示错误。
第一个元素
14 - 第一个元素
by Anthony Fu (@antfu) #简单 #array
题目
实现一个通用First<T>
,它接受一个数组T
并返回它的第一个元素的类型。
例如:
type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]
type head1 = First<arr1> // expected to be 'a'
type head2 = First<arr2> // expected to be 3
知识点
infer
在extends
语句中,支持infer
关键字,表示语句中待推断的类型变量。这个有点难以理解,还是看例子吧。
// 如果泛型变量 T 是 `() => infer R` 的子集,那么返回通过 infer 获取到的函数返回值,否则返回 boolean 类型。
type Func<T> = T extends () => infer R ? R : boolean;
let func1: Func<number>; // => boolean
let func2: Func<''>; // => boolean
let func3: Func<() => Promise<number>>; // => Promise<number>
// T 如果可以赋值给 [infer H, ...any[]],就返回数组的第一个类型。
type arrHead<T> = T extends [infer H, ...any[]] ? H : never;
type arr = ["one", "two", "three"];
type head = arrHead<arr>; // "one"
解答
- 利用
infer
获取第一个元素
type First<T extends any[]> = T extends [infer First, ...infer Rest] ? First : never
- 利用
0
下标直接获取,判断一下是不是空数组。
type First<T extends any[]> = T extends [] ? never : T[0]
- 利用
T['length']
返回数组长度判断是否是空数组,然后用0
下标获取第一个元素。
type First<T extends any[]> = T['length'] extends 0 ? never : T[0]
- 利用之前的知识点
T[number]
返回所有元素的联合类型。
// T[number] T 为空时会返回 never
type First<T extends any[]> = T[0] extends T[number] ? T[0] : never
这里需要注意下,First<[3, 2, 1]>,泛型里的所有参数都是类型而不是 JavaScript 对象。所以如果要从 JS 里引用的话,需要用 typeof。
获取元组长度
18 - 获取元组长度
by sinoon (@sinoon) #简单 #tuple
题目
创建一个通用的Length
,接受一个readonly
的数组,返回这个数组的长度。
例如:
type tesla = ['tesla', 'model 3', 'model X', 'model Y']
type spaceX = ['FALCON 9', 'FALCON HEAVY', 'DRAGON', 'STARSHIP', 'HUMAN SPACEFLIGHT']
type teslaLength = Length<tesla> // expected 4
type spaceXLength = Length<spaceX> // expected 5
解答
这个没啥说的,知识点都是上面的。在泛型传入时约束下必须传入元组就行,否则 test case 过不了
type Length<T extends readonly unknown[]> = T['length']
Exclude
43 - Exclude
by Zheeeng (@zheeeng) #简单 #built-in
题目
实现 TS 内置的 Exclude<T, K>
,但不可以使用它。
// 结果:'name'|'age'
type ExcludeResult = Exclude<'name'|'age'|'sex', 'sex'|'address'>
知识点
extends
extends
的另一个特性。分配式
,当使用泛型extends
左边类型是一个联合类型时,会进行拆分,有点类似数学中的分解因式:
type Diff<T, U> = T extends U ? never : T; // 找出T的差集
type Filter<T, U> = T extends U ? T : never; // 找出交集
type T30 = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">; // => "b" | "d"
// <"a" | "b" | "c" | "d", "a" | "c" | "f"> 相当于
/*
* <'a', "a" | "c" | "f"> |
* <'b', "a" | "c" | "f"> |
* <'c', "a" | "c" | "f"> |
* <'d', "a" | "c" | "f">
*/
type T31 = Filter<"a" | "b" | "c" | "d", "a" | "c" | "f">; // => "a" | "c"
// <"a" | "b" | "c" | "d", "a" | "c" | "f"> 同上
let demo1: Diff<number, string>; // => number
解答
type MyExclude<T, U> = T extends U ? never : T
Awaited
189 - Awaited
by Maciej Sikora (@maciejsikora) #简单 #promise #built-in
题目
假如我们有一个 Promise 对象,这个 Promise 对象会返回一个类型。在 TS 中,我们用 Promise 中的 T 来描述这个 Promise 返回的类型。请你实现一个类型,可以获取这个类型。
比如:Promise<ExampleType>
,请你返回 ExampleType 类型。
这个挑战来自于 @maciejsikora 的文章:original article
解答
没啥说的,知识点都是上面的。传入值需要约束为 Promise,返回值需要递归判断 Promise 的泛型参数是否 Promise。
type MyAwaited<T extends Promise<any>> = T extends Promise<infer P>
? P extends Promise<any>
? MyAwaited<P>
: P
: T;
ps: 2022.12.20 日更新,发现题目加了个测试例,约束需要改一下。
type MyAwaited<T extends { then: (onfulfilled: any) => any }> = T extends {
then: (onfulfilled: (arg: infer U) => any) => any;
}
? U extends Promise<unknown>
? MyAwaited<U>
: U
: never;
If
268 - If
by Pavel Glushkov (@pashutk) #简单 #utils
题目
实现一个 IF
类型,它接收一个条件类型 C
,一个判断为真时的返回类型 T
,以及一个判断为假时的返回类型 F
。 C
只能是 true
或者 false
, T
和 F
可以是任意类型。
举例:
type A = If<true, 'a', 'b'> // expected to be 'a'
type B = If<false, 'a', 'b'> // expected to be 'b'
解答
这题本身很简单,注意约束一下 C 的传入类型就行。
type If<C extends boolean, T, F> = C extends true ? T : F;
关键在于 ts 配置,在不同的环境下,C extends boolean
有时 boolean
包含 null
有时不包含。
在 tsconfig.json
中开启 "strictNullChecks": true
。
interface User {
name: string;
age?: number;
}
function printUserInfo(user: User) {
console.log(`${user.name}, ${user.age.toString()}`)
// => error TS2532: Object is possibly 'undefined'.
console.log(`${user.name}, ${user.age!.toString()}`)
// => 使用 ! 断言,告诉 TS 你已经确认 user.age 是非空的。
if (user.age != null) {
console.log(`${user.name}, ${user.age.toString()}`)
}
// => 或者使用类型守卫,用 if 条件检查 user.age 是非空的。
建议在所有情况下都开启 null
的严格模式检查。
Concat
533 - Concat
by Andrey Krasovsky (@bre30kra69cs) #简单 #array
题目
在类型系统里实现 JavaScript 内置的 Array.concat
方法,这个类型接受两个参数,返回的新数组类型应该按照输入参数从左到右的顺序合并为一个新的数组。
举例,
type Result = Concat<[1], [2]> // expected to be [1, 2]
解答
这个没啥说的,用 JS 的思路,使用展开运算符,然后看报错添加约束。
type Concat<T extends unknown[], U extends unknown[]> = [...T, ...U];
Includes
898 - Includes
by null (@kynefuk) #简单 #array
题目
在类型系统里实现 JavaScript 的 Array.includes
方法,这个类型接受两个参数,返回的类型要么是 true
要么是 false
。
举例来说,
type isPillarMen = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'> // expected to be `false`
解答
这道题在简单
里有点复杂了。一般复杂的泛型最好先用 JS
来写一个思路,然后再转换成 TS
泛型。
function Includes(arr, key) {
function helper(arr, key) {
const [head, ...tail] = arr;
if (head === key) {
return true;
} else {
return tail.length === 0 ? false : helper(tail, key);
}
}
helper(arr, key);
}
// 使用 `@type-challenges/utils` 里的 `Equal`
import type { Equal, Expect } from '@type-challenges/utils'
type Includes<T extends readonly any[], U> = T extends [infer Head, ...infer Rest]
? Equal<Head, U> extends true ? true : Includes<Rest, U>
: false
或者自己写一个 Equal
type Equal1<X, Y> =
(<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2)
? true
: false;
Push
3057 - Push
by jiangshan (@jiangshanmeta) #简单 #array
题目
在类型系统里实现通用的 Array.push
。
举例如下,
type Result = Push<[1, 2], '3'> // [1, 2, '3']
解答
又恢复到很简单的,没啥说的。
type Push<T extends unknown[], U> = [...T, U]
Unshift
3060 - Unshift
by jiangshan (@jiangshanmeta) #简单 #array
题目
实现类型版本的 Array.unshift
。
举例,
type Result = Unshift<[1, 2], 0> // [0, 1, 2,]
解答
送分题,同上 。
type Unshift<T extends unknown[], U> = [U, ...T]
Parameters
3312 - Parameters
by midorizemi (@midorizemi) #简单 #infer #tuple #built-in
题目
实现内置的 Parameters 类型,而不是直接使用它,可参考TypeScript官方文档。
解答
算是熟悉 infer
的适用吧。
type MyParameters<T extends (...args: any[]) => any> = T extends (...args: infer P) => any
? P
: never;
以上就是本篇文章的全部内容啦,感谢观看。
转载自:https://juejin.cn/post/7103046028235898888