记录写一个 TS 类型的思考优化过程

某一天,一个朋友忽然在微信里问我,“['TYPE'] => { 'TYPE': 'TYPE' },写个函数转换这个,类型怎么加?”。笔者立刻的回复就是,“用 reduce 将数组转对象,TS 类型写个泛型便可”。html

说是这么说,可是发如今写 TS 类型上实际上却的确不是那么容易,因而,笔者立刻偷偷开始尝试。(推荐你们能够在 TypeScript Playground 上练习 TS 类型)typescript

首先转换从数组转换到对象,咱们用 reduce 很容易达成目标:数组

function convert(source) {
  return source.reduce((memo, key) => {
    return { 
      ...memo,
      [key]: key
    }
  }, {});
}
复制代码

但本文的重点是写这个函数的类型。简而言之,就是须要写 convert 函数参数的类型和返回值类型,从而能够定义函数的形状。微信

因而,笔者折腾了一会,写出来下述类型,编辑器

function convert<K extends string>(source: K[]): Record<K, K> {
  return source.reduce((memo, key) => {
    return { 
      ...memo,
      [key]: key
    }
  }, {} as any);
}
type TNAME = 'LIN' | 'HUA';

const peoples: TNAME[] = ['LIN', 'HUA'];
const peopleMap = convert<TNAME>(peoples);
function getNameFromPeopleMap<T extends any>(name: T): T {
  return peopleMap[name];
}

const t = getNameFromPeopleMap('HUA');
复制代码


新增一个 getNameFromPeopleMap 函数,编写泛型 (name: T): T。从而达成了如下效果:
函数

image.png


发给朋友,可是立刻被吐槽了,首先 LIN、HUA 两个值就在代码上被重复了两次,TNAME 类型略显冗余。其次,上述类型只是定义了函数传参值和返回值相同,并无达到预期中约束传参值的效果。

因此传入任意值,编辑器都不会提示类型错误。
优化

image.png

所以,上述类型若是打个分,连及格线都不达。因而,进行考虑优化。为了解决上述两个问题,一段时间后,新的类型火热出炉!ui

function convert<K extends string>(source: readonly K[]): Record<K, K> {
  return source.reduce((memo, key) => {
    return { 
      ...memo,
      [key]: key
    }
  }, {} as any);
}

const peoples = ['LIN', 'HUA'] as const;
const peopleMap = convert<typeof peoples[number]>(peoples);
function getNameFromPeopleMap<T extends typeof peoples[number]>(name: T): T {
  return peopleMap[name] as any;
}

const t = getNameFromPeopleMap('LIN');
复制代码

为了减小冗余的 TNAME。使用了 TypeScript 3.4 版本新增的 const 断言。它能够将咱们一个字面量表达式断言为一个 TS 类型。以下:spa

// Type '"HELLO"'
const STR = "HELLO" as const;

// Type 'readonly [10, 20]'
const POINT = [10, 20] as const;

// Type '{ readonly text: "hello" }'
const PAYLOAD = { text: "hello" } as const;
复制代码

所以,无需 TNAME[],咱们的 peoples 拥有了 TS 类型。
3d

image.png

同时,修改 getNameFromPeopleMap 的函数泛型声明为 <T extends typeof peoples[number]>(name: T): T。咱们便可约束函数传入值只能为 peoples 的子项。

可能有些同窗比较困惑,上述泛型是什么意思。笔者这里稍微解释一下。

image.png

其实 typeof peoples[number] 真正能够理解为,先执行 typeof peoples,便可得出 readonly ["LIN", "HUA"] 类型。而后 [number] 能够理解为,从 readonly ["LIN", "HUA"] 中使用 number 类型做为 key 将类型转换为字符串字面量类型 "LIN" | "HUA"。

那么 T extends 即表明着进行泛型约束。所以咱们就能够成功对函数传参进行类型限制。

image.png

瞧一瞧,多么成功。当我自信的把这段代码发给朋友时,又遭到了无情吐槽,“我想要的是 peopleMap 直接使用”

的确,笔者反思了一下,为何须要莫名其妙多一个函数去取值呢?还不是由于没法从 convert 函数上完成传参值和返回值的相等的类型限制嘛。

嗯,若是能从 convert 函数上直接支持就行了。

因而笔者将目光挪到了可疑的 convert 函数类型 (source: readonly K[]): Record<K, K> 上。首先从 Record<K, K> 上看,已经没法约束类型了。按照需求,返回值应该是 key 与 value 相等的对象类型。怎么写呢?笔者查阅了 TS 文档后,得出一种写法:

type IRecord<K extends keyof any> = {
  [P in K]: P;
}
复制代码

可能你们好奇 keyof any 是什么?其实它刚恰好就是对象的 key 类型。由于 TS 限制对象 key 类型只能为 string、number、symbol。

image.png

使用效果以下:

image.png

所以最终能够干掉以前多余的函数,得出以下类型:

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

function convert<K extends string>(source: readonly K[]): IRecord<K> {
  return source.reduce((memo, key) => {
    return { 
      ...memo,
      [key]: key
    }
  }, {} as any);
}

const peoples = ['LIN', 'HUA'] as const;
const peopleMap = convert<typeof peoples[number]>(peoples);
const t = peopleMap.LIN;
复制代码

同时也达到了对 peopleMap 的取值类型限制。

image.png


将代码发给了朋友,终于获得了满意。可是他仍是但愿能够简化一下。

好叭,那简化一下!

function convert<K extends keyof any>(source: readonly K[]): { [P in K]: P }
{
  return source.reduce((memo, key) => {
    return { 
      ...memo,
      [key]: key
    }
  }, {} as any);
}

const peoples = ['LIN', 'HUA'] as const;
const peopleMap = convert(peoples);
const t = peopleMap.LIN;
复制代码

简简单单的一个 TS 类型的需求,发现编写起来仍是挺有意思的。第一,做为笔记;第二,也但愿能够帮助到有须要的同窗们。

谢谢你们~若有助益,不胜荣幸!

相关文章
相关标签/搜索