TS 之 Union, Enum, Tuple 运用自如指南
前言
学习TypeScript有两个难点。一个是了解ts的语法。目前ts的语法特性很多,一些看起来复杂的问题,如果你了解了语法就会发现其实很简单,你只是不知道解决问题的对应语法特性而已。第二是了解类型特性。ts中的enum、union、tuple等类型提供了不同角度的类型声明方式。这些类型中有些是彼此之间相似程度比较高,有些也可以相互转化。了解这些类型越全面,对我们解决typescript中一些难题很有帮助。
TypeScript Basic Types
Tuple和Enum属于TypeScriptBasic Types中的一个。因为特性比较特殊,因此值得深入了解。
-
Boolean
-
Number
-
String
-
Array
-
Tuple
-
Enum
-
Unknown
-
Any
-
Void
-
Null and Undefined
-
Never
-
Object
Tuple
定义元组的方式很简单。
// Declare a tuple type
let x: [string, number];
// Initialize it
x = ["hello", 10]; // OK
// Initialize it incorrectly
x = [10, "hello"]; // Error
type NestedTuple = [string, Array<number>, [boolean, {name: string}]]
// 可以定义复杂的tuple
Tuple -> Union
Tuple转成Union很多见,简单写就是
type Tuple = [string, number, boolean]
type Union = Tuple[number] // string | number | boolean
抽成utility type则会是
type Tuple2Union<Tuple extends readonly unknown[]> = Tuple[number]
type Tuple = [string, number, boolean]
type Union = Tuple2Union<Tuple>
反过来 Union到Tuple基本上不会用到,所以忽略。
转换的具体场景比较多,一般在迭代的时候会用到这个技巧。 例如:我们想要通过tuple定义的类型map出一个对象。
type Tuple = ["blue", "yellow", "red"]
// 想要转换出{ blue: string, yellow: string, red: string }的一个结构体
type MappedTuple = {
[k in Tuple[number]]: string
}
题外话 始终注意type中extends的对象。要分清楚哪个extends哪个
type Includes<T extends readonly any[], P> = P extends T[number] ? true: false
// correct!
type Includes<T extends readonly any[], P> = T[number] extends P ? true: false
// incorrect!
type isPillarMen = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Wamuu'> // expected to be `false`
Enum
枚举是一种为一组数值提供更友好名称的方法。
enum Color {
Red = 1,
Green,
Blue,
}
let c: Color = Color.Green;
enum的key得是string,其value要么是string,要么是number 这两个有一些区别,分开讨论。
1.数字枚举
enum Color {
Red = 1,
Green,
Blue,
}
2.字符串枚举
enum CardinalDirection {
North = 'N',
East = 'E',
South = 'S',
West = 'W',
}
3.混合枚举
enum BooleanLikeHeterogeneousEnum {
No = 0,
Yes = "YES",
}
简而言之不要这么用。
特性1 不仅是type,还可以作为值来用
enum Color {
Red = 1,
Green,
Blue,
}
上面👆定义的Color枚举 compile过后值为
{
1: "Red",
2: "Green",
3: "Blue",
Blue: 3,
Green: 2,
Red: 1,
}
let colorName: string = Color[2];
console.log(colorName); // 'Green'
enum CardinalDirection {
North = 'N',
East = 'E',
South = 'S',
West = 'W',
}
上面👆定义的CardinalDirection枚举 compile过后值为
{
East: "E",
North: "N",
South: "S",
West: "W",
}
注意数字枚举和字符串枚举最终值是不一样的。
enum完全可以作为object来用!
特性2 loose type-checking
数字枚举 会有loose type-checking的问题。举个例子
const color1: Color = 4 // Ok
const color2: Color.Red = 5 // Ok
const color3: Color ='6' // Error
我们期待的是上面三个表达式都会报错,但实际情况是只有最后一个会报type error错误。 造成这个问题的原因见 这里 。因此建议在使用enum的时候 尽量使用字符串枚举或者是坚决避免写如下的代码。
enum Color {
Red = 1,
Green,
Blue,
}
const value1: Color = 3
// 不要这么写!
const value2: Color = Color.Blue
// 正确写法
function foo(arg: Color) {
if (arg === 1) {
// 不要这么写!
}
if (arg === Color.Red) {
// 正确写法
}
}
不过有意思的是当我们需要通过Color枚举创建一个ColorMap的时候,type-checking是正常的。
const ColorMap: {
[key in Color]: string;
} = {
1: 'red color',
2: 'green color',
3: 'blue color',
4: 'x' // Error!
}
这里下标为4时typescript会报错。
const enum
和 declare const
可以使用 const enum
的方式创建枚举。
const enum Direction {
Up,
Down,
Left,
Right,
}
let directions = [
Direction.Up,
Direction.Down,
Direction.Left,
Direction.Right,
];
// let directions = [
// 0 /* Up */,
// 1 /* Down */,
// 2 /* Left */,
// 3 /* Right */,
// ];
const value = Direction[Direction.Up] // 报错!
编译过后的版本如上,和不加const的区别是加了const编译过后Direction枚举不会作为值存在。所有以值的方式使用的地方都会转成对应的枚举值。这时候不能再将枚举当作object来使用了。
declare enum
的方式定义的枚举不能作为值来使用。
declare enum Direction {
Up,
Down,
Left,
Right,
}
let directions = [
Direction.Up,
Direction.Down,
Direction.Left,
Direction.Right,
];
上面的这种方式,typescript虽然不会显式的报错,但是typescript编译会失败。简而言之也不要这么使用枚举。
Enum -> Union
由于Enum有key,value两套东西,这里分别做个介绍。
EnumKey组成Union
经常有需要获取enum的key的union的场景。enum由于是有值的,因此转Union的过程和将对象的key转成union的场景一致。
enum CardinalDirection {
North = 'N',
East = 'E',
South = 'S',
West = 'W',
}
const DirectionObj = {
North: 'N',
East: 'E',
South: 'S',
West: 'W',
}
type Type1 = keyof typeof CardinalDirection // "North" | "East" | "South" | "West"
type Type2 = keyof typeof DirectionObj // "North" | "East" | "South" | "West"
数字枚举也一样,转的过程中不会出现数字枚举
enum Direction {
Up,
Down,
Left,
Right,
}
type Type = keyof typeof Direction // "Up" | "Down" | "Left" | "Right"
// 不会出现 0 | 1 | 2 | 3 | "Up" | "Down" | "Left" | "Right"
// 至于为啥会觉得可能出现这种type,你可以再往上看看数字枚举和字符串枚举的区别
EnumValue 组成Union
如何通过下面这个枚举获得 "N" | "E" | "S" | "W"这Union呢?
enum CardinalDirection {
North = 'N',
East = 'E',
South = 'S',
West = 'W',
}
方法也是有的,使用typescript的模版字符串。
type ValueUnion = `${CardinalDirection}`
// "N" | "E" | "S" | "W"
Union
union也很好理解,就是多个类型的“或”
function printId(id: number | string) {
console.log("Your ID is: " + id);
}
// OK
printId(101);
// OK
printId("202");
// Error
printId({ myID: 22342 });
type UnionType = number | {} | string | '123' | 2312
// number | {} | string
下面定义的两个type有很大的区别。
type Union = Array<string | number> // (string | number)[]
type Tuple = [string, number]
在这里元组长度是固定的,为2且第一个元素是string,第二个元素是number。 但是Union类型不限制长度。并且每个元素都既可能是string又可能是number。两个表达的含义就不一样了。
Union在conditional types中使用
经常会以 xxx extends UnionType的形式出现,ts会帮我们做是否包含的判断。例如
type Nullish = null | undefined
type isNullish<T> = T extends Nullish ? true : false
type isNull = isNullish<null> // true
type isNull2 = isNullish<number> // false
// 我们可以再玩一些有意思的
type isNull3 = isNullish<null|undefined> // 这里依然是true
Union在mappd types中使用
Union经常会在mapped types中会用到。 首先下面的P的类型为"x" | "y"
type Point = { x: number; y: number };
type P = keyof Point; // "x" | "y"
type Union = "x" | "y"
type Obj = {
[k in Union]: string
} // { x: string; y: string };
因此下面五种type map之后都是一样的
type Union = "x" | "y"
type Point = { x: number; y: number };
type Tuple = ["x", "y"]
enum EnumMapKey {
x,
y,
}
enum EnumMapValue {
First = 'x',
Second = 'y',
}
// 第一种
type Obj1 = {
[k in Union]: string
}
// 第二种
type Obj2 = {
[k in keyof Point]: string
}
// 第三种
type Obj3 = {
[k in Tuple[number]]: string
}
// 第四种
type Obj4 = {
[k in keyof typeof EnumMapKey]: string
}
// 第五种
type Obj5 = {
[k in `${EnumMapValue}`]: string
}
或者是
type Obj5 = {
[k in EnumMapValue]: string
}
// 效果是一样的。
Union在Template literal types中的使用
除了上面在将枚举中的value组成Union时用到Template literal types之外,它还可以和Union结合发挥强大力量。
type Digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
type ThreeDigits = `${Digit}${Digit}${Digit}`;
// "000" | "001" | "002" | "003" | "004" | "005" | "006" | "007" | "008" | "009" | "010" | "011" | "012" | "013" | "014" | "015" | "016" | "017" | "018" | "019" | "020" | "021" | "022" | ... 976 more ... | "999"
我们可以实现Union的排列组合了。不过值得注意的是转换之后的类型就转为了string,不再是number了。 如果你执行如下的操作,你会得到一个tserror。
type UniType = string | number | boolean | [number]
type Template = `${UniType}`
// Type 'UniType' is not assignable to type 'string | number | bigint | boolean | null | undefined'.
Union的成员必须得是简单类型。
小测试
假设我们有一个函数foo,接受两个参数,但是这个函数比较特殊,它要么接受一个string类型以及一个{name: string}类型,要么接受两个number类型的参数。那么如何定义这个函数类型。
foo('xx', {name: 'hello'}) // correct
foo(3232, 232) // correct
foo('xx', 123) // error!
一共有两种方案,答案如下
function foo(...args: [string, {name: string}] | [number, number]) {
}
foo('xx', {name: 'hello'})
foo(3232, 232)
foo('xx', 123) // error!
function bar(arg1: string, arg2: {name: string}): void
function bar(arg1: number, arg2: number): void
function bar(arg1: string | number, arg2: {name: string} | number) {
}
bar('xx', {name: 'hello'}) // correct
bar(3232, 232) // correct
bar('xx', 123) // error!
// 很明显第一种简单。
转载自:https://juejin.cn/post/7030025556519305223