TS类型体操,从爬到跑!

序言

最近在努力学习TS 类型体操,这篇文章作为一篇学习汇总,题目由浅入深,可以帮助需要的人快速上手TS,如有疑问,欢迎指出讨论。

第一题

以下代码为什么会提示错误,如何解决呢。

type User = {
  id: number;
  kind: string;
};

function makeCustomer<T extends User>(u: T): T {
  return {
    id: u.id,
    kind: 'customer'
  }
}
 // Error(TS 编译器版本:v4.4.2)
  // Type '{ id: number; kind: string; }' is not assignable to type 'T'.
  // '{ id: number; kind: string; }' is assignable to the constraint of type 'T', 
  // but 'T' could be instantiated with a different subtype of constraint 'User'.
复制代码

考察内容

对 extends 的理解。

题解

extends 的含义是类型约束, T 受到 User的约束,必须包含User 中规定的类型,但是不局限于 User 中的类型,也就是说 T 的类型也许会更广泛,所以返回时,多余类型没有处理已导致报错。解决方案就是处理好多余的类型

type User = {
  id: number;
  kind: string;
};

// T assignable User
function makeCustomer<T extends User>(u: T): T {
  return {
    ...u,
    id: u.id,
    kind: 'customer'
  }
}
复制代码

我们通过扩展运算符将多余的属性承载返回即可。

第二题

本道题我们希望参数 a 和 b 的类型都是一致的,即 a 和 b 同时为 number 或 string 类型。当它们的类型不一致的值,TS 类型检查器能自动提示对应的错误信息。

function f(a: string | number, b: string | number) {
  if (typeof a === 'string') {
    return a + ':' + b; // no error but b can be number!
  } else {
    return a + b; // error as b can be number | string
  }
}

f(2, 3); // Ok
f(1, 'a'); // Error
f('a', 2); // Error
f('a', 'b') // Ok
复制代码

考察内容

函数重载

题解

第一种:函数重载

function f(a: string, b: string): string
function f(a: number, b: number): number
function f(a: string | number, b: string | number ): string | number {
  if (typeof a === 'string') {
    return a + ':' + b;
  } else {
    return ((a as number) + (b as number));
  }
}

f(2, 3); // Ok
f(1, 'a'); // Error
f('a', 2); // Error
f('a', 'b') // Ok
复制代码

第三题

如何定义一个 SetOptional 工具类型,支持把给定的 keys 对应的属性变成可选的

type Foo = {
 a: number;
 b?: string;
 c: boolean;
}

// 测试用例
type SomeOptional = SetOptional<Foo, 'a' | 'b'>;

// type SomeOptional = {
//  a?: number; // 该属性已变成可选的
//  b?: string; // 保持不变
//  c: boolean; 
// }
复制代码

分析

题目是,将参数中的属性变为可选,其他不变,我们分析得出结论

  1. 首先获取到要变为可选的参数,我们用 pick 获取到参数中的属性,然后用Partial 将他们全部变为可选。
  2. 使用 Pick 和 Exclude 将不变化的参数拿出来.
  1. 将两个结果交叉类型,然后扁平处理
//获取处理完的交叉类型
type setOptional <T , K extends keyof T> = 
Partial<Pick<T , k>> & Pick< T, Exclude<keyof T ,K>>
//数组扁平处理
type flattening <T> = 
<P in keyof T> : T[p]

//最终版本
type flattening <T> = 
<P in keyof T> : T[p]

type setOptional <T , K extends keyof T> = 
flattening<Partial<Pick<T , k>> & Pick< T, Exclude<keyof T ,K>>>
复制代码

第四题

何定义一个 ConditionalPick 工具类型,支持根据指定的 Condition 条件来生成新的类型,对应的使用示例如下

interface Example {
 a: string;
 b: string | number;
 c: () => void;
 d: {};
}

// 测试用例:
type StringKeysOnly = ConditionalPick<Example, string>;
//=> {a: string}
复制代码

分析

题目要求我们可以过滤出给定类型的属性。我们的思路可以按照如下分析

  1. 遍历每一个属性,将他的类型约束于给定的类型,符合约束把该属性的类型赋值为属性名,不符合约束变成never
  2. 然后再把用pick 从 对象中 把不为never的属性拿出来
