ts类型体操训练

380 阅读5分钟

前期准备

在进行类型体操训练前,首先需要有一定的ts基础知识,在这个基础之上,通过类型体操训练可以帮助我们更好的理解和使用ts。 建议至少过两遍官方文档演练场是个好地方,当你有任何疑惑的时候可以直接写出来验证你的想法。

体操训练

地址:类型体操训练

一点建议:虽然可以直接在网页中编写,但还是强烈建议大家下载到本地。一方面可以帮助自己了解ts的相关配置,另一方面也可以更好的留存。有什么想要要记的随时记录,用的时候查询也方便。随学随记是个好习惯,做过一遍可能并不能良好的吸收,记录下来方便随时查看,随着时间的叠加这便是宝贵的财富。

目的

在写本文之前,我在本地完成了easy和medium题目,目前在挑战hard中。一是为知识的分享,二是想让自己回顾下前面的知识点,后续的题目中多是对前面题目的灵活组合运用。

easy

实现Pick

Pick是ts自带的工具类型,除Pick外还有Omit、ReadOnly、Exclude等,掌握这些内容可以帮助我们事办功倍。

什么是Pick,Pick帮助我们在接口类型中取出我们想要的某一或某几项,与之对应是Omit

interface Todo {
    title: string;
    description: string;
    completed: boolean;
}

type TodoPreview = Pick<Todo, "title" | "completed">;

TodoPreview {
    title: string;
    completed: boolean;
}

在代码中使用范型时可以使用更有语义的词,比如这里使用了KeyType,而没有使用U或者K。

// 实现
type MyPick<T, KeyType extends keyof T> = {
  [K in KeyType]: T[K];
};
实现Readonly
// 实现
type MyReadonly<T> = { readonly [Key in keyof T]: T[Key] };
Tuple to Object(元组转换为对象)
// example
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'}
// 实现
type TupleToObject<T extends readonly string[]> = {
  [K in T[number]]: K;
};
First of Array (获取元组第一项)
// example
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: type First<T extends any[]> = T extends [] ? never : T[0];
解法2: type First<T extends any[]> = T["length"] extends 0 ? never : T[0];
Length of Tuple(获取元组长度)
// example
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
// 实现
type Length<T extends readonly any[]> = T["length"];
实现Exclude

image.png

// 实现
// 当类型参数为联合类型,且在extends左边,每个元素会被单独处理然后拼接在一起
type MyExclude<T, U> = T extends U ? never : T;
Awaited
假如我们有一个 Promise 对象,这个 Promise 对象会返回一个类型。在 TS 中,我们用 Promise 中的 T 来描述这个 Promise 返回的类型。请你实现一个类型,可以获取这个类型。

比如:`Promise<ExampleType>`,请你返回 ExampleType 类型。
type MyAwaited<T extends Promise<any>> = T extends Promise<infer U>
  ? U extends Promise<any>
    ? MyAwaited<U>
    : U
  : T;
  
type X = Promise<string>;
type Y = Promise<{ field: number }>;
type Z = Promise<Promise<string | number>>;

type ExampleTypeX =  MyAwaited<X>  // string;
type ExampleTypeY =  MyAwaited<Y>  // { field: number };
type ExampleTypeZ =  MyAwaited<Z>  // string | number;
If
实现一个 `If` 类型,它接收一个条件类型 `C` ,一个判断为真时的返回类型 `T` ,以及一个判断为假时的返回类型 `F`。 `C` 只能是 `true` 或者 `false`, `T` 和 `F` 可以是任意类型。

举例:
type A = If<true, 'a', 'b'>  // expected to be 'a'
type B = If<false, 'a', 'b'> // expected to be 'b'


type If<C extends boolean, T, F> = C extends true ? T : F;

Concat
在类型系统里实现 JavaScript 内置的 `Array.concat` 方法,这个类型接受两个参数,返回的新数组类型应该按照输入参数从左到右的顺序合并为一个新的数组。

