[译]如何在 Typescript 中实现通用的 Valueof 辅助泛型

原文地址:How to implement a generic ValueOf helper type in TypeScript.
原文作者:Stephan Meijer

第一次尝试翻译啦,先挑一篇简单的开始。


Sudhanshu 昨天在KCD Discord上问了一个关于 Typescrpit 有趣的问题:

是否可以将变量的类型限制为普通对象的值?

我提供了解决方案,但他想了解其中的原理,所以我就顺手写了这篇文章来与各位分享这点知识。

我们先从纯 Javascript 代码开始,看一个符合 Sudhanshu 要求的在运行时执行校验的例子

const SHAPES = {
  SQUARE: 'square',
  CIRCLE: 'circle',
};

const value = 'square';

// 校验 value 是否是 SHAPES 的 values 之一
const validValues = Object.values(SHAPES);
const isValid = validValues.includes(value);

if (!isValid) {
  throw new TypeError(
    `'value' should be one of: ${validValues.join(' | ')}`
  );
}

以上代码将会在 value 不等于 square 或 circle 时抛出错误。运行时校验挺好的,但问题是这些工作是否可以通过 Typescrpit 在静态阶段完成。

哈哈,这当然可以啦!

限制对象的值

我们面临的第一个挑战是,我们在操作一个对象(object)而不是一个类型(type)。

所以,我们应该先从对象提取出类型,那么就该使用 typeof

const SHAPES = {
  SQUARE: 'square',
  CIRCLE: 'circle',
};

type Shape = typeof SHAPES;

现在类型 Shape 就是

type Shape = { 
  SQUARE: string;
  CIRCLE: string;
}

但这还不是我想要的。

如果我们需要验证 value 是否包含在对象 SHAPES 的值(square | circle)中,我们当然需要验证。我们可以通过关键字as const将对象声明为常量,加上该申明后,我们向 Typescript 保证我们不会在运行时改变这一对象,Typescript 会将它视为一种类似于枚举类型的对象。

const SHAPES = {
  SQUARE: 'square',
  CIRCLE: 'circle',
} as const;

加了as const后,类型 Shape 就变为

type Shape = { 
  readonly SQUARE: 'square';
  readonly CIRCLE: 'circle'; 
}

解释一下这里发生了什么:

  • 首先,所有属性都被标记为只读readonly,此后再修改该对象都将会收到来自 typescript 的错误提示
  • 其次,现在各属性都被限制为它们对应的枚举值,而不是之前的 string 类型。

有了它之后,我们就得到了一个可以使用的类型了。Typescript 并没有 valueof 这样的辅助类型,但我们知道它有一个 keyof,看看它能不能帮到我们。

type keys = keyof Shape;

keyof 从以上类型 Shape 中获取它所有键的联合类型,所以类型 keys 就等同于:

type keys = 'SQUARE' | 'CIRCLE';

有了键,那么我们就能获取到值了。其实或许到这,你可能就已经知道该如何提取值并复用它了。

就比如,如果你想得到 SQUARE 的类型,那么你可以:

type Square = Shape['SQUARE']; // square

现在,你可以基于以上Square类型声明一个新的联合类型,大家应该会这么写:

type ValidShapes = Shape['SQUARE'] | Shape['CIRCLE']; // square | circle

当然,你也可以改为这样更简短的方式:

type ValidShapes = Shape['SQUARE' | 'CIRCLE']; // square | circle

我们来总结一下。以上我们使用keyof来获取反映Shpae键的联合类型,以及一个比较简捷的从值创建联合类型的方法。现在我们再回头看以上最后一个代码片段,你会发现索引参数不过是一个联合类型。其实,其实我们可以直接将keyof丢在里面。

汇总一下以上关键代码片段:

// 使用 as const 将对象声明为常量,因此 ts 将其识别为枚举类型
const SHAPES = {
  SQUARE: 'square',
  CIRCLE: 'circle',
} as const;

// 从对象 SHAPES 中创建一个类型
type Shape = typeof SHAPES;

// 从对象的键 (SQUARE | CIRCLE) 创建一个联合类型
type Shapes = keyof Shape;

// 从对象的值 (square | circle) 创建一个联合类型
type Values = Shape[Shapes];

然后我们就可以用类型 Values 来限定属性的类型了。

const shape: Values = 'circle';

当我们尝试给类型Values限定的变量赋除 square 或 circle 之外的任何值时,typescript 都将会抛出错误。

那么,既然完成任务,今天的文章就结束啦!

多提一句,虽然运行时校验真的很棒,但是现在我们已经不再需要它了,因为如果我们赋了一个不被支持的值时,甚至都不能通过编译 ♂️。

通用的valueof

其实上面的做法已经很够用了,但,如果能让它实现复用岂不是更棒?于是,我决定再多写两行,用上在 typescript 中比较高级的泛型。

我们重复下解决方案:

type Shape = typeof SHAPES;
type Shapes = keyof Shape;
type Values = Shape[Shapes];

让我们尝试将它转成泛型的实现。

第一步先将 type 收敛成一行,其实就是之前提到的将keyof丢在索引中。当然我们并不打算将typeof也内联 ,虽然加上并不难,但是就会变得很复杂,有机会我下次单独讨论。

type Values = Shape[keyof Shape];

类型Values其实与之前没差,用法也一样 const shape: Values = 'circle',现在看看泛型的写法:

type Values     = Shape[keyof Shape];
type ValueOf = T    [keyof T];

我加了一些空格做前后对比,所以其实应该能一目了然:

  • 首先,我们将类型变量 T 附加到类型上,这里的 T 是一种特殊的变量(不熟悉泛型的同学我多说一句,这里的 T 可以是任意的大写字母),它只适用于类型而不是值。
  • 其次,我们不再使用具体的类型Shape,而是直接使用该变量 T 作为参数。其实这里就是使用 T 替换了Shape

很简单,就这些了,这回真的写完了。你可以放心的将 Valueof添加到你的typescript项目工具类型声明中去了。

// e.g.: global.d.ts
type ValueOf = T[keyof T];

// Shape 是类型
const circle: ValueOf = 'circle';
const rectangle: ValueOf = 'rectangle'; // err

// SHAPES 是 JS 对象
const circle: ValueOf = 'circle';
const rectangle: ValueOf = 'rectangle'; // err

你可能感兴趣的:([译]如何在 Typescript 中实现通用的 Valueof 辅助泛型)