//首先我们定义一个拿出符合要求的key的方法

type ConditionalKeys<T, K> = {
	[P in keyof T ] : T[P] extends K ? P: never
}[keyof T]
/**
结果为:{a:a;b:never:c:never:d:never}[a|b|c|d]=>
a|never|never|never=>
a
**/
//然后我们通过Pick将符合的key对应的属性拿出来

type ConditionalPick<T ,Conditional> = Pick< T , ConditionalKeys<T, Conditional>>
复制代码

第五题

定义一个工具类型,AppendArgs ,为一个函数类型,增加指定类型的参数,新增加的参数名字为 x ,将作为新函类型的第一个参数,案例如下

type Fn = (a: number, b: string) => number
type AppendArgument<F, A> = // 你的实现代码

type FinalFn = AppendArgument<Fn, boolean> 
// (x: boolean, a: number, b: string) => number
复制代码

本题考查内容:

  1. Parameters (获取函数的参数类型)。
  2. ReturnType (获取函数的返回值类型)。
  1. 对 infer 的使用

题解1

使用 Parameters 和 ReturnType ,如果不了解,下面有详细解释。

type AppendArgument<F extends (...args: any) => any, A> 
  = (x: A, ...args: Parameters<F>) => ReturnType<F> 

type Fn = (a: number, b: string) => number
type FinalF = AppendArgument<Fn, boolean> 
// (x: boolean, a: number, b: string) => number
复制代码

首先定义好原先的 Fn 和 我们新定义的参数 A ,然后用 Parameters 将原先的参数类型取出,赋值给原先的参数,最后用 RetrnType 返回结果类型。

知识点补充

Parameters

declare function f1(arg: { a: number; b: string }): void;
 
type T0 = Parameters<() => string>;
type T0 = []

type T1 = Parameters<(s: string) => void>;
type T1 = [s: string]

type T2 = Parameters<<T>(arg: T) => T>;
type T2 = [arg: unknown]

type T3 = Parameters<typeof f1>;
type T3 = [arg: {
    a: number;
    b: string;
}]
复制代码

通过上例我们可以知道, Paramters 接受一个function 参数或者是 typeof Fn ,返回的是该函数的参数类型。

ReturnType

declare function f1(): { a: number; b: string };
 
type T0 = ReturnType<() => string>;
     
type T0 = string
type T1 = ReturnType<(s: string) => void>;
     
type T1 = void
type T2 = ReturnType<<T>() => T>;
     
type T2 = unknown
type T3 = ReturnType<<T extends U, U extends number[]>() => T>;
     
type T3 = number[]
type T4 = ReturnType<typeof f1>;
     
type T4 = {
    a: number;
    b: string;
}
复制代码

ReturnType 同样接受一个 Fn,构造一个由 function 的返回类型组成的类型Type。

题解2:

infer

第六题

定义一个 NativeFlat 工具类型,支持把数组类型拍平(扁平化)。具体的使用示例如下所示:

type NaiveFlat<T extends any[]> = // 你的实现代码

// 测试用例:
type NaiveResult = NaiveFlat<[['a'], ['b', 'c'], ['d']]>
// NaiveResult的结果: "a" | "b" | "c" | "d"
复制代码

在完成 NaiveFlat 工具类型之后,在继续实现 DeepFlat 工具类型,以支持多维数组类型:

type DeepFlat<T extends any[]> = unknown // 你的实现代码

// 测试用例
type Deep = [['a'], ['b', 'c'], [['d']], [[[['e']]]]];
type DeepTestResult = DeepFlat<Deep>  
// DeepTestResult: "a" | "b" | "c" | "d" | "e"
复制代码

考查内容

要数组扁平化,typeof ArrayInstance[number] ,通过数组值获取对应类型

题解 NativeFlat

type NaiveFlat<T extends any[]> = {
	[P in keyof T ] : T[P] extends any[] ? T[P][number] : T[P]
}[number]
//NaiveResult = ["a", "b" | "c", "d"][number]
type NaiveResult = NaiveFlat<[['a'], ['b', 'c'], ['d']]>
// NaiveResult: "a" | "b" | "c" | "d" | "e"
复制代码