举例:
type Result = Concat<[1], [2]> // expected to be [1, 2]
type Concat<T extends any[], U extends any[]> = [...T, ...U];
Includes
在类型系统里实现 JavaScript 的 `Array.includes` 方法,这个类型接受两个参数,返回的类型要么是 `true` 要么是 `false`。


举例:
type isPillarMen = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'> 
// expected to be `false`
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y
  ? 1
  : 2
  ? true
  : false;

type Includes<T extends readonly any[], U> = T extends [infer F, ...infer R]
  ? Equal<F, U> extends true
    ? true
    : Includes<R, U>
  : false;
Push
在类型系统里实现通用的 `Array.push`。

举例:
type Result = Push<[1, 2], '3'> // [1, 2, '3']
type Push<T extends unknown[], U> = [...T, U];
Unshift
实现类型版本的 `Array.unshift`。
实现原理和上面类似

举例:
type Result = Unshift<[1, 2], 0> // [0, 1, 2,]
type Unshift<T extends unknown[], U> = [U, ...T];
实现Parameters
// 获取函数参数的类型  
type MyParameters<T extends (...args: any[]) => any> = T extends (
  ...args: infer Arguments
) => any
  ? Arguments
  : never;
  
  
// case

const foo = (arg1: string, arg2: number): void => {};
const bar = (arg1: boolean, arg2: { a: "A" }): void => {};
const baz = (): void => {};
type FooParameters = MyParameters<typeof foo>   //[string, number]
type BarParameters = MyParameters<typeof bar>   //[boolean, { a: "A" }]
type BazParameters = MyParameters<typeof baz>   //[]

medium

实现ReturnType
// 示例
const fn = (v: boolean) => {
  if (v)
    return 1
  else
    return 2
}

type a = MyReturnType<typeof fn> // 应推导出 "1 | 2"
type MyReturnType<T extends Function> = T extends (
  ...args: any[]
) => infer ReturnType
  ? ReturnType
  : never;
实现Omit

Omit 会创建一个省略 K 中字段的 T 对象。

// 示例

interface Todo {
  title: string
  description: string
  completed: boolean
}

type TodoPreview = MyOmit<Todo, 'description' | 'title'>

const todo: TodoPreview = {
  completed: false,
}
// 实现
// 方法1
type MyExclude<T, U> = T extends U ? never : T;

type MyOmit<T, K extends keyof T> = {
  [P in MyExclude<keyof T, K>]: T[P];
};

// 方法2
type MyOmit<T, K> = { [Key in keyof T as Key extends K ? never : Key]: T[Key] };
实现Readonly

实现一个通用MyReadonly2<T, K>,它带有两种类型的参数TK

K指定应设置为Readonly的T的属性集。如果未提供K,则应使所有属性都变为只读,就像普通的Readonly<T>一样。

// 示例

interface Todo {
  title: string
  description: string
  completed: boolean
}

const todo: MyReadonly2<Todo, 'title' | 'description'> = {
  title: "Hey",
  description: "foobar",
  completed: false,
}

todo.title = "Hello" // Error: cannot reassign a readonly property
todo.description = "barFoo" // Error: cannot reassign a readonly property
todo.completed = true // OK
// 实现
type MyReadonly2<T, K extends keyof T = keyof T> = Omit<T, K> & {
  readonly [U in K]: T[U];
};
深度Readonly

实现一个通用的DeepReadonly<T>,它将对象的每个参数及其子对象递归地设为只读。

// 示例
type X = { 
  x: { 
    a: 1
    b: 'hi'
  }
  y: 'hey'
}

type Expected = { 
  readonly x: { 
    readonly a: 1
    readonly b: 'hi'
  }
  readonly y: 'hey' 
}

type Todo = DeepReadonly<X> // should be same as `Expected`
// 实现
type MyReadonly2<T, K extends keyof T = keyof T> = Omit<T, K> & {
  readonly [U in K]: T[U];
};