从TS类型体操入手,学习TS

很早之前就在github上面看到了type-challenges这个项目,但一直没去刷,最近准备面试,刚好借此复习一下TS。它的中文名叫 TypeScript 类型体操姿势合集,就是像Leetcode那样会有一些题目,然后根据题目要求完成类型的编写并通过测试用例。本文通过一些比较easy的题目,先梳理一下TS中比较基础的类型运算。

1、实现 Pick

题目要求

原题链接:00004-easy-pick
,题目的要求是:不使用 Pick<T, K> ,实现 TS 内置的 Pick<T, K> 的功能,从类型 T 中选出符合 K 的属性,构造一个新的类型,T是一个对象类型,K是一个联合类型

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

type TodoPreview = MyPick<Todo, 'title' | 'completed'>

const todo: TodoPreview = {
    title: 'Clean room',
    completed: false,
}

大致思路 🤔

如果从js的角度看的话,不就是给你一组key,然后去指定的对象上选取包含在这组key中的属性嘛。那我们只要循环这组key,然后挨个去取对象上对应的属性不就行了嘛,这道题的类型运算也差不多是这样的思路。

in 操作符 ⚔️

TypeScript 语言的类型运算中,in运算符有不同的用法,用来取出(遍历)联合类型的每一个成员类型。举个例子

type U = 'a'|'b'|'c';

type Foo = {
  [Prop in U]: number;
};
// 等同于
type Foo = {
  a: number,
  b: number,
  c: number
};

其实就可以认为是一种循环,循环遍历联合类型U

keyof 操作符 ⚔️

keyof 是一个单目运算符,用于将对象类型的键组合成一个联合类型。

interface T {
  0: boolean;
  a: string;
  b(): void;
}

type KeyT = keyof T; // 0 | 'a' | 'b'

方括号运算符 ⚔️

方括号运算符([])用于取出对象的键值类型,比如T[K]会返回对象T的属性K的类型。

type Person = {
  age: number;
  name: string;
  alive: boolean;
};

// Age 的类型是 number
type Age = Person['age'];

不要把这个方括号里的age理解成字符串,age是一个值类型,它是一个类型!!!把他换成一个类型别名也是可以的

type Person = {
  age: number;
  name: string;
  alive: boolean;
};
type key = 'age'
// Age 的类型是 number
type Age = Person[key];

这一点可以很好的理解这道题目的代码

答案 📄

答案已经呼之欲出了,用in操作符遍历第二个参数K,然后使用方括号取到对应属性的类型

type MyPick<T, K extends keyof T> = {
  [key in K]: T[key]
}

keyof将T的所有键转换成一个联合类型,extends用来约束K必须是keyof T的子集。这样做的目的是确保K中的每个分量在T中都存在对应的键。

搞定,下一题😎

2、对象属性只读

篇幅原因就不描述题目要求了,可以直接查看原题链接:00007-easy-readonly

大致思路 🤔

和上一道题做法差不多,不同的是新返回的对象和原来的在结构上是一模一样的,而且每个属性都变成了只读的。

readonly

readonly可以防止对象的属性被更改

type Person = {
  readonly age: number;
  name: string;
  alive: boolean;
};
const person: Person = {
    age: 18,
    name: 'jack',
    alive: true
}

// 报错 Cannot assign to 'age' because it is a read-only property.
person.age = 99

答案 📄

type MyReadonly<T> = {
  readonly [k in keyof T]: T[k]
}

就是给每个属性加上readonly修饰

3、元组转换为对象

原题链接:00011-easy-tuple-to-object,这道题就开始有点意思了。要先了解一下元组这个类型和方括号运算符的高级用法

元组 ⚔️

元组类型是另一种类型 Array 类型,它确切地知道它包含多少元素,以及它在特定位置包含哪些类型。这一点和数组很不一样,数组的长度是未知的,而且并不能够知道每个索引位置的类型。

type tuple = [string, number, boolean]
const tup: tuple = ['1', 1, true]

// 报错 Type 'number' is not assignable to type 'string'
const tup2: tuple = [1, 1, true]

tup2中的元素的类型并没有和tuple类型中一一对应,除此之外元素个数也要相等才能赋值。从对象的角度来看元组这个东西,其实就有点像一个键为数字的对象