本题的思路并不复杂,首先拿到每一项,然后判断这项是否为数组,如果是就用 [number] 这种方法将他转换为联合类型,最后返回的是一个一元数组,我们的目标是返回联合类型,所以还需要使用一次[number] 将结果的一元数组再转换成联合类型

题解 DeepFlat

type DeepFlat<T extends any[]> = {
	[P in keyof T ] : T[P] extends any[] ? DeepFlat<T[P]>: T[P]
}[number]
type DeepFlatResult = DeepFlat<[['a'], ['b', 'c'], [['d']], [[[['e']]]]]>
复制代码

这题其实是上面的衍生题,主要就是利用递归实现多维数组的展开

第七题

使用类型别名定义一个 EmptyObject 类型,使得该类型只允许空对象赋值

type EmptyObject = {} 

// 测试用例
const shouldPass: EmptyObject = {}; // 可以正常赋值
const shouldFail: EmptyObject = { // 将出现编译错误
  prop: "TS"
}
复制代码

使用类型别名定义一个 takeSomeTypeOnly 函数的类型定义,让它的参数只允许严格SomeType类型的值。具体的使用示例如下所示

type SomeType =  {
  prop: string
}

// 更改以下函数的类型定义,让它的参数只允许严格SomeType类型的值
function takeSomeTypeOnly(x: SomeType) { return x }

// 测试用例:
const x = { prop: 'a' };
takeSomeTypeOnly(x) // 可以正常调用

const y = { prop: 'a', addditionalProp: 'x' };
takeSomeTypeOnly(y) // 将出现编译错误
复制代码

考查内容

never 的应用,如果一个类型被赋值为了never,那么它将不接受任何赋值,这样就起到了限制参数的作用。

题解 EmptyObject

type EmptyObject = {
  // type PropertyKey = string | number | symbol
  [K in PropertyKey]: never 
}

// 测试用例
const shouldPass: EmptyObject = {}; // 可以正常赋值
const shouldFail: EmptyObject = { // 将出现编译错误
  prop: "TS"
}
复制代码

解题思路为,将该对象key值遍历,将所有属性赋值为never,属性为never类型意味着无法赋值,无法赋值也就没法拥有属性,有属性就有类型,有类型就不是never,所以就会报错。

题解 takeSomeTypeOnly

解题思路:

  1. SomeType类型的属性就不管,不是SomeType就把类型变为never
type SomeType =  {
  prop: string
}

type Exclusive<T1, T2 extends T1> = {
  [K in keyof T2]: K extends keyof T1 ? T2[K] : never 
}

// 更改以下函数的类型定义,让它的参数只允许严格SomeType类型的值
function takeSomeTypeOnly<T extends SomeType>(x: Exclusive<SomeType, T>) { return x }

// 测试用例:
const x = { prop: 'a' };
takeSomeTypeOnly(x) // 可以正常调用

const y = { prop: 'a', addditionalProp: 'x' };
takeSomeTypeOnly(y) // 将出现编译错误
复制代码

定义入参,将SomeType的通过,没包含的赋值为never将无法通过。

第八题

定义 NonEmptyArray 工具类型,用于确保数据非空数组。

type NonEmptyArray<T> = // 你的实现代码

const a: NonEmptyArray<string> = [] // 将出现编译错误
const b: NonEmptyArray<string> = ['Hello TS'] // 非空数据,正常使用
复制代码

考察内容

  1. 如何获取数组的第一项并赋予类型

题解1

type NonEmptyArray<T> = [T, ...T[]]
复制代码

将数组第一项定义为 T 类型,如果是空数组,第一项将为 undifind类型

题解2

type NonEmptyArray<T> = T[] & { 0: T };
复制代码

利用交叉类型,交叉结果为索引 0 的那项类型为T,目的是和第一种结果一样

第九题

定义一个 JoinStrArray 工具类型,用于根据指定的 Separator 分隔符,对字符串数组类型进行拼接。具体的使用示例如下所示

type JoinStrArray<Arr extends string[], 
     Separator extends string, Result extends string = ""> = // 你的实现代码

