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

原文地址:How to implement a generic ValueOf<T> 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> = T    [keyof T];

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

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

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

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

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

// SHAPES 是 JS 对象
const circle: ValueOf<typeof SHAPES> = 'circle';
const rectangle: ValueOf<typeof SHAPES> = 'rectangle'; // err
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容