Derive Union Type from Tuple/Array

TypeScript

在 TypeScript 中,如果希望一个变量只能取某几个固定值中的一个,可以这么写:

type Type = 'a' | 'b';
const a: Type = 'a'; // ✔
const c: Type = 'c'; // ✖

然而,在实际的开发过程中,可能会遇到这样的需求:希望 TypeScript 可以限定某一个类型只能取某几个固定的值,同时这几个值又可以组成一个数组,方便 JavaScript 在运行时动态的执行匹配功能(如 Array.prototype.some)。

如果直接尝试在 TypeScript 中写数组,实际无法达到预想的效果:

const list = ['a', 'b'];
type Type = list[number]; // Type = string

这是因为,TypeScript 默认 list 的类型是 string[],而不是 ('a' | 'b')[]。因此,在转化成 Type 的时候,得到的结果是更宽泛的字符串类型,而不是限定死的两个固定值。这其中,一个很重要的原因是 JavaScript 语言的动态性。数组随时可以被加入/删除元素,因而默认只能假设这是一个字符串类型的数组,而不能过多约束。

为了达到目的,有以下几个变通的写法:

const list: ['a', 'b'] = ['a', 'b'];
type Type = list[number]; // Type = 'a' | 'b';

这种写法比较啰嗦,重新写了一遍完整的数组用于定死类型的选择范围。

也可以通过写一个辅助函数来达到类似的效果:

declare const tupleStr: <T extends string[]>(...args: T) => T;
const list = tupleStr('a', 'b');
type Type = list[number]; // Type = 'a' | 'b';

在 Ant Design 中可以找到类似的写法。这里也有一个类似的 gist

注:上述这种写法需要 TypeScript 3.0 的支持

当然,上述的方案或多或少都需要额外写一些东西,有些麻烦。在 TypeScript 3.4 中,可以通过 as const 这个语法来告知 TypeScript 数组是静态的、并不会增加或者减少内容。有了这样的前提假设,TypeScript 就可以更好的进行类型推导,把实际的类型结果限制到已知的几个有限的值范围内。例子如下:

const list1 = ['a', 'b'] as const;
const list2 = <const> ['a', 'b'];
type Type1 = list1[number];
type Type2 = list2[number];

上述两种写法是等价的(参考这里),都可以达到目的。另外,由于在 TypeScript 中限制了数组,之后想要在数组中做改动都是会导致编译器报错的。

参考