// 测试用例
type Names = ["Sem", "Lolo", "Kaquko"]
type NamesComma = JoinStrArray<Names, ","> // "Sem,Lolo,Kaquko"
type NamesSpace = JoinStrArray<Names, " "> // "Sem Lolo Kaquko"
type NamesStars = JoinStrArray<Names, "⭐️"> // "Sem⭐️Lolo⭐️Kaquko"
复制代码

考查内容

该题主要考察的是 infer 操作符,模板自变量和递归条件类型

解题思路

  1. 因为入参是一个数组,我们要重复的取数组中的元素,所以需要用到递归,那么就要做好结束条件的设计。
  2. 结束条件和字符串拼接,需要 infer 将 待拼接的和剩余的参数分开。等剩余参数没有了,就说明结束了,返回结果。
  1. 做字符串拼接,需要用到模板自变量。

递归关键点:

  1. JoinStrArray 的入参是 <数组,符号,和结果>
  2. 我们要做数组的递归,用 infer 分离出 当前要拼接的元素,和剩余带拼接的数组
  1. 把当前要拼接的元素拼接完成后作为 JoinStrArray 的入参结果,剩余的参数作为 入参的数组,符号保持不变,就完成了我们的拼接逻辑。
  2. 当 剩余参数不为 string[] ,说明没有剩余参数,退出递归。

题解

type JoinStrArray<Arr extends string[], Separator extends string, Result extends string = ""> = 
//第一步,使用infer 分离出 当前元素和剩余参数
Arr extends [infer El ,...infer Rest  ] 
//第二部判断是否有剩余参数
? Rest extends string[] 
//第二部判断是否为空数组
? El extends string 
//第三步判断是否第一次
?Result extends ""
? JoinStrArray< Rest ,Separator,`${El}`>
:JoinStrArray<Rest,Separator,`${Result}${Separator}${El}`> 
:`${Result}`
:`${Result}`
:`${Result}`
    

type Names = ["Sem", "Lolo", "Kaquko"]
type NamesComma = JoinStrArray<Names, ","> // "Sem,Lolo,Kaquko"
type NamesSpace = JoinStrArray<Names, " "> // "Sem Lolo Kaquko"
type NamesStars = JoinStrArray<Names, "⭐️"> // "Sem⭐️Lolo⭐️Kaquko"
复制代码

小结:本题主要关键点在于用 infer 分离出递归的结束条件,然后模板自变量做拼接。

第十题

实现一个 Trim 工具类型,用于对字符串字面量类型进行去空格处理。具体的使用示例如下所示

type Trim<V extends string> = // 你的实现代码

// 测试用例
Trim<' coolFish '>
//=> 'coolFish'
复制代码

考查内容

模板自变量和递归的使用

解题思路

我们要思考如何去掉一个空格呢?我们利用模板自变量,和 infer 操作符,分离出空格和内容,取出内容,就能去掉一个空格,再加入递归就能去掉多个空格。我们考虑到还有左右两种情况的空格。

type clearLeft<V extends string> = V extends ` ${infer R}` ?clearLeft<R>:V

type clearRight<V extends string> = V extends `${infer R} ` ?clearRight<R>:V

type Trim<V extends string> = clearRight<clearLeft<V>>

// 测试用例
type test  = Trim<' coolFish '>
//=> 'coolFish'
复制代码

小结:我们用模板自变量的方法一个一个的把空格去掉了

第十一题

实现一个 IsEqual 工具类型,用于比较两个类型是否相等。具体的使用示例如下所示:

type IsEqual<A, B> = // 你的实现代码

// 测试用例
type E0 = IsEqual<1, 2>; // false
type E1 = IsEqual<{ a: 1 }, { a: 1 }> // true
type E2 = IsEqual<[1], []>; // false
复制代码

考察内容

extends 的应用,这里有个小技巧,相互约束的两个属性,类型相同。

题解

type IsEqual<A, B> = A extends B? B extends A ? true :false :false

// 测试用例
type E0 = IsEqual<1, 2>; // false
type E1 = IsEqual<{ a: 1 }, { a: 1 }> // true
type E2 = IsEqual<[1], []>; // false
复制代码

第十二题

