从一些简单的例子来学习类型体操,效率更高。
所有题目来自 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` 的成员属性。 ```ts 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",报错
-
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 条测试用例 1. `MyPick<Todo, 'title' | 'completed' | 'invalid'>` **K extends keyof T,限制了 K 只能是 T 的成员属性,否则报错** 2. `Expect<Equal<Expected1, MyPick<Todo, 'title'>>>` 3. `Expect<Equal<Expected2, MyPick<Todo, 'title' | 'completed'>>>` **2 和 3 都是 U 是否是 K 的一个属性,如果是就添加。就达到了 Pick 的效果了。** ```ts 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 为只读属性
interface Todo { title: string; description: string; completed: boolean; } type todoKey = keyof Todo // "title" | "description" | "completed"0
元组转换为对象
11 - 元组转换为对象
by sinoon (@sinoon) #简单
题目
传入一个元组类型,将这个元组类型转换为对象类型,这个对象类型的键/值都是从元组中遍历出来。
例如:
interface Todo { title: string; description: string; completed: boolean; } type todoKey = keyof Todo // "title" | "description" | "completed"1
知识点
-
const
字面量,可以理解为不可修改。interface Todo { title: string; description: string; completed: boolean; } type todoKey = keyof Todo // "title" | "description" | "completed"2
const a2 = '123' type a2 = typeof a2 // '123' a2 = "321" // 无法分配到 "a2" ,因为它是常数。
interface Todo { title: string; description: string; completed: boolean; } type todoKey = keyof Todo // "title" | "description" | "completed"3
-
typeof
在TS
里可以理解为有两部分,一部分是原来的JS
,一部分是类型
。如果要获取一个JavaScript
变量的类型就可以用typeof
。interface Todo { title: string; description: string; completed: boolean; } type todoKey = keyof Todo // "title" | "description" | "completed"4
// 结果:(a: number, b:number) => number type t1 = typeof add // 结果:{ name: string; age: number; } type t2 = typeof obj
interface Todo { title: string; description: string; completed: boolean; } type todoKey = keyof Todo // "title" | "description" | "completed"5
解答
还和上面一样,只不过需要先用T[number]
获取到元组的所有元素,
然后遍历,
因为是获取到的元素,返回值直接返回 P 就行。
interface Todo { title: string; description: string; completed: boolean; } type todoKey = keyof Todo // "title" | "description" | "completed"6
注意泛型里的 T extends readonly string[],如果使用 any 会导致 @ts-expect-error 这条不通过。
@ts-expect-error,如果我们把这个注释放在代码行前面,TypeScript 就会预期下面的代码会报错。就是报错才正常,如果不报错那这行注释会提示错误。
第一个元素
14 - 第一个元素
by Anthony Fu (@antfu) #简单 #array
题目
实现一个通用First<T>
,它接受一个数组T
并返回它的第一个元素的类型。
例如:
interface Todo { title: string; description: string; completed: boolean; } type todoKey = keyof Todo // "title" | "description" | "completed"7
知识点
-
infer
在extends
语句中,支持infer
关键字,表示语句中待推断的类型变量。这个有点难以理解,还是看例子吧。interface Todo { title: string; description: string; completed: boolean; } type todoKey = keyof Todo // "title" | "description" | "completed"8
let func1: Func ; // => boolean let func2: Func<''>; // => boolean let func3: Func<() => Promise >; // => Promise
interface Todo { title: string; description: string; completed: boolean; } type todoKey = keyof Todo // "title" | "description" | "completed"9
解答
-
利用
infer
获取第一个元素function getProperty(obj, key) { return obj[key]; }0
-
利用
0
下标直接获取,判断一下是不是空数组。function getProperty(obj, key) { return obj[key]; }1
-
利用
T['length']
返回数组长度判断是否是空数组,然后用0
下标获取第一个元素。function getProperty(obj, key) { return obj[key]; }2
-
利用之前的知识点
T[number]
返回所有元素的联合类型。function getProperty(obj, key) { return obj[key]; }3
这里需要注意下,First<[3, 2, 1]>,泛型里的所有参数都是类型而不是 JavaScript 对象。所以如果要从 JS 里引用的话,需要用 typeof。
获取元组长度
18 - 获取元组长度
by sinoon (@sinoon) #简单 #tuple
题目
创建一个通用的Length
,接受一个readonly
的数组,返回这个数组的长度。
例如:
function getProperty(obj, key) { return obj[key]; }4
解答
这个没啥说的,知识点都是上面的。在泛型传入时约束下必须传入元组就行,否则 test case 过不了
function getProperty(obj, key) { return obj[key]; }5
Exclude
43 - Exclude
by Zheeeng (@zheeeng) #简单 #built-in
题目
实现 TS 内置的 Exclude<T, K>
,但不可以使用它。
function getProperty(obj, key) { return obj[key]; }6
知识点
-
extends
extends
的另一个特性。分配式
,当extends
左边类型是一个联合类型时,会进行拆分,有点类似数学中的分解因式:
function getProperty(obj, key) { return obj[key]; }7
解答
function getProperty(obj, key) { return obj[key]; }8
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。
function getProperty(obj, key) { return obj[key]; }9
If
268 - If
by Pavel Glushkov (@pashutk) #简单 #utils
题目
实现一个 IF
类型,它接收一个条件类型 C
,一个判断为真时的返回类型 T
,以及一个判断为假时的返回类型 F
。 C
只能是 true
或者 false
, T
和 F
可以是任意类型。
举例:
这时,我们直接添加泛型,会报错。因为现在无法确定 `K` 是否是 `T` 的成员属性。 ```ts function getProperty<T, K>(obj: T, key: K) { return obj[key]; // Error 类型“K”无法用于索引类型“T”。 }0
解答
这题本身很简单,注意约束一下 C 的传入类型就行。
这时,我们直接添加泛型,会报错。因为现在无法确定 `K` 是否是 `T` 的成员属性。 ```ts function getProperty<T, K>(obj: T, key: K) { return obj[key]; // Error 类型“K”无法用于索引类型“T”。 }1
关键在于 ts 配置,在不同的环境下,C extends boolean
有时 boolean
包含 null
有时不包含。
在 tsconfig.json
中开启 "strictNullChecks": true
。
这时,我们直接添加泛型,会报错。因为现在无法确定 `K` 是否是 `T` 的成员属性。 ```ts function getProperty<T, K>(obj: T, key: K) { return obj[key]; // Error 类型“K”无法用于索引类型“T”。 }2
建议在所有情况下都开启 null
的严格模式检查。
Concat
533 - Concat
by Andrey Krasovsky (@bre30kra69cs) #简单 #array
题目
在类型系统里实现 JavaScript 内置的 Array.concat
方法,这个类型接受两个参数,返回的新数组类型应该按照输入参数从左到右的顺序合并为一个新的数组。
举例,
这时,我们直接添加泛型,会报错。因为现在无法确定 `K` 是否是 `T` 的成员属性。 ```ts function getProperty<T, K>(obj: T, key: K) { return obj[key]; // Error 类型“K”无法用于索引类型“T”。 }3
解答
这个没啥说的,用 JS 的思路,使用展开运算符,然后看报错添加约束。
这时,我们直接添加泛型,会报错。因为现在无法确定 `K` 是否是 `T` 的成员属性。 ```ts function getProperty<T, K>(obj: T, key: K) { return obj[key]; // Error 类型“K”无法用于索引类型“T”。 }4
Includes
898 - Includes
by null (@kynefuk) #简单 #array
题目
在类型系统里实现 JavaScript 的 Array.includes
方法,这个类型接受两个参数,返回的类型要么是 true
要么是 false
。
举例来说,
这时,我们直接添加泛型,会报错。因为现在无法确定 `K` 是否是 `T` 的成员属性。 ```ts function getProperty<T, K>(obj: T, key: K) { return obj[key]; // Error 类型“K”无法用于索引类型“T”。 }5
解答
这道题在简单
里有点复杂了。一般复杂的泛型最好先用 JS
来写一个思路,然后再转换成 TS
泛型。
这时,我们直接添加泛型,会报错。因为现在无法确定 `K` 是否是 `T` 的成员属性。 ```ts function getProperty<T, K>(obj: T, key: K) { return obj[key]; // Error 类型“K”无法用于索引类型“T”。 }6
这时,我们直接添加泛型,会报错。因为现在无法确定 `K` 是否是 `T` 的成员属性。 ```ts function getProperty<T, K>(obj: T, key: K) { return obj[key]; // Error 类型“K”无法用于索引类型“T”。 }7
或者自己写一个 Equal
这时,我们直接添加泛型,会报错。因为现在无法确定 `K` 是否是 `T` 的成员属性。 ```ts function getProperty<T, K>(obj: T, key: K) { return obj[key]; // Error 类型“K”无法用于索引类型“T”。 }8
Push
3057 - Push
by jiangshan (@jiangshanmeta) #简单 #array
题目
在类型系统里实现通用的 Array.push
。
举例如下,
这时,我们直接添加泛型,会报错。因为现在无法确定 `K` 是否是 `T` 的成员属性。 ```ts function getProperty<T, K>(obj: T, key: K) { return obj[key]; // Error 类型“K”无法用于索引类型“T”。 }9
解答
又恢复到很简单的,没啥说的。
// 这里泛型的意思就是,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",报错0
Unshift
3060 - Unshift
by jiangshan (@jiangshanmeta) #简单 #array
题目
实现类型版本的 Array.unshift
。
举例,
// 这里泛型的意思就是,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",报错1
解答
送分题,同上 。
// 这里泛型的意思就是,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
Parameters
3312 - Parameters
by midorizemi (@midorizemi) #简单 #infer #tuple #built-in
题目
实现内置的 Parameters 类型,而不是直接使用它,可参考TypeScript官方文档。
解答
算是熟悉 infer
的适用吧。
// 这里泛型的意思就是,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",报错3