# TS引入的新类型

# 元组 tuple

元组可以看做是数组的拓展,它表示已知元素数量和类型的数组。

应用场景:数组长度和类型都固定的情况下,可以使用元组进行管理!

const teacherInfo: [string, string, number] = ['zws', 'jon', 18]

const teacherList: [string, string, number][] = [
  ['zws', 'jon', 18],
  ['sun', 'dea', 22],
  ['za2', 'wall', 26]
]

let tupleDemo: [string, number, boolean];
tupleDemo = ["a", 2, false];

// error 不能将类型“number”分配给类型“string”。 不能将类型“string”分配给类型“number”。
tupleDemo = [2, "a", false]; 

// Type '[string, number]' is not assignable to type '[string, number, boolean]'. Source has 2 element(s) but target requires 3.(2322)
tupleDemo = ["a", 2]; 

let tuple: [string, number] = ['123', 123];
// 等价于
interface TupleType extends Array<number | string> {
  0: string;
  1: number;
  length: 2;
}

let tuple1: TupleType = ['123', 123]

# Enum 枚举

你可以使用常量const描述某种不会改变的值,但某些值是在一定范围内的一系列常量,如星期(周一~周日)、三原色(红黄蓝)、方位(东南西北)等,这种类型的值称为枚举

枚举成员默认为数值类型

# 数值枚举

使用关键字 enum 来声明一个枚举类型数据。

enum Direction {
  Up,
  Down,
  Left,
  Right
}

console.log(Direction.Up) // 0
console.log(Direction[0]) // "Up"

可以修改这个数值,比如你想让这个编码从 1 开始而不是 0,可以如下定义:

enum Direction {
  Up = 1,
  Down,  // 2
  Left,  // 3
  Right  // 4
}

未赋初始值的枚举项会接着上个项的值进行递增。

也可以指定任意索引值:

enum Direction {
  Up = 200,
  Down = 404,
  Left = 500,
  Right  // 默认递增
}

数字枚举在定义值的时候,可以使用计算值和常量。但是要注意,如果某个字段使用了计算值或常量,那么该字段后面紧接着的字段必须设置初始值,这里不能使用默认的递增值了,来看例子:

const getValue = () => {
  return 0;
};
enum ErrorIndex {
  a = getValue(),
  b, // error 枚举成员必须具有初始化的值
  c
}
enum RightIndex {
  a = getValue(),
  b = 1,
  c
}
const Start = 1;
enum Index {
  a = Start,
  b, // error 枚举成员必须具有初始化的值
  c
}

# 双向映射

使用 tsc 将上述代码编译为 js 后,可以发现 ts 使用 Direction[(Direction["Up"] = 0)] = "Up" 这样的内部实现对对象成员进行了双向的赋值。

由此可以看出,enum 具有双向映射的特点

var Direction
;(function (Direction) {
  Direction[(Direction['Up'] = 0)] = 'Up'
  Direction[(Direction['Down'] = 1)] = 'Down'
  Direction[(Direction['Left'] = 2)] = 'Left'
  Direction[(Direction['Right'] = 3)] = 'Right'
})(Direction || (Direction = {}))

console.log(Direction.Up) // 0
console.log(Direction[0]) // "Up"

# 字符串枚举

枚举成员为字符串时,其之后的成员也必须是字符串。

enum Direction {
  Up, // 未赋值,默认为0
  Down = '南',
  Left = '西',
  Right = '东'
}

# 常量枚举

PS: ts version 1.4 新增 const enum**(完全嵌入的枚举)**

你可以使用constenum关键字组合,声明一个常量枚举。

特点:定义了枚举值之后,编译成 JavaScript 的代码会创建一个对应的对象,这个对象我们可以在程序运行的时候使用。而加上了const关键字后,编译后的代码不会创建这个对象:

来看我们下面的定义:

enum Status {
  Off,
  On
}
const enum Animal {
  Dog,
  Cat
}
const status = Status.On;
const animal = Animal.Dog;

上面的例子编译成 JavaScript 之后是这样的:

var Status;
(function(Status) {
  Status[(Status["Off"] = 0)] = "Off";
  Status[(Status["On"] = 1)] = "On";
})(Status || (Status = {}));
var status = Status.On;
var animal = 0; /* Dog */

