TypeScript 的类型是非常复杂的,虽然日常大家经常用到的无非是那几个简单的 type interface 之类的,但是你打开某些知名的 js lib 的源码,有时候会发现一些非常复杂的类型定义,今天简单分享个我之前写 colors-web 的时候定义的一个类型,其实很简单。
首先,我们看下需求:
现有一个 class,他的方法是运行时动态赋予的,可能会有一百多个方法
如 Color 类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
class Color {
styles = [];
constructor() {}
color(colorValue} {
this.styles.push('color:'+colorValue);
return this;
}
bgColor(colorValue) {
this.styles.push('background-color:'+colorValue);
return this;
}
}
const cssColors = ['red','green','yello'...] // 这里定义所有的 css 标准颜色名称,供146个
// 将 colors 变成 Color 的实例方法
const ColorFactory = ()=> {
const instance = new Color();
cssColors.forEach((color) => {
Object.defineProperty(inst, color, {
get() {
this.styles.push(`color:${color};`);
return this;
},
});
Object.defineProperty(inst, color + "Bg", {
get() {
this.styles.push(`background:${color};`);
return this;
},
});
});
}
|
需求是通过 d.ts 定义以上方法的类型。
实现
本文只提供一种实现思路,实际上这个需求应该还可以有其他实现方式,只是我对 ts 的理解还没有太深入
基本类型
首先,我们先把 Color 类本身的类型定义好。
1. 定义好 cssColors 类型,Color 类的参数和后续的方法都依赖这个基本的类型定义
1
2
3
4
5
6
7
8
|
type CSSColors = | "black" | "silver" | "gray" | "white" | ...; // css color 共有 146 个名字
type HexColor = `#${string}`;
type RGBColor = `rgb(${number},${number},${number})`;
type RGBAColor = `rgba(${number},${number},${number},${number})`;
type hslColor = `hsl(${number},${number}%,${number}%)`;
type hslaColor = `hsla(${number},${number}%,${number}%,${number})`;
type colorValue = hexColor | rgbColor | rgbaColor | hslColor | hslaColor;
|
这里没有定义的非常精确,特别是 HexColor,实际上 ts 里也可以精确定义,只是比较复杂,这里有一个关于用正则表达式定义字面量类型的讨论,其中有一个方案,可以把正则转成类型体操
https://github.com/Microsoft/TypeScript/issues/6579#issuecomment-711022216
另外这里定义用到的是 模板字符串字面量类型,是 TS 4.1 新增的特性。
2. 定义 Color 类的类型
1
2
3
4
|
interface Colors {
color: (color: ColorValue) => Colors;
bg: (color: ColorValue) => Colors;
}
|
3. 将 CSSColors 附加到 Colors 中
1
2
3
4
5
6
7
8
9
10
11
12
|
// 首先定义一个 ColorBase,他的属性是由146个css颜色的字符串字面量定义的
// 通过 in 关键字遍历出来
interface ColorBase {
[key in CSSColors]: ((o?: string) => Colors) & Colors;
}
// 然后让 Colors 继承 ColorBase 即可拥有这些方法
interface Colors extends ColorBase {
}
const color = new Colors();
color.red(); // pass
|
其实就是一个 in 和 extends 关键字的使用,不过这里还有一个比较有意思的点,就是我这个类的属性可以有两种调用方式:
1
2
3
4
5
|
const color = new Colors();
color.red().green();
// 也可以这样
color.red.green.log();
|
在 js 中,通过 Object.defineProperty 给 red 这样的属性定义 getter ,即可在里面塞入逻辑实现下面第二种调用方式。
对应到 ts 中,就是刚才的定义:
1
|
red: ((o?: string) => Colors) & Colors;
|
其中第一个类型是一个正常的返回当前实例的调用,第二个类型是刚才hack的getter调用,属性本身的类型就是类本身。
不得不承认,ts 里的特性是很多的,很多时候写出一个可以正常 run 的类型甚至都有一定的运气成分,本文只是一个小分享,希望对同学们能够有帮助。
本文提到的项目的 github 地址:https://github.com/yu-tou/colors-web
类型定义见:https://github.com/yu-tou/colors-web/blob/master/src/index.d.ts