type tuple = {
    0: string,
    1: number,
    2: boolean
}
const tup: tuple = ['1', 1, true]

这是可行的,和元组表达的意思也是一致的,第一个位置的元素类型为stirng,第二个位置的元素类型为number,第三个位置的元素类型为boolean

方括号运算符 ⚔️

前面提到的方括号运算符里面可以是对象的某个键名(其实也是一个值类型),但也可以是一个索引类型,这样的话最终返回的结果就不是单个类型了,而是该索引类型(就是键的类型)对应的所有的元素的类型组合而成的联合类型。

示例1interface Test {
    [p: string]: number
}
// number
type stringTypes = Test[string]

示例2type tuple = ['1', 1, true]
// true | "1" | 1
type allTypes = tuple[number]

第一个示例中的最终取得的类型是number,因为含有string类型的索引签名对应的属性类型就是number。

第二个示例中会得到元组中所有元素的类型组成的联合类型,因为其实元组的索引都是number类型的,所以可以一次全部取到所有元素的类型。

答案 📄

前面铺垫了那么多就是为了解这道题目的,先给出答案。

type TupleToObject<T extends readonly any[]> = {
  [P in T[number]]: P
}

解析

  • in操作符就不用讲了吧
  • T[number]的作用就是获取元组所有元素对应的类型,返回一个联合类型,那这刚好不就可以用in来遍历嘛,然后每遍历到的一个类型同时作为键和值即可。

4、实现 Exclude

原题链接:00043-easy-exclude,这道题看似有点摸不着头脑,但是掌握相关知识点就会变得很简单。

大致思路 🤔

这道题给我们两个联合类型TU(可以把联合类型看成是一个类型集合),求存在T中而不存在于U中的类型,从集合的角度来讲就是T - U,求差集。要求差集,先解决两个问题:1、如何判断T中的某个类型是否存在于U中。2、如何去除T中存在于U中的类型。

条件类型 ⚔️

条件类型可以根据类型输入来判断返回何种类型

示例1// 报错 Type '"message"' cannot be used to index type 'T'.
type MessageOf<T> = T["message"];
// 正确的做法,extends约束了T必须有一个message的属性
type MessageOf<T extends { message: unknown }> = T["message"];

示例2type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;

示例2,泛型T就是输入类型,先判断T是否满足{ message: unknown }的约束,如果T存在message属性,就返回message属性的类型,否则返回never

如果T是一个联合类型,就会出现分布式条件的情况

type ToArray<Type> = Type extends any ? Type[] : never;
type StrArrOrNumArr = ToArray<string | number>;  // string[] | number[]

// 等价于
type StrArrOrNumArr = (string extends any ? string[] : never)
| (number extends any ? number[] : never)

这两种情况是等价的,也就是说分布式条件会对联合类型中的每个类型都判断一次,并且运算的结果也是联合类型。那么我们可以利用这一点来判断T中的类型是否存在于U中,即T中的每个类型是否满足U的约束。

答案 📄

type MyExclude<T, U> = T extends U ? never : T

T中的类型存在于U中时,就返回never是为了剔除掉这个类型。举个例子再结合上面所讲的条件类型,应该会比较清晰了。

type excludeNever = string | never | number  // string | number

可以看到最终生成的联合类型是没有never的。

现在假设T='a' | 'b' | 'c'U='a',那么答案给出的代码就等价于

('a' extends 'a' ? never : 'a')
| ('b' extends 'a' ? never : 'b')
| ('c' extends 'a' ? never : 'c')

never | 'b' | 'c' => 'b' | 'c'

这样不就求出了T-U嘛。

总结

通过一些简单的类型题目,复习了一遍TS中基础的类型运算,刚开始做这些题目的时候还是有点吃力的,因为对这些东西并不熟悉,甚至有些点根本就不知道。要把TS学好,还是得多练啊。

文章中如果有不准确,或者错误的地方。大家可以在评论区勘误一下,thank you!🤞

原文链接:https://juejin.cn/post/7265996663406968844 作者:大田稻谷

(0)
上一篇 2023年8月11日 上午10:25
下一篇 2023年8月12日 上午10:05

相关推荐

发表回复

登录后才能评论