#built-in
是 type-challenges
库中的一个 tag
,里面一共包括了 7
个类型,其中前两个就是之前介绍过的类型体操之实现 Pick 和 Omit 中的 Pick
和 Omit
,这 7
个类型如下
Pick
Readonly
Exclude
Awaited
Parameters
ReturnType
Omit
都是 TS
的内置类型(buit-in
),大多数都是属于 type-challenges
简单难度的类型挑战
关于 type-challenges
库的介绍、其对应测试方法以及 Pick
和 Omit
类型的实现可以参考类型体操之实现 Pick 和 Omit
Readonly
题目描述
不要使用内置的 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
测试用例
/* _____________ 测试用例 _____________ */
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
}
}
题目模板
/* _____________ 你的代码 _____________ */
type MyReadonly<T> = any
解答
Readonly
字如其名,就是只能读,ES6
的 const
就是这种,const
的特性是浏览器提供的,并不是通过某种 JS
逻辑实现的,而 Readonly
在 ts
也是如此
靠的是 ts
的 readonly
属性修饰符
readonly
可以在class
interface
type
array-like
定义中使用它,也可以用来定义一个函数的参数。既然是只读的意味着一旦定义了就不能再修改,所以这些属性必须在声明的时候或者在类中对它进行初始化。
interface Point {
readonly x: number;
readonly y: number;
}
const start: Point = {
x: 0,
y: 0
}
start.x = 2 // 报错 Cannot assign to 'x' because it is a read-only property.
那么 readonly
的实现可以说是手到擒来了,解答如下
type MyReadonly<T> = {
readonly [P in keyof T]: T[P]
}
用一个泛型 P
指代 T
中的任意一个属性,keyof T
则用于获取 T
类型的所有键,其返回类型是联合类型和 Object.keys()
非常像哦
const todo1 = {
title: "",
description: "",
completed: true,
meta: {
author: "",
}
}
Object.keys(todo1);
// (4)['title', 'description', 'completed', 'meta']
而 in
是 JS
的原生运算符,如果指定属性在指定对象中,则 in
运算符返回 true
,而对于 ts
来说,这种语法 [P in K]
可以用来遍历联合类型里的类型(注意,遍历每一个)
T[P]
的意思就比较好理解了,interface
具象化为对象,T[P]
不就是表示 T
中属性为 P
的类型值吗?即 Todo['title']: string
留一个伏笔,注意上面的 Object.keys()
的返回结果,不包含 author
属性,而解答中的 Readonly
也一样,只是一个浅层的只读
Exclude
题目描述
实现内置的 Exclude<T, U>
类型,但不能直接使用它本身。
从联合类型T中排除U的类型成员,来构造一个新的类型。
例如:
type Result = MyExclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'
测试用例
/* _____________ 测试用例 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<MyExclude<'a' | 'b' | 'c', 'a'>, 'b' | 'c'>>,
Expect<Equal<MyExclude<'a' | 'b' | 'c', 'a' | 'b'>, 'c'>>,
Expect<Equal<MyExclude<string | number | (() => void), Function>, string | number>>,
]
题目模板
/* _____________ 你的代码 _____________ */
type MyExclude<T, U> = any
解答
Exclude
不就是纯纯的联合类型版 Pick
吗?也是一个筛选,我直接一个 API
实现(JS
版)
const MyExclude = T.filter(item => {
return !U.include(item);
})
而对于 TS
来说,就没有这么好用的函数、 lambda
,其实现如下
type MyExclude<T, U> = T extends U ? never : T;
这个三元表达式可以视为 !U.include(item)
,也可以视为 T.filter(() => {...})
,因为如果 T
是一个联合类型,则会对每一个 T
的成员执行一次这个三元表达式,即 T.item extends U ? never : T.item
而 never
就有意思了,never
通常用来表示一个从来不会有返回值的函数,而三元表达式实际上就是 lambda
函数,特殊一点的函数,因此在此处可以作为一个筛选的作用,即当 T extends U
为真时,不会有类型返回
Awaited
题目描述
假如我们有一个 Promise
对象,这个 Promise
对象会返回一个类型。在 TS
中,我们用 Promise
中的 T
来描述这个 Promise
返回的类型。请你实现一个类型,可以获取这个类型。
例如:Promise<ExampleType>
,请你返回 ExampleType
类型。
type ExampleType = Promise<string>
type Result = MyAwaited<ExampleType> // string
测试用例
/* _____________ 测试用例 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type X = Promise<string>
type Y = Promise<{ field: number }>
type Z = Promise<Promise<string | number>>
type Z1 = Promise<Promise<Promise<string | boolean>>>
type T = { then: (onfulfilled: (arg: number) => any) => any }
type cases = [
Expect<Equal<MyAwaited<X>, string>>,
Expect<Equal<MyAwaited<Y>, { field: number }>>,
Expect<Equal<MyAwaited<Z>, string | number>>,
Expect<Equal<MyAwaited<Z1>, string | boolean>>,
Expect<Equal<MyAwaited<T>, number>>,
]
// @ts-expect-error
type error = MyAwaited<number>
题目模板
/* _____________ 你的代码 _____________ */
type MyAwaited<T> = any
解答
这道题不应该是简单
仔细观察题目描述,本身是一个很简单的例子
type Result = MyAwaited<Promise<string>> // string
实现上可以通过 extends
条件类型和 infer
推断类型,extends
在 Exclude
中已经介绍过了,而 infer
其实就是一种 运行时 的类型,和泛型其实差不多,举个例子
type Awaited<T> = T extends Promise<infer U> ? U : T;
假设 T: Promise<string>
和 U: string
则
type Awaited<Promise<string>> = Promise<string> extends Promise<string> ? string : Promise<string>;
type Awaited<Promise<string>> = string;
假设 T: Promise<string>
和 U: number
则
type Awaited<Promise<string>> = Promise<string> extends Promise<number> ? string : Promise<string>;
type Awaited<Promise<string>> = Promise<string>;
但实际上在运行前定义时我们不知道 U
是啥,也没有传入,所以就需要使用 infer
来做个占位,留空,同时也避免和传入的泛型冲突
很有意思的特性
但实际上这样的写法过不了所有测试
type MyAwaited<T> = T extends Promise<infer U> ? U : T;
这就要求做题家需要了解一下 Promise
的两个特性,链式调用以及兼容其它 Promise
实现(可以谷歌一下)
链式调用其实就是嵌套调用,实现如下
type MyAwaited<T extends Promise<any>> = T extends Promise<infer R>
? R extends Promise<any>
? MyAwaited<R>
: R
: never;
如果 R
仍然属于 Promise<any>
类型,那么就再来一次 MyAwaited<R>
,否则直接返回 R
兼容其它 Promise
实现,则可以使用 PromiseLike
这个内置类型
PS: 所谓的其它 Promise
实现就是必须具有 then
属性的对象
因此最终的实现如下
type MyAwaited<T extends PromiseLike<any>> = T extends PromiseLike<infer R>
? R extends PromiseLike<any>
? MyAwaited<R>
: R
: never;
Parameters
题目描述
实现内置的 Parameters
类型,而不是直接使用它,可参考 TypeScript官方文档。
例如:
const foo = (arg1: string, arg2: number): void => {}
type FunctionParamsType = MyParameters<typeof foo> // [arg1: string, arg2: number]
测试用例
/* _____________ 测试用例 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
const foo = (arg1: string, arg2: number): void => {}
const bar = (arg1: boolean, arg2: { a: 'A' }): void => {}
const baz = (): void => {}
type cases = [
Expect<Equal<MyParameters<typeof foo>, [string, number]>>,
Expect<Equal<MyParameters<typeof bar>, [boolean, { a: 'A' }]>>,
Expect<Equal<MyParameters<typeof baz>, []>>,
]
题目模板
/* _____________ 你的代码 _____________ */
type MyParameters<T extends (...args: any[]) => any> = any