前言
本文假定读者对 TypeScript 有一定的了解,比如你得知道什么是交叉类型(intersection type)和联合类型(union type)及常用的工具类型(Utility Types)
有如下代码:
class Conf {
constructor(conf = { width: 800, height: 480, render: "canvas" }{}) {
this.conf = conf;
this.copyFromMultipleVal(conf, 'width', 'w', 800);
}
copyFromMultipleVal(conf, key, otherKey, defalutVal) {
this.conf[key] = conf[key] || conf[otherKey] || defalutVal;
}
}
现在需要你给上面的Conf修改为ts语言时,我第一次写的时候如下:
interface ConfOptions {
width?: number;
w?: number;
height?: number;
render?: string;
}
interface ConfOptionsTrue {
width: number;
height: number;
render: string;
}
class Conf {
conf: ConfOptionsTrue;
constructor(conf: ConfOptions = { width: 800, height: 480, render: "canvas" }) {
// @ts-ignore
this.conf = conf;
this.copyFromMultipleVal(conf, "width", "w", 800);
}
copyFromMultipleVal(conf, key, otherKey, defalutVal) {
this.conf[key] = conf[key] || conf[otherKey] || defalutVal;
}
}
问题
其实最上面的那段js代码,就是一个普通的类。但是在实例化的时候,为了兼容各种各样的神奇参数,不得不做的hack(这样的代码在修改旧代码的时候特别常见)。把他修改为TypeScript的,添加的类型并不准确,在别人使用你的这个类的时候,类型并不能准确的告诉他 width和w其实是一个属性,在代码里面若想设置width属性的值会有以下情况:
const c1 = new Conf({ width: 100, w: 200 });
const c2 = new Conf({ width: 100 });
const c3 = new Conf({ w: 200 });
其实我们写TS类型,并不想达到如下的效果。希望类型提示的更加准确些。
总结下来,若想设置width就是:(w、width)两个属性 有且仅有一个 不能为空。
解决
如果一个类型只有一个或者两个这样的参数,我们可以使用联合类型结合never进行屏蔽相对立的属性。
interface BaseConfOptions {
height?: number;
h?: number;
parallel?: number;
frames?: number;
clarity?: string;
renderClarity?: string;
}
type ConfOptions =
| (BaseConfOptions & { width?: number; w?: never })
| ({ w?: number; width?: never } & BaseConfOptions);
效果如下:
堪称完美,用TS的智能类型提示,又让我们的代码更加的精准。
有没有更优雅的写法呢?肯定是有的,先上答案:
type RequireOnlyOne<T, U extends keyof T = keyof T> = Omit<T, U> &
{
[K in U]-?: Required<Pick<T, K>> & Partial<Record<Exclude<U, K>, never>>;
}[U];
修改代码如下:
type ConfOptionsOnlyOne = RequireOnlyOne<ConfOptions, "width" | "w" >;
class Conf {
conf: ConfOptionsTrue;
constructor(conf: ConfOptionsOnlyOne = { width: 800, height: 480, render: "canvas" }) {
// @ts-ignore
this.conf = conf;
this.copyFromMultipleVal(conf, "width", "w", 800);
}
}
理解表达式
咋一看这个类型有点复杂,但是我们拿一个例子一步步的拆解,来理解。
type RequireOnlyOne<T, U extends keyof T = keyof T> = Omit<T, U> &
{
[K in U]-?: Required<Pick<T, K>> & Partial<Record<Exclude<U, K>, never>>;
}[U];
interface Square {
width: number;
height?: number;
h?: number;
}
type Test = RequireOnlyOne<Square, "height" | "h">;
- 先把最外面的泛型转换成真实,类型中的key
type One = { width: number } & {
height: Required<{ height?: number }> & Partial<{ h: never }>;
h: Required<{ h?: number }> & Partial<{ height: never }>;
}["height" | "h"];
- 把表达式中的 Required和Partial,转换下
type Two = { width: number } & {
height: { height: number; h?: never };
h: { h: number; height?: never };
}["height" | "h"];
- 最后再简化下,达到最终形态
type Three =
| { width: number; height: number; h?: never }
| { width: number; h: number; height?: never };
其它
与这个类型相类似的,还有一个RequireAtLeastOne类型,即:至少要 选择一个 属性
type RequireAtLeastOne<T, R extends keyof T = keyof T> = Omit<T, R> &
{
[K in R]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<R, K>>>;
}[R];
两个辅助类型的示例请参考这里
总结
在日常的开发过程中我们遇到TS类型的问题,尽可能的多思考下,怎么样优雅的处理问题,也可以多看一些辅助类型源码(utility-types),帮助我们更好的理解TS。也让TS更好的服务于我们的代码。否则,我们仅仅处于只能简单的看懂TS和简单的使用。没有理解到TS的精髓。