先是定义一个变量 Status,然后定义一个立即执行函数,在函数内给 Status 添加对应属性。

  • 首先Status[“Off”] = 0是给Status对象设置Off属性,并且值设为 0,这个赋值表达式的返回值是等号右边的值,也就是 0,所以Status[Status[“Off”] = 0] = "Off"相当于Status[0] = “Off”
  • 创建了这个对象之后,将 Status 的 On 属性值赋值给 status;
  • 再来看下 animal 的处理,我们看到编译后的代码并没有像Status创建一个Animal对象,而是直接把Animal.Dog的值0替换到了const animal = Animal.Dog表达式的Animal.Dog位置,这就是const enum的用法了

常量枚举中不允许使用计算值(变量或表达式)

image-20210503183210347

# *异构枚举

异构枚举就是枚举值中成员值既有数字类型又有字符串类型:

enum Result {
  Faild = 0,
  Success = "Success"
}

如果不是真的需要,不建议使用。因为往往我们将一类值整理为一个枚举值的时候,它们的特点是相似的。

# *枚举成员类型和联合枚举类型

  1. 枚举成员类型

    我们可以把符合条件的枚举值的成员作为类型来使用,来看例子:

    enum Animal {
      Dog = 1,
      Cat = 2
    }
    interface Dog {
      type: Animal.Dog; // 这里使用Animal.Dog作为类型,指定接口Dog的必须有一个type字段,且类型为Animal.Dog
    }
    interface Cat {
      type: Animal.Cat; // 这里同上
    }
    let cat1: Cat = {
      type: Animal.Dog // error [ts] 不能将类型“Animal.Dog”分配给类型“Animal.Cat”
    };
    let dog: Dog = {
      type: Animal.Dog
    };
    
  2. 联合枚举类型

    当我们的枚举值符合条件时,这个枚举值就可以看做是一个包含所有成员的联合类型,先来看例子:

    enum Status {
      Off,
      On
    }
    interface Light {
      status: Status;
    }
    enum Animal {
      Dog = 1,
      Cat = 2
    }
    const light1: Light = {
      status: Animal.Dog // error 不能将类型“Animal.Dog”分配给类型“Status”
    };
    const light2: Light = {
      status: Status.Off
    };
    const light3: Light = {
      status: Status.On
    };
    

    上面例子定义接口 Light 的 status 字段的类型为枚举值 Status,那么此时 status 的属性值必须为 Status.Off 和 Status.On 中的一个,也就是相当于status: Status.Off | Status.On

# 特殊类型

# any

any:有时,我们在编写代码的时候,并不能清楚地知道一个值到底是什么类型,这时就需要用到 any 类型,即任意类型;

# void

void: voidany 相反,any 是表示任意类型,而 void 是表示没有任意类型,就是什么类型都不是;

注意:void 类型的变量只能赋值为 undefinednull其他类型不能赋值给 void 类型的变量

# never

never: never 类型指那些永不存在的值的类型,它是那些总会抛出异常或根本不会有返回值的函数表达式的返回值类型。

当变量被永不为真的类型保护所约束时,该变量也是 never 类型。

这三个其实跟Function有很大的关系:

// void类型:没有返回值
function sayHello(): void {
  console.log('hello')
}
// never类型:函数执行不完
function errorEmitter(): never {
  while (true) {}
}
// 函数的解构赋值
function add({ first, second }: { first: number; second: number }): number {
  return first + second
}
add({ first: 1, second: 2 })

有的小伙伴就问了,这个never有啥用?

  • 限制类型
  • 控制流程
  • 类型运算

举个具体点的例子,当你有一个 union type:

interface Foo {
  type: 'foo'
}

interface Bar {
  type: 'bar'
}

type All = Foo | Bar

在 switch 当中判断 type,TS 是可以收窄类型的 (discriminated union):

function handleValue(val: All) {
  switch (val.type) {
    case 'foo':
      // 这里 val 被收窄为 Foo
      break
    case 'bar':
      // val 在这里是 Bar
      break
    default:
      // val 在这里是 never
      const exhaustiveCheck: never = val
      break
  }
}

