https://github.com/type-challenges/type-challenges/blob/master/README.zh-CN.md
TypeScript 的类型系统是图灵完备的,也就是说它能描述任何可计算逻辑,简单点来说就是循环、条件判断等该有的语法都有。
如果只是把ts看作为一个js的类型插件,那以下的题目有相当大的一部分在js中是无法应用的,因为js缺少tuple等的无法去typeof推导的类型。期待一个纯ts的runtime。
前置
keyof interface
keyof返回interface的每一个key生成一个union类型
[K in union]
in遍历union类型
interface Todo {
title: string
description: string
completed: boolean
}
type U = keyof Todo // U 为 title | description | completed
type O = {[K in U]: string} // O 为 { title: string, description: string, complete: string }
const obj:O = {
title:'标题',
description: '描述',
completed: '完成',
}
infer
infer 关键字表示在 extends 条件语句中待推断的类型变量
type MyReturnType
这句表示如果T能赋值给(…arg: any) => infer P则结果是P否则返回never
challenge
simple
实现Pick
interface Todo {
title: string
description: string
completed: boolean
}
type TodoPreview = MyPick<Todo, 'title' | 'completed'>
const todo: TodoPreview = {
title: 'Clean room',
completed: false,
}
// solution
type MyPick<T, K extends keyof T> = {
[P in K]: T[P]
}
实现Readonly与Mutable
type Person = {
readonly name: string;
age: number;
}
// 结果:{ readonly name: string; readonly age: number; }
type ReadonlyResult = MyReadonly<Person>
// 结果:{ name: string; age: number; }
type MutableResult = MyMutable<Person>
// solution
type MyReadonly<T> = {
readonly [P in keyof T]: T[P]
}
type MyMutable<T> = {
-readonly [P in keyof T]: T[P]
}
note
- -readonly:表示把readonly关键词去掉,去掉之后此字段变为可改的.
实现Partical
interface Todo {
title: string
description: string
}
const todo: MyPartial<Todo> = {
title: "Hey",
description: "foobar"
}
// solution
type MyPartial<T> = {
[P in keyof T]?: T[P]
}
实现Exclude(差集)、Omit(省略)与Extract(交集)
type T0 = MyExclude<"a" | "b" | "c", "a">; // T0 = "b" | "c"
// soultion
type MyExclude<T, U> = T extends U ? never: T
// note
// never永不使用的类型
Extract(交集)与Exclude相反
// soultion
type MyExclude<T, U> = T extends U ? T : never
Omit可以借助在上面已经实现过的Pick和Exclude配合来实现,如下:
// Omit实现
type MyOmit<T, K> = Pick<T, Exclude<keyof T, K>>
实现Record
type keys = 'Cat'|'Dot'
type Animal = {
name: string;
age: number;
}
type Expected = {
Cat: {
name: string;
age: number;
};
Dog: {
name: string;
age: number;
}
}
// 结果:Expected
type RecordResult = MyRecord<keys, Animal>
// solution
type MyRecord<K extends keyof any, T> = {
[P in K]: T;
};
note
K extends keyof any表示任意类型的K
First期望返回数组的第一个元素
// 结果:3
type result1 = First<[3, 2, 1]>
// 结果:never
type result2 = First<[]>
// solution
// 索引实现方式
type First<T extends any[]> = T extends [] ? never : T[0]
// 占位实现方式
type First<T extends any[]> = T extends [infer R, ...infer L] ? R : never
note
- T extends []:用来判断T是否是一个空数组。
- T[0]:根据下标取数组第一个元素。
- infer R: 表示数组第一个元素的占位。
- …infer L: 表示数组剩余元素的占位。
Last期望返回数组中的最后一个元素
```typescript // way1:索引思想 type Last// 结果:3 type result = Last<[1, 2, 3]> // 结果:never type result2 = First<[]>
= [any, …T][T[‘length’]]
// way2: 后占位思想
type Last
**note**<br />**索引思想实现**<br />[any, ...T]:此代码表示我们构建了一个新数组,**并添加了一个新元素到第一个位置,然后把原数组T中的元素依次push到any之后中**,可以用以下伪代码表示:
```typescript
// 原数组
const T = [1, 2, 3]
// 新数组
const arr = [any, 1, 2, 3]
// 结果: 3
const result = arr[T['length']]
T[‘length’]:这里我们获取到的是原始T数组的长度,例如[1, 2, 3],长度值为3。而在新数组中由于第一个位置插入了一个any,因此索引为3的位置正好是最后一个元素的索引,通过这种方式就能达到我们的目的。
占位符实现
把最后一个元素单独领出来,如果extends成立就返回这个L
Pop、Push、Shift、Unshift【用不到】
// Pop结果:[1, 2]
type popResult = Pop<[1, 2, 3]>
// Push结果:[1, 2, 3, 4]
type pushResult = Push<[1, 2, 3], 4>
// Shift结果:[2, 3]
type shiftResult = Shift<[1, 2, 3]>
// Unshift结果:[0, 1, 2, 3]
type unshiftResult = Unshift<[1, 2, 3], 0>
// solution
type Pop<T> = T extends [...infer R, infer _L] ? R : never // _L未使用只是作为占位符
type Push<T extends any[], K> = [...T, K]
type Shift<T extends any[]> = T extends [infer F, ...infer R] ? R : never
type Unshift<T extends any[], K> = [K, ...T]
note
继续索引与占位符思想
只能对定长的元组使用类型推断,对于
const list = [1, 2, 3] typeof list只能推断是一个number[],实用性几乎没有~
Trim、TrimLeft以及TrimRight【用不到】
const t1 = TrimLeft<' str'> // 'str'
const t2 = Trim<' str '> // 'str'
const t3 = TrimRight<'str '> // 'str'
// solution
type Space = ' ' | '\n' | '\t'
type TrimLeft<S extends string> = S extends `${Space}${infer R}` ? TrimLeft<R> : S
type Trim<S extends string> = S extends (`${Space}${infer R}` | `${infer R}${Space}`) ? Trim<R> : S
type TrimRight<S extends string> = S extends `${infer R}${Space}` ? TrimRight<R> : S
note
由于’str’不能是变量 typeof str,所以实用性几乎没有
实现ReturnType
不使用 ReturnType 实现 TypeScript 的 ReturnType
例如:
type Fn = (v: boolean) => 1 | 2
const fn:Fn = (v: boolean) => {
if (v)
return 1
else
return 2
}
// type a = ReturnType<typeof fn> // 应推导出 "1 | 2"
// solution
type MyReturnType<T> = T extends (...arg: any) => infer P ? P :never;
type a = MyReturnType<typeof fn> // 应推导出 "1 | 2"
note
infer 关键字表示在 extends 条件语句中待推断的类型变量
type MyReturnType
这句表示如果T能赋值给(…arg: any) => infer P则结果是P否则返回never
PromiseType(promise包裹类型)
PromiseType是用来获取Promise包裹类型的,例如:
function getInfo (): Promise<string|number> {
return Promise.resolve(1)
}
// 结果:() => Promise<string|number>
type funcType = typeof getInfo
// 结果:Promise<string|number>
type returnResult = ReturnType<funcType>
// 结果:string|number
type PromiseResult = PromiseType<returnResult>
// solution
type PromiseResult<T> = T extends Promise(infer R) ? R : never
note
- T extends Promise
:判断T是否是Promise 的子类型
Parameters(函数的参数类型)
Parameters是用来获取一个函数的参数类型的,其中获取的结果是一个元组,用法如下:
const add = (a: number, b: string): void => {}
// [number, string]
type result = MyParameters<typeof add>
// solution
type MyParameters<T extends (...args: any) => any>
= T extends (...args: infer R) => any ? R : never
middle
deepReadonly
// 类型:
type X = {
b: string
c: {
d: boolean
e: undefined,
f: null
}
}
// 期望结果:
type Y = {
readonly b: string
readonly c: {
readonly d: boolean
readonly e: undefined,
readonly f: null
}
}
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends Record<string, any> ? DeepReadonly<T[P]> : T[P]
}
type t = DeepReadonly<X>
note
如果是一个Record
AppendToObject(对象添加新属性)
// 结果:{ id: number; name: string; }
interface Obj {
id: number
}
type result = AppendToObject<Obj, 'name', string>
// solution
type basicKeyType = string | number | symbol
type AppendToObject<T, K extends basicKeyType, V> = {
[P in keyof T | K]: P extends keyof T ? T[P] : V
}
note
- basicKeyType:在JavaScript中,因为一个对象的属性只能是string、number或者symbol这三种类型,所以我们限定K必须满足此条件。
- [P in keyof T | K]:这里表示keyof T的联合类型和K【 [(P in keyof T) | K]】,组合成一个新的联合类型。
ConcatObj,合并interface
// 结果:{ id: number; name: string; }
interface Obj1 {
id: number
}
interface Obj2 {
name: string;
}
// solution
type ConcatObj<T, N extends object> = {
[P in (keyof T | keyof N)]: P extends keyof T
? T[P] : P extends keyof N
? N[P] : never
}