Typescript类型如何跟js的语法做映射,小小探究

883 阅读7分钟

前言

之前看了很多typescript大神的文章,不知道大家有没有跟我一样的感觉,总是感觉他们总结的吧,总是缺点啥,但是又不知道是什么,后来我在github上找到一个叫type-challenges,我简单看了一下简单题和中等题,发现很多不就是当初面试的面试题吗。。。

惊喜之余,慢慢总结出了一些规律,发现自己一些觉得缺的部分很简单,就是如何把类型和平时的编程语法做转换。

举一个例子,泛型最基本的作用是什么,不就是像函数一样传入参数吗,

type demo<T> = T;

类比:

function demo(T) {
    return T
}

好了,是不是我们把遇到的ts训练题通过这么一转化,然后总结一些从ts到js转化的基本套路,是不是typescript就变得简单一些了呢?

联合类型是对象类型的爸爸

对象类型我们指的是:

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

// 比如:
const person:Record<string, any> =  {
    name: 'jack',
    age: 18
}

然后第一个类比来了,首先这里我把联合类型看成了数组。

type U = string | number;

// 类比
const U = [string, number]

既然是数组,那么我们就可以遍历它。

那么如何遍历数组呢,目前总结了两个常用的方式

  • 跟对象类型有关的话,通过in关键字
  • 跟条件判断有关的话,通过extends关键字

首先我们看跟对象类型有关的用法,用一个常见的Pick类型来做解释。这个类型案例如下:

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

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

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

Pick类型实现如下:

type Pick<T extends keyof Record<string, any>, K extends keyof T> = { 
    [key in K] : T[key] 
};

K extends keyof T,是不是就得到了T的所有key值的联合类型,比如

type Todo =  {
  title: string
  description: string
  completed: boolean
}

keyof Todo // 'title' | 'description' | 'completed'

key in K 就可以看做遍历联合类型,类比js代码

const result = {}
// K是联合类型所以类比为数组
// key指的是K[i]意思是数组里的每一项
for(let i = 0; i < K.length; i++){
    result[K[i]] = T[K[i]];
}

所以Pick类型转化为js:

function Pick(T, K: keyof T) {
    let result = {};
    for(let i = 0; i < K.length; i++){
        result[K[i]] = T[K[i]];
    }
    return result;
}

结论一

我们要操作对象类型,那么必须借助遍历联合类型,也就是你工作中遇到写对象类型的ts类型,必须想到联合类型。

extends映射带有条件判断的for循环

好了,接着看extends上的for循环语法:

Exclude是TS内部的一个工具类型,使用效果如下:

案例:

type Result = Exclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'

实现如下:

type Exclude<T, K extends T> = T extends K ? never : T

联合类型 extends 的时候,联合类型会被拆散,单独判断,啥意思呢,请看下面。

Exclude<'a'|'b'|'c', 'c'|'b'> 可以转化为如下

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

'a' | never | never这一段类型为啥变为'a',稍微解释一下,不过任何类型联合上 never 类型,还是原来的类型。

转化为js

function Exclude(T, K: T){
    let result = [];
    for(let i = 0; i < T.length; i++){
        if(T.includes[K[i]]){
            result.push(K[i])
        } else {
            result.push(never)
        }
        
    }
    return result;
}

结论二

  • extends 语法实际上就是for循环里带了一个if判断(或者说三元运算符的判断)
  • 联合类型配合extends,可以把联合类型打散,分别进入for循环,然后还被extends语法的if判断返回不同的值

结论运用

好了,接着我们利用上面两种机制,做一个题,实现Omit,Omit类型是个啥效果呢,请看案例:

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

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

const todo: TodoPreview = {
  completed: false,
}

Omit: 删除,删除给出的key,我们说了联合类型就是数组,所以在我们这里可以认为删除'description' | 'title'就是删除['description', 'title']

我们分析一下啊,既然要删除对象类型上的某些属性,是不是我们要遍历一下这个对象。

好了遍历对象,必须要借助in来遍历联合类型了。

然后,我们要删除某些key,问题来了,兄弟们,TS里面没有这种语法,比如key为never就删除整个值,如果可以的话,我们可以这么写

type Omit<T, K extends keyof T> = { [key in T extends K ? never : key] : T[key] };

结论三

所以这里记个笔记,就是对象类型不支持在key里面直接删除key,所以我们只能换个思路,不能删除意味着只能遍历,只能看不能删(值是可以删的,比如[key in T] : key extends K ? never : key),never可以删值。

那咋办,我们换个思路,我们如果直接遍历的就是我们想要的结果的联合类型,然后在for循环这个联合类型,是不是也可以。

好了,第一步,先把我们想遍历的数组也就是联合类型求出,

type K<keyof M, P> = M extends P ? nerver : M

