TypeScript技巧之多个属性

490 阅读3分钟

前言

本文假定读者对 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的,添加的类型并不准确,在别人使用你的这个类的时候,类型并不能准确的告诉他 widthw其实是一个属性,在代码里面若想设置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">;
  1. 先把最外面的泛型转换成真实,类型中的key
type One = { width: number } & {
  height: Required<{ height?: number }> & Partial<{ h: never }>;
  h: Required<{ h?: number }> & Partial<{ height: never }>;
}["height" | "h"];
  1. 把表达式中的 Required和Partial,转换下
type Two = { width: number } & {
  height: { height: number; h?: never };
  h: { h: number; height?: never };
}["height" | "h"];
  1. 最后再简化下,达到最终形态
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的精髓。

参考