TS从零单排系列之 - 数据类型

412 阅读7分钟

原始类型

boolean

string

字符串支持模版字符串

number

const decLiteral: number = 6 
const hexLiteral: number = 0xf00d 
const binaryLiteral: number = 0b1010 
const octalLiteral: number = 0o744

void

表示没有任何类型,当一个函数没有返回值时,你通常会见到其返回值类型是 void:

function warnUser(): void {
    alert("This is my warning message");
}

实际上只有nullundefined可以赋给void:

const a: void = undefined

undefined和null

ypeScript 里,undefined 和 null 两者各自有自己的类型分别叫做 undefined 和 null,和void相似,它们的本身的类型用处不是很大:

let a: undefined = undefined;
let b: null = null;

默认情况下 nullundefined 是所有类型的子类型,就是说你可以把 nullundefined 赋值给 number 类型的变量。

但是在正式项目中一般都是开启 --strictNullChecks 检测的,即 null 和 undefined 只能赋值给 any 和它们各自(一个例外是 undefined 是也可以分配给void),可以规避非常多的问题。

symbol

注意:我们在使用 Symbol 的时候,必须添加 es6 的编译辅助库,如下:

2020-01-05-20-49-18

Symbol 是在ES2015之后成为新的原始类型,它通过 Symbol 构造函数创建:

const sym1 = Symbol('key1');
const sym2 = Symbol('key2');

而且 Symbol 的值是唯一不变的:

Symbol('key1') === Symbol('key1') // false

bigint

BigInt 类型在 TypeScript3.2 版本被内置,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了JavaScript构造函数 Number 能够表示的安全整数范围。

注意:我们在使用 BigInt 的时候,必须添加 ESNext 的编译辅助库,如下:

2020-01-05-20-47-38

特殊类型

any

any 类型的变量是可以进行任意进行赋值、实例化、函数执行等操作

unknown

当 unknown 类型被确定是某个类型之前,它不能被进行任何操作比如实例化、getter、函数执行等等

let demandOne: any;
let demandTwo: unknown;

demandOne = 'Hello, Tuture'; // 可以的
demandTwo = 'Hello, Ant Design'; // 可以的

demandOne.foo.bar() // 可以的
demandTwo.foo.bar() // 报错

never

never 类型表示的是那些永不存在的值的类型,never 类型是任何类型的子类型,也可以赋值给任何类型;
然而,没有类型是 never 的子类型或可以赋值给 never 类型(除了never本身之外)。
never类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型;
变量也可能是 never类型,当它们被永不为真的类型保护所约束时。

即使any也不可以赋值给never。

两个场景中 never 比较常见:

// 抛出异常的函数永远不会有返回值
function error(message: string): never {
    throw new Error(message);
}

// 空数组,而且永远是空的
const empty: 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 里面我们把被收窄为 neverval 赋值给一个显式声明为 never 的变量。如果一切逻辑正确,那么这里应该能够编译通过。但是假如后来有一天你的同事改了 All 的类型:type All = Foo | Bar | Baz

然而他忘记了在 handleValue 里面加上针对 Baz 的处理逻辑,这个时候在 default branch 里面 val 会被收窄为 Baz,导致无法赋值给 never,产生一个编译错误。所以通过这个办法,你可以确保 handleValue 总是穷尽 (exhaust) 了所有 All 的可能类型。

nevereg和void的区别

void可以被赋值为null或者undefined的类型,never则是一个不包含值得类型 拥有void返回值的函数能正常运行,拥有never返回值类型的函数无法正常返回,无法终止或者会抛出异常

非原始类型

数组

数组有两种类型定义方式,一种是使用泛型:

const list: Array<number> = [1, 2, 3]

另一种使用更加广泛那就是在元素类型后面接上 []:

const list: number[] = [1, 2, 3]

元组

元组类型与数组类型非常相似,表示一个已知元素数量和类型的数组,各元素的类型不必相同。
这就是元组与数组的不同之处,元组的类型如果多出或者少于规定的类型是会报错的,必须严格跟事先声明的类型一致才不会报错。
元组中包含的元素,必须与声明的类型一致,而且不能多、不能少,甚至顺序不能不符。


let x: [string, number]; 
x = ['hello', 10]; // OK 
x = [10, 'hello']; // Error

此外,还有一个个元组越界问题,比如 Typescript 允许向元组中使用数组的push方法插入新元素:

const tuple: [string, number] = ['a', 1];
tuple.push(2); // ok
console.log(tuple); // ["a", 1, 2] -> 正常打印出来

但是当我们访问新加入的元素时,会报错:

console.log(tuple[2]); 
// Tuple type '[string, number]' of length '2' has no element at index '2'