实现一个 Head 工具类型,用于获取数组类型的第一个类型。具体的使用示例如下所示

type Head<T extends Array<any>> = // 你的实现代码

// 测试用例
type H0 = Head<[]> // never
type H1 = Head<[1]> // 1
type H2 = Head<[3, 2]> // 3
复制代码

考察内容

  1. infer 声明变量,占位使用
  2. 数组非空判断

题解一

利用 infer 声明变量占位

type Head<T extends Array<any>> =
T extends [ first: infer R , ...rest: any[]  ] ?  R : never

// 测试用例
type H0 = Head<[]> // never
type H1 = Head<[1]> // 1
type H2 = Head<[3, 2]> // 3
复制代码

题解二

判断数组非空,然后取第一项

type Head<T extends Array<any>> = T['length'] extends 0 ? never :T[0]
// 测试用例
type H0 = Head<[]> // never
type H1 = Head<[1]> // 1
type H2 = Head<[3, 2]> // 3
复制代码

第十三题

实现一个 Tail 工具类型,用于获取数组类型除了第一个类型外,剩余的类型。具体的使用示例如下所示:

type Tail<T extends Array<any>> =  // 你的实现代码

// 测试用例
type T0 = Tail<[]> // []
type T1 = Tail<[1, 2]> // [2]
type T2 = Tail<[1, 2, 3, 4, 5]> // [2, 3, 4, 5]
复制代码

考察内容

infer 的应用

题解

type Tail<T extends Array<any>> =  T extends [first: infer A, ...rest :infer B] ? B :never

// 测试用例
type T0 = Tail<[]> // []
type T3 = Tail<[1]> // [2]
type T1 = Tail<[1, 2]> // [2]
type T2 = Tail<[1, 2, 3, 4, 5]> // [2, 3, 4, 5]
复制代码

第十四题

实现一个 Unshift 工具类型,用于把指定类型 E 作为第一个元素添加到 T 数组类型中。具体的使用示例如下所示:

type Unshift<T extends any[], E> =  // 你的实现代码

// 测试用例
type Arr0 = Unshift<[], 1>; // [1]
type Arr1 = Unshift<[1, 2, 3], 0>; // [0, 1, 2, 3]
复制代码

考察内容

对数组的理解,infer的使用 和扩展运算符的使用

题解

  1. 将传入的参数放在数组的第一项,原有的数组使用扩展运算符打开,组合成新的数组
type Unshift<T extends any[], E> =  [E,...T]

// 测试用例
type Arr0 = Unshift<[], 1>; // [1]
type Arr1 = Unshift<[1, 2, 3], 0>; // [0, 1, 2, 3]
复制代码

第十五题

实现一个 Push 工具类型,用于把指定类型 E 作为第最后一个元素添加到 T 数组类型中。具体的使用示例如下所示:

type Push<T extends any[], V> = // 你的实现代码

// 测试用例
type Arr0 = Push<[], 1> // [1]
type Arr1 = Push<[1, 2, 3], 4> // [1, 2, 3, 4]
复制代码

考察内容

对数组的理解,infer的使用 和扩展运算符的使用

题解

  1. 将传入的参数放在数组的第一项,原有的数组使用扩展运算符打开,组合成新的数组
type Push<T extends any[], V> = [...T,V]

// 测试用例
type Arr0 = Push<[], 1> // [1]
type Arr1 = Push<[1, 2, 3], 4> // [1, 2, 3, 4]
复制代码

第十六题

实现一个 Shift 工具类型,用于移除 T 数组类型中的第一个类型。具体的使用示例如下所示:

type Shift<T extends any[]> = // 你的实现代码

// 测试用例
type S0 = Shift<[1, 2, 3]> 
type S1 = Shift<[string,number,boolean]> 
复制代码

考察内容

对数组的理解,infer的使用 和扩展运算符的使用

题解

利用 infer 占位,取出除第一项的其他所有项

type Shift<T extends any[]> = T extends [first:infer A, ...rest :infer B] ? B : never

// 测试用例
type S0 = Shift<[1, 2, 3]> 
type S1 = Shift<[string,number,boolean]> 
复制代码

猜你喜欢

转载自juejin.im/post/7016698434480504845