上面这段代码不就是Exclude类型吗,所以我们改一下:

Exclude<keyof M, P>

这里很重要的知识点,对象不能够删除属性,也就是key,但是联合类型可以删除,可以增加,所以对于一个对象增加和删除属性,你就要想到怎么用联合类型去转换。

接下里看Omit的实现

type Pick<T, K extends keyof T> = { [key in K] : T[key] };
type Exclude<T, K extends T> = T extends K ? never : T
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>

其实Omit转换为js就是

function Omit(T, K:T) {
    Pick(T, Exclude(keyof T, K: keyof T))
}

结论四

对象类型的增加属性和删除属性都是借助联合类型去实现的

实战中等难度的题

我们再看一个案例:

type foo = {
  name: string;
  age: string;
}
type coo = {
  age: number;
  sex: string
}

type Result = Merge<foo,coo>; // expected to be {name: string, age: number, sex: string}

一个merge运算符,它实际上就是把coo里的属性拿出来,然后再把foo里独有的属性拿出来做了一个合并。

我们来分析一下这个题,首先属性的merge,根据我上面的经验,一般对象的类型好像没啥特殊的功能,很多都要靠着联合类型的遍历和联合类型的类型判断去拓展对象的,好吧,这里我就往那个方向想了。

并且,我们新手初期,都要往这个方向想,联合类型是对象类型拓展的爸爸。

所以merge对象,我就可以看做merge联合类型。

再举个例子,还是用上面的案例,

type A = 'name' | 'age';
type B = 'age' | 'sex';
// 联合,求出了交集
type C = A & B  // 'age'
type D = A | B // ‘name’ | 'age' | 'sex'

兄弟们,有没有啥想法了,联合类型可以轻易求出交集和并集,那么加上之前in可以遍历联合类型,这就意味着,我们实现merge的思路变为:

type foo = {
  name: string;
  age: string;
}
type coo = {
  age: number;
  sex: string
}

type Result = foo独有的属性对象 & foo和coo交集(属性的值以coo为准) & coo独有的属性对象

所以我们第一步,求出foo独有的属性对象,独有是啥意思,就是 某类型<typeof foo, typeof coo> 能够得到

{
 name: string
}

又因为对象的筛选全靠联合类型,所以我们需要通过联合类型的筛选功能。

这个一下子让我想到了extends语法

type A = keyof foo; // 'name' | 'age'
type B = keyof coo // 'age' | 'sex'
type C = A extends B ? A : never;

是不是上面的C就获取到了foo的独有属性了。

这不是Exclude类型吗,用起来

type C = Exclude<keyof foo, keyof coo>

所以foo独有的属性对象为

type PreUnique = {
    [key in Exclude<keyof foo, keyof coo>]: foo<key>
};

同理coo独有属性对象为

type EndUnique = {
    [key in Exclude<keyof coo, keyof coo>]: coo<key>
};

上面已经知道交集的办法了,所以我们写下来:

type InterSection = {
  [p in keyof foo & keyof coo]: coo[p];
}

获取两个对象属性交集的联合类型:

type InterSectionKey = keyof F & keyof S;

获取两个对象的交集, 并且类型以第二个为准

获取两个对象中前一个对象独有的属性的联合类型

type PreUniqueKey<F, S> = keyof Omit<F, keyof S>;

获取两个对象中前一个对象独有的属性的对象

type EndUnique<F, S> = {
    [key in keyof Omit<F, keyof S>]: F<key>
};

所以这个merge函数就出来了

type Merge<F, S> = {
  [p in Exclude<keyof F, keyof S>]:F[p];
} & {
  [p in keyof F & keyof S]:S[p];
} & {
  [p in Exclude<keyof S, keyof F>]:S[p];
} 

是不是很简单,我们继续总结一下:

  • 两个对象类型的交集,差集,在typescript无法依靠对象本身去操作啥,只有靠联合类型的交集和差集,来帮助对象类型实现交集和差集

立竿见影的效果你马上看到(中等题)

diff函数

求两个对象的差集

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

type Result1 = Diff<Foo,Bar> // { b: number, c: boolean }
type Result2 = Diff<Bar,Foo> // { b: number, c: boolean }

这道题的链接:github.com/type-challe…

有兴趣的同学点击接受挑战就可以自己尝试解决一下了。

大家按照我之前的思路来做一下这个题呗,是中等难度。

思路:

对象类型的处理其实就是联合类型的处理,这里求差集,我们转为求联合类型中Bar独有的部分,这个之前是不是已经实现了,你自己不妨试试。

继续立竿见影

你不妨试着挑战这些题,应该很快就能做出来!

简单题 实现 Readonly

github.com/type-challe…

中等题 实现 Readonly 2

github.com/type-challe…

好了本文结束,后面接着做题继续总结ts转js的规律