enum 枚举

枚举是一种为数字值集赋予更友好名称的方法

数字枚举

当我们声明一个枚举类型是,虽然没有给它们赋值,但是它们的值其实是默认的数字类型,而且默认从0开始依次累加:

enum Direction {
    Up,
    Down,
    Left,
    Right
}

console.log(Direction.Up === 0); // true
console.log(Direction.Down === 1); // true
console.log(Direction.Left === 2); // true
console.log(Direction.Right === 3); // true

因此当我们把第一个值赋值后,后面也会根据第一个值进行累加:

enum Direction {
    Up = 10,
    Down,
    Left,
    Right
}
console.log(Direction.Up, Direction.Down, Direction.Left, Direction.Right); 
// 10 11 12 13

或者,全部都采用手动赋值:

enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green;

let colorName: string = Color[2];
console.log(colorName);  // 显示'Green'因为上面代码里它的值是2

字符串枚举

在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。

enum Direction {
    Up = 'Up',
    Down = 'Down',
    Left = 'Left',
    Right = 'Right'
}

console.log(Direction['Right'], Direction.Up); // Right Up

异构枚举

既然我们已经有了字符串枚举和数字枚举,那么这两个枚举是不是可以混合使用呢?

enum BooleanLikeHeterogeneousEnum {
    No = 0,
    Yes = "YES",
}

是的,这样也是没问题的,通常情况下我们很少会这样使用枚举,但是从技术的角度来说,它是可行的。

反向映射

enum Direction {
    Up,
    Down,
    Left,
    Right
}

console.log(Direction.Up === 0); // true
console.log(Direction.Down === 1); // true
console.log(Direction.Left === 2); // true
console.log(Direction.Right === 3); // true


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

// 编译后的结果
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); // true
console.log(Direction.Down === 1); // true
console.log(Direction.Left === 2); // true
console.log(Direction.Right === 3); // true

不会为字符串枚举成员生成反向映射

const 枚举

枚举其实可以被 const 声明为常量的,这样有什么好处?我们看以下例子:

const enum Direction {
    Up = 'Up',
    Down = 'Down',
    Left = 'Left',
    Right = 'Right'
}

const a = Direction.Up;

大家猜一下它被编译为 JavaScript 后是怎样的?

var a = "Up";

我们在上面看到枚举类型会被编译为 JavaScript 对象,怎么这里没有了?

这就是常量枚举的作用,因为下面的变量 a 已经使用过了枚举类型,之后就没有用了,也没有必要存在与 JavaScript 中了, TypeScript 在这一步就把 Direction 去掉了,我们直接使用 Direction 的值即可,这是性能提升的一个方案。

如果你非要 TypeScript 保留对象 Direction ,那么可以添加编译选项 --preserveConstEnums

枚举合并

我们可以分开声明枚举,他们会自动合并

enum Direction {
    Up = 'Up',
    Down = 'Down',
    Left = 'Left',
    Right = 'Right'
}

enum Direction {
    Center = 1
}

编译为 JavaScript 后的代码如下:

var Direction;
(function (Direction) {
    Direction["Up"] = "Up";
    Direction["Down"] = "Down";
    Direction["Left"] = "Left";
    Direction["Right"] = "Right";
})(Direction || (Direction = {}));
(function (Direction) {
    Direction[Direction["Center"] = 1] = "Center";
})(Direction || (Direction = {}));

因此上面的代码并不冲突。

为枚举添加静态方法

借助 namespace 命名空间,我们甚至可以给枚举添加静态方法。

我们举个简单的例子,假设有十二个月份:

enum Month {
    January,
    February,
    March,
    April,
    May,
    June,
    July,
    August,
    September,
    October,
    November,
    December,
}

我们要编写一个静态方法,这个方法可以帮助我们把夏天的月份找出来:

function isSummer(month: Month) {
    switch (month) {
        case Month.June:
        case Month.July:
        case Month.August:
            return true;
        default:
            return false
    }
}

想要把两者结合就需要借助命名空间的力量了:

namespace Month {
    export function isSummer(month: Month) {
        switch (month) {
            case Month.June:
            case Month.July:
            case Month.August:
                return true;
            default:
                return false
        }
    }
}

console.log(Month.isSummer(Month.January)) // false

Object

object 表示非原始类型,也就是除 numberstringbooleansymbolnullundefined 之外的类型。


// 这是下一节会提到的枚举类型
enum Direction {
    Center = 1
}

let value: object

value = Direction
value = [1]
value = [1, 'hello']
value = {}

我们看到,普通对象、枚举、数组、元组通通都是 object 类型。