Open
Description
TypeScript 从数组元素的值中定义 Union Type
参考文章:
场景
本文处理下面这个 TypeScript 开发场景:
有一个数组,数组中每一项都是一个对象,我希望定义一个 Type,是由数组中的每个对象中的某个属性值组成的联合类型 (union type)
// 数组
const configs = [
{
name: 'width',
value: '60px',
},
{
name: 'height',
value: '40px',
}
];
我期望将 configs 中的每一项中的 name 的实际字符串值,提炼成一个 union type。
// 期望得到的 type
type ConfigName = 'width' | 'height';
最直接的做法是直接像上面一样手动定义一下。这样写的缺点是,如果 configs 中的元素又增加了,例如 name 的取值增加一个 'max-width' ,那我们需要更新 ConfigName 的的定义。无疑增加了维护成本。
使用 TypeScript 编程的原则之一:更少的类型维护,更安全的类型推断。
解决思路
首先,我们尝试用 typeof 来进行类型定义。
type ConfigName = typeof configs[number]['name'];
// 推断结果为
// type ConfigName = string;
// 期望:
// type ConfigName = "width" | "height";
这与我们实际期望不符,我们发现,被推断为了 string 类型。并不是我们期望的 union type
const 断言
使用 TypeScriptconst 断言
const 断言有以下两个作用:
- 表达式中任何字面类型都不应被扩展(例如,不能从 'hello' 变成 string)
- 对象和数组字面量获取 readonly 属性。
根据第 1 条规则,看上去好像可以恰好解决我们的问题,试一试:
// 对象断言为 const
const configs = [
{
name: 'width',
value: '60px',
},
{
name: 'height',
value: '40px',
}
] as const;
type ConfigName = typeof configs[number]['name'];
// 推断为:
// type ConfigName = "width" | "height";
// 期望:
// type ConfigName = "width" | "height";
缺点:推断的属性会被赋予 readonly
不可变属性
泛型函数 Generic Functions
在 TypeScript 中,我们想要描述两个值之间的对应关系时,会使用泛型。
type ConfigItem<T> = {
name: T;
value: string;
}
function defineConfigs<T extends string>(configs: Array<ConfigItem<T>>) {
return configs;
}
const configs = defineConfigs([
{
name: 'width',
value: '60px',
},
{
name: 'height',
value: '40px',
}
]);
type ConfigName = typeof configs[number]['name'];
// 推断为:
// type ConfigName = "width" | "height";
// 期望:
// type ConfigName = "width" | "height";
原理:我们使用泛型函数在函数的输入和输出之间创建了一个链接,当调用它时,会根据其输入的具体值,得到一个具体的类型。
总结
至此,我们达到了最初的目的:
- 定义类型来约束值。应用于编写 configs 数组,数组的每个元素时一个对象,这个对象需要定义一个类型 ConfigItem,这样便于编写每个 config 的值。
- 通过值来推断类型。应用于获取实际的 configs 数组中的某个 value 的所有取值情况,这样不需要在 ConfigItem 中定义成 union type 了。减少定义 ConfigItem 里的 name 为 union type 的成本。