04_pick
题目
实现 TS 内置的 Pick<T,K>
,不过不可以使用它本身。
从类型 T
中选择出属性 K
,构造成一个新的类型。
例如:
1
2
3
4
5
6
7
8
9
10
11
12
|
interface Todo {
title: string
description: string
completed: boolean
}
type TodoPreview = MyPick<Todo, 'title' | 'completed'>
const todo: TodoPreview = {
title: 'Clean room',
completed: false,
}
|
知识点
- 联合类型
|
- 映射类型
in、keyof
- 约束
extends
解题
1
2
|
// 签名
type MyPick<T, K> = any
|
看题目可知泛型类型T
实际上是 TODO 接口,K
是一个联合类型
由于联合类型K
必须是T
的 key 值,所以这里需要使用约束,需要注意的是如果仅仅用 K extenfs T
表达是不够的,因为K
必须是T
的key值,所以应该这样写:
1
2
3
|
type MyPick<T,K extends keyof T> = {
}
|
然后在声明的新的类型中,key 对应的是传入的K
值,value 对应的是 TODO 中对应的属性,所以只需要遍历生成即可:
1
2
3
|
type MyPick<T,K extends keyof T> = {
[F in K] : T[F]
}
|
test-case
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<Expected1, MyPick<Todo, 'title'>>>,
Expect<Equal<Expected2, MyPick<Todo, 'title' | 'completed'>>>,
// @ts-expect-error
MyPick<Todo, 'title' | 'completed' | 'invalid'>,
]
interface Todo {
title: string
description: string
completed: boolean
}
interface Expected1 {
title: string
}
interface Expected2 {
title: string
completed: boolean
}
|
大功告成。
07_readonly
题目
实现Readonly<T>
,接收一个 泛型参数,并返回一个完全一样的类型,只是所有属性都会被 readonly
所修饰。
例如:
1
2
3
4
5
6
7
8
9
10
11
12
|
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
|
知识点
解题
1
2
|
// 签名
type MyReadonly<T> = any
|
题目要求接受一个泛型类型T
,T
是一个接口,需要做的就是将接口内的所有字段添加上readonly
属性。
在 ts 中添加 readonly 属性很简单,只要在字段名称添加 readonly 声明即可,例如:
1
2
3
4
|
interface Todo {
readonly title: string // title属性只读
description: string
}
|
所以,解这道题只需要遍历泛型类型T
的所有字段,并在字段前添加上readonly
属性:
1
2
3
|
type MyReadonly<T> = {
readonly [F in keyof T]:T[F]
}
|
test-case
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<MyReadonly<Todo1>, Readonly<Todo1>>>,
]
interface Todo1 {
title: string
description: string
completed: boolean
meta: {
author: string
}
}
|
大功告成。
11_Tuple_to_Object
题目
传入一个元组类型,将这个元组类型转换为对象类型,这个对象类型的键/值都是从元组中遍历出来。
例如:
1
2
3
|
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'}
|
知识点
解题
1
2
|
// 签名
type TupleToObject<T extends readonly any[]> = any
|
做这道题需要一些前置知识,就是理解 as const
所代表的意义,是 ts 在 3.4 版本中新增的功能,称为 const 断言
,具体请参考官方文档,在这道题中,as const
的作用就是声明一个字面量只读数组:
1
2
3
4
|
const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const
type r = typeof tuple
const arr: r = ["aba"] // TS2322: Type '"aba"' is not assignable to type '"tesla"'.
|
在知道传入的参数到底是什么玩意之后,我们可以先将给到的签名修改一下,约束传入的泛型 T
应该是一个只有字符串的数组,而且应该返回一个对象:
1
2
3
|
type TupleToObject<T extends readonly string[]> = {
}
|
然后就需要遍历传入的数组,将提取数组中的值为对象的 key ,value 则是与之相同的值。
在 TS 中遍历一个数组,可以采用 in T[number]
的方式:
1
2
3
|
type TupleToObject<T extends readonly string[]> = {
[F in T[number]]: F
}
|
test-case
1
2
3
4
5
6
7
8
9
10
11
|
import type {Equal, Expect} from '@type-challenges/utils'
// 在 ts 中 as const = '字面量类型',跟 js 的 const 有一定区别
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' }>>,
]
// @ts-expect-error
type error = TupleToObject<[[1, 2], {}]>
|
在测试用例中,最下面有一个 @ts-expect-error
的注解声明,意思是如果下面的语句不报错,TS 就会报错,我们在上面传入参数时已经限制了泛型类型 T
为 readonly string[]
,所以用例中这样传参肯定会抛出错误的,测试通过。
14_First
题目
实现一个通用First<T>
,它接受一个数组T
并返回它的第一个元素的类型。
例如:
1
2
3
4
5
|
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
|
知识点
解题
1
2
|
// 签名
type First<T extends any[]> = any
|
按照惯例先约束一下泛型类型 T
,这一题需要注意,在后面的测试用例中,作为参数的数组并没有限制特定的类型,所以应该使用unknown[]
约束比较合适:
1
|
type First<T extends unknown[]> = any
|
在 TS 中获取数组的第一个元素方法有很多,这次我将会尝试使用其中两种解法来解这道题,一种是比较简单的,一种是相对进阶的解法。
先来看看最简单的解法一:判断传入的参数是否为空数组,如果是空数组,返回never
,否则返回数组的第一个元素,在 TS 中访问数组下标只需要使用T[index]
即可
1
|
type First<T extends unknown[]> = T extends [] ? never : T[0]
|
解法二,使用关键字infer
来对数组进行操作,infer
关键字官方翻译为推断,infer
必须要结合extends
一起使用,可以在条件语句中声明一个变量,接收待推断的类型,例如:
1
2
3
4
|
// 可以获取函数返回值的类型
type getReturnType<T> = T extends (...args:any[]) => infer R ? R : any
type funType = getReturnType<(str: string) => string> // string
|
所以,只要使用infer
来推断参数数组中第一个元素,返回就可以了:
1
|
type First<T extends unknown[]> = T extends [infer First,...unknown[]] ? First : never
|
test-case
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<First<[3, 2, 1]>, 3>>,
Expect<Equal<First<[() => 123, { a: string }]>, () => 123>>,
Expect<Equal<First<[]>, never>>,
Expect<Equal<First<[undefined]>, undefined>>,
]
type errors = [
// @ts-expect-error
First<'notArray'>,
// @ts-expect-error
First<{ 0: 'arrayLike' }>,
]
|
18_Length_of_Tuple
题目
创建一个通用的Length
,接受一个readonly
的数组,返回这个数组的长度。
例如:
1
2
3
4
5
|
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
|
知识点
解题
1
2
|
// 签名
type Length<T> = any
|
首先处理一下测试用例中的 @ts-expect-error
,只要加上约束即可,参数是字符串元组,元组是只读的,所以:
1
|
type Length<T extends readonly string[]> = any
|
然后,TS 中取数组长度有一个最简单的方法,就是 T["length"]
,所以这题可以直接用这个方法解:
1
|
type Length<T extends readonly string[]> = T["length"]
|
test-case
1
2
3
4
5
6
7
8
9
10
11
12
13
|
import type { Equal, Expect } from '@type-challenges/utils'
const tesla = ['tesla', 'model 3', 'model X', 'model Y'] as const
const spaceX = ['FALCON 9', 'FALCON HEAVY', 'DRAGON', 'STARSHIP', 'HUMAN SPACEFLIGHT'] as const
type cases = [
Expect<Equal<Length<typeof tesla>, 4>>,
Expect<Equal<Length<typeof spaceX>, 5>>,
// @ts-expect-error
Length<5>,
// @ts-expect-error
Length<'hello world'>,
]
|
测试通过。
(持续更新中 …)