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类型

  1. interface Todo {
  2. title: string
  3. description: string
  4. completed: boolean
  5. }
  6. type U = keyof Todo // U 为 title | description | completed
  7. type O = {[K in U]: string} // O 为 { title: string, description: string, complete: string }
  8. const obj:O = {
  9. title:'标题',
  10. description: '描述',
  11. completed: '完成',
  12. }

infer

infer 关键字表示在 extends 条件语句中待推断的类型变量
type MyReturnType = T extends (…arg: any) => infer P ? P :never;
这句表示如果T能赋值给(…arg: any) => infer P则结果是P否则返回never

challenge

simple

实现Pick

  1. interface Todo {
  2. title: string
  3. description: string
  4. completed: boolean
  5. }
  6. type TodoPreview = MyPick<Todo, 'title' | 'completed'>
  7. const todo: TodoPreview = {
  8. title: 'Clean room',
  9. completed: false,
  10. }
  11. // solution
  12. type MyPick<T, K extends keyof T> = {
  13. [P in K]: T[P]
  14. }

实现Readonly与Mutable

  1. type Person = {
  2. readonly name: string;
  3. age: number;
  4. }
  5. // 结果:{ readonly name: string; readonly age: number; }
  6. type ReadonlyResult = MyReadonly<Person>
  7. // 结果:{ name: string; age: number; }
  8. type MutableResult = MyMutable<Person>
  9. // solution
  10. type MyReadonly<T> = {
  11. readonly [P in keyof T]: T[P]
  12. }
  13. type MyMutable<T> = {
  14. -readonly [P in keyof T]: T[P]
  15. }

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期望返回数组中的最后一个元素

    // 结果:3
    type result = Last<[1, 2, 3]>
    // 结果:never
    type result2 = First<[]>
    
    ```typescript // way1:索引思想 type Last = [any, …T][T[‘length’]]

// way2: 后占位思想 type Last = T extends […infer R, infer L] ? L : never

**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 extends (…arg: any) => infer P ? P :never;
这句表示如果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类型则递归调用这个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
}

hard


ing…

参考答案
https://wangtunan.github.io/blog/typescript/challenge.html#tupletounion-%E5%85%83%E7%BB%84%E8%BD%AC%E8%81%94%E5%90%88%E7%B1%BB%E5%9E%8B