注意在 default 里面我们把被收窄为 never 的 val 赋值给一个显式声明为 never 的变量。

如果一切逻辑正确,那么这里应该能够编译通过。

但是假如后来有一天你的同事改了 All 的类型:

type All = Foo | Bar | Baz

然而他忘记了在 handleValue 里面加上针对 Baz 的处理逻辑,这个时候在 default branch 里面 val 会被收窄为 Baz,导致无法赋值给 never,产生一个编译错误。

所以通过这个办法,你可以确保 handleValue 总是穷尽 (exhaust) 了所有 All 的可能类型。

再来一个流程控制的示例:

never可以用来使得异常的处理更加安全,如果一个函数返回了never类型,那意味着这个函数不会返回给caller,这就意味着caller在调用返回never函数后的代码都成了unreachable code。

类型运算示例:

  1. 不相交类型的inteserction结果为never:
type result = 1 & 2 // 结果为never
  1. 是任何类型的subtype
type Check<T> = never extends T ? true : false
type result = check<xxx> // 结果始终为true
  1. 除了never,没有其他类型是never的subtype
type Check<T> = never extends never ? false : T extends never ? true : false
type result = check<xxx> // 结果始终为false
  1. 布尔运算

union运算的幺元,intersection运算的零元

T | never // 结果为T
T & never // 结果为never

# unknown

unknown: unknown类型是TypeScript在3.0版本新增的类型,它表示未知的类型。

特点:

  • 任何类型的值都可以赋值给 unknown 类型;

  • 如果没有类型断言或基于控制流的类型细化时 unknown 不可以赋值给其它类型,此时它只能赋值给 unknown 和 any 类型:

    let value2: unknown;
    let value3: string = value2; // error 不能将类型“unknown”分配给类型“string”
    value1 = value2;
    
  • 如果没有类型断言或基于控制流的类型细化,则不能在它上面进行任何操作:

    let value4: unknown;
    value4 += 1; // error 对象的类型为 "unknown"
    
  • unknown 与任何其它类型组成的**交叉类型**,最后都等于其它类型:

    type type1 = unknown & string; // type1 => string
    type type2 = number & unknown; // type2 => number
    type type3 = unknown & unknown; // type3 => unknown
    type type4 = unknown & string[]; // type4 => string[]
    
  • unknown 与任何其它类型组成的**联合类型**,都等于 unknown 类型,但只有any例外,unknown与any组成的联合类型等于any):

    type type5 = string | unknown; // type5 => unknown
    type type6 = any | unknown; // type6 => any
    type type7 = number[] | unknown; // type7 => unknown
    
  • never 类型是 unknown 的子类型:

    type type8 = never extends unknown ? true : false; // type8 => true
    
  • keyof unknown 等于类型 never:

    type type9 = keyof unknown; // type9 => never
    
  • 只能对 unknown 进行等或不等操作,不能进行其它操作:

    value1 === value2;
    value1 !== value2;
    value1 += value2; // error
    
  • unknown 类型的值不能访问其属性、作为函数调用和作为类创建实例:

    let value5: unknown;
    value5.age; // error
    value5(); // error
    new value5(); // error
    
  • 使用映射类型时如果遍历的是 unknown 类型,则不会映射任何属性:

    type Types<T> = { [P in keyof T]: number };
    type type10 = Types<any>; // type10 => { [x: string]: number }
    type type11 = Types<unknown>; // type10 => {}
    

我们在实际使用中,如果有类型无法确定的情况,要尽量避免使用 any,why?

与any的区别:

  • 当一个值我们不能确定它的类型的时候,可以指定它是any类型;任意类型的值来使用,这往往会产生问题,如下:

    let tmp: any
    console.log(tmp.name) // 当 tmp 是一个对象时,访问name属性是没问题的
    console.log(tmp.toFixed())  // 当 tmp 是数值类型的时候,调用它的toFixed方法没问题
    console.log(tmp.length) // 当 tmp 是字符串或数组时获取它的length属性是没问题
    
    // 而上面的三个式子都不会报错,因为tmp是any类型
    
  • 当你指定值为unknown类型的时候,如果没有通过基于控制流的类型断言来缩小范围的话,是不能对它进行任何操作;

结论:unknown相比于any是安全的