小鱼的博客 小鱼的博客
首页
  • 《ES6 教程》笔记
  • 《JavaScript教程》笔记
  • 《vue》笔记
  • 《git》学习笔记
  • 《TypeScript》学习笔记
  • 《Java教程》笔记
更多
  • 网站
  • 资源
  • Vue资源
分类
标签
归档
关于
友情链接
GitHub (opens new window)

小鱼

前端界的大佬,后端界的小学生
首页
  • 《ES6 教程》笔记
  • 《JavaScript教程》笔记
  • 《vue》笔记
  • 《git》学习笔记
  • 《TypeScript》学习笔记
  • 《Java教程》笔记
更多
  • 网站
  • 资源
  • Vue资源
分类
标签
归档
关于
友情链接
GitHub (opens new window)
  • 简介

  • 基础

    • 数据类型
      • 布尔值
      • 数值
      • 字符串
      • Null 和 Undefined
      • Symbol 类型
        • Symbol 基础知识
      • 数组(Array)
        • 「类型 + 方括号」表示法
        • 数组泛型
        • 用接口表示数组
        • 类数组
        • any 在数组中的应用
      • 元组(Tuple)
        • 简单的例子
        • 越界的元素
      • 枚举(Enum)类型
        • 简单的例子
        • 手动赋值
        • 常数项和计算所得项
        • 常数枚举
        • 外部枚举
      • 任意值(Any)类型
        • 什么是任意值类型
        • 任意值的属性和方法
        • 未声明类型的变量
      • 空值(Void)类型
      • Never类型
      • Object类型
      • Unknown
        • 使用类型断言缩小未知范围.
        • 使用类型收缩
    • 类型断言
    • 类型推论
    • 联合类型
    • 类型别名
    • 交叉类型
    • 可辨识联合
    • 类型保护与区分类型
    • TypeScript 函数
    • TypeScript 接口
    • TypeScript 类
    • TypeScript泛型
  • 《TypeScript》学习笔记
  • 基础
小鱼
2021-08-25
目录

数据类型

我们知道 Javascript 数据类型分为原始数据类型和引用数据类型。

原始数据类型 包括:

  • 字符串(String)
  • 数字(Number)
  • 布尔(Boolean)
  • 对空(Null)
  • 未定义(Undefined)
  • Symbol

共6种类型。

引用数据类型 包括对象(Object)、数组(Array)、函数(Function)。

Typescript 的数据类型在 Javascript的基础上,增加了

  • 枚举(Enum)
  • 元组(Tuple)
  • Any
  • Void
  • Never

注意

  1. 类型注解一律小写,不要使用大写英文。
  2. 使用构造函数 String 生成的实例,本质上是对象,而不是字符串类型,Number、Boolean 同理。

# 布尔值

最基本的数据类型就是简单的true/false值,在JavaScript和TypeScript里叫做boolean(其它语言中也一样)。

布尔值是最基础的数据类型,在 TypeScript 中,使用 boolean 定义布尔值类型:

let isDone: boolean = false;

// 编译通过
// 后面约定,未强调编译错误的代码片段,默认为编译通过
1
2
3
4

注意,使用构造函数 Boolean 创造的对象不是布尔值:

let createdByNewBoolean: boolean = new Boolean(1);

// Type 'Boolean' is not assignable to type 'boolean'.
//   'boolean' is a primitive, but 'Boolean' is a wrapper object. Prefer using 'boolean' when possible.
1
2
3
4

事实上 new Boolean() 返回的是一个 Boolean 对象:

let createdByNewBoolean: Boolean = new Boolean(1);
1

直接调用 Boolean 也可以返回一个 boolean 类型:

let createdByBoolean: boolean = Boolean(1);
1

在 TypeScript 中,boolean 是 JavaScript 中的基本类型,而 Boolean 是 JavaScript 中的构造函数。其他基本类型(除了 null 和 undefined)一样,不再赘述。

# 数值

使用 number 定义数值类型:

let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
// ES6 中的二进制表示法
let binaryLiteral: number = 0b1010;
// ES6 中的八进制表示法
let octalLiteral: number = 0o744;
let notANumber: number = NaN;
let infinityNumber: number = Infinity;
1
2
3
4
5
6
7
8

编译结果:

var decLiteral = 6;
var hexLiteral = 0xf00d;
// ES6 中的二进制表示法
var binaryLiteral = 10;
// ES6 中的八进制表示法
var octalLiteral = 484;
var notANumber = NaN;
var infinityNumber = Infinity;
1
2
3
4
5
6
7
8

其中 0b1010 和 0o744 是 ES6 中的二进制和八进制表示法 (opens new window),它们会被编译为十进制数字。

# 字符串

使用 string 定义字符串类型:

let myName: string = 'Tom';
let myAge: number = 25;
// 模板字符串
let sentence: string = `Hello, my name is ${myName}.
I'll be ${myAge + 1} years old next month.`;
1
2
3
4
5

编译结果:

var myName = 'Tom';
var myAge = 25;
// 模板字符串
var sentence = "Hello, my name is " + myName + ".\nI'll be " + (myAge + 1) + " years old next month.";
1
2
3
4

其中 ` 用来定义 ES6 中的模板字符串 (opens new window),${expr} 用来在模板字符串中嵌入表达式。

# Null 和 Undefined

在 TypeScript 中,可以使用 null 和 undefined 来定义这两个原始数据类型:

let u: undefined = undefined;
let n: null = null;
1
2

与 void 的区别是,undefined 和 null 是所有类型的子类型。也就是说 undefined 类型的变量,可以赋值给 number 类型的变量:

// 这样不会报错
let num: number = undefined;
// 这样也不会报错
let u: undefined;
let num: number = u;
1
2
3
4
5

而 void 类型的变量不能赋值给 number 类型的变量:

let u: void;
let num: number = u;
// Type 'void' is not assignable to type 'number'.
1
2
3

然而,当你指定了--strictNullChecks标记,null和undefined只能赋值给void和它们各自。 这能避免很多常见的问题。 也许在某处你想传入一个 string或null或undefined,你可以使用联合类型string | null | undefined。 再次说明,稍后我们会介绍联合类型。

# Symbol 类型

Symbol是ES6引入的一个新特性 —— 新到什么程度呢?ES5之前是没有任何办法可以模拟Symbol的

但是,我们日常开发工作中,直接使用到Symbol的场景似乎很少。我在网上搜了很多资料,对Symbol开始逐渐加深了理解,接下来就谈谈我的一些看法。具体的内容请参考阮一峰的《ES6 入门教程》中Symbol (opens new window)章节

# Symbol 基础知识

symbol 是一种全新的基本数据类型 (primitive data type)

Symbol()函数会返回symbol类型的值,作为构造函数来说它并不完整,因为它不支持语法:

new Symbol()   // Uncaught TypeError: Symbol is not a constructor
1

注意

注意,Symbol函数前不能使用new命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。也就是说,由于 Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。

每个从Symbol()返回的symbol值都是唯一的,使用Symbol()创建新的symbol值,并用一个可选的字符串作为其描述 —— 描述相同的两个Symbol值依然是不同的,这样做的话在控制台显示比较容易区分

const symbol1 = Symbol();
const symbol2 = Symbol(42);
const symbol3 = Symbol('foo'); //描述

console.log(typeof symbol1); // "symbol"

console.log(symbol2 === 42); //  false

console.log(symbol3.toString()); // "Symbol(foo)"

console.log(Symbol('foo') === Symbol('foo')); //  false
1
2
3
4
5
6
7
8
9
10
11

如果 Symbol 的参数是一个对象,就会调用该对象的toString方法,将其转为字符串,然后才生成一个 Symbol 值。

const obj = {
  toString() {
    return 'abc';
  }
};
const sym = Symbol(obj);
sym // Symbol(abc)

1
2
3
4
5
6
7
8

一个symbol值能作为对象属性的标识符 —— 这是该数据类型仅有的目的(划重点)

const obj = {};
const  myPrivateMethod  = Symbol();
obj[myPrivateMethod] = function() {...};

1
2
3
4

当一个 symbol 类型的值在属性赋值语句中被用作标识符,该属性(像这个 symbol 一样)是匿名的, 并且是不可枚举的 —— 因为这个属性是不可枚举的,它不会在循环结构 for( ... in ...) 中作为成员出现,也因为这个属性是匿名的,它同样不会出现在 Object.getOwnPropertyNames() 的返回数组里。

这个属性可以通过创建时的原始 symbol 值访问到,或者通过遍历Object.getOwnPropertySymbols() 返回的数组。

在上面的代码示例中,只有通过保存在变量 myPrivateMethod的值可以访问到对象属性(划重点)

const obj = {};

const aProperty = Symbol("a");
obj[aProperty] = "a";
obj[Symbol.for("b")] = "b";
obj["c"] = "c";
obj.d = "d";

for (let i in obj) {
  console.log(i); // 输出 "c" 和 "d"
}


console.log(Object.getOwnPropertySymbols(obj)); // [ Symbol(a), Symbol(b) ]

console.log(obj[aProperty]); // a
console.log(obj[Symbol.for("b")]); // b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 数组(Array)

在 TypeScript 中,数组类型有多种定义方式,比较灵活。

# 「类型 + 方括号」表示法

最简单的方法是使用「类型 + 方括号」来表示数组:

let fibonacci: number[] = [1, 1, 2, 3, 5];
1

数组的项中不允许出现其他的类型:

let fibonacci: number[] = [1, '1', 2, 3, 5];

// Type 'string' is not assignable to type 'number'.
1
2
3

数组的一些方法的参数也会根据数组在定义时约定的类型进行限制:

let fibonacci: number[] = [1, 1, 2, 3, 5];
fibonacci.push('8');

// Argument of type '"8"' is not assignable to parameter of type 'number'.
1
2
3
4

上例中,push 方法只允许传入 number 类型的参数,但是却传了一个 "8" 类型的参数,所以报错了。这里 "8" 是一个字符串字面量类型,会在后续章节中详细介绍。

# 数组泛型

我们也可以使用数组泛型(Array Generic) Array< elemType > 来表示数组:

let fibonacci: Array<number> = [1, 1, 2, 3, 5];
1

关于泛型,可以参考泛型一章。

# 用接口表示数组

接口也可以用来描述数组:

interface NumberArray {
    [index: number]: number;
}
let fibonacci: NumberArray = [1, 1, 2, 3, 5];
1
2
3
4

NumberArray 表示:只要索引的类型是数字时,那么值的类型必须是数字。

虽然接口也可以用来描述数组,但是我们一般不会这么做,因为这种方式比前两种方式复杂多了。

不过有一种情况例外,那就是它常用来表示类数组。

# 类数组

类数组(Array-like Object)不是数组类型,比如 arguments:

function sum() {
    let args: number[] = arguments;
}

// Type 'IArguments' is missing the following properties from type 'number[]': pop, push, concat, join, and 24 more.
1
2
3
4
5

上例中,arguments 实际上是一个类数组,不能用普通的数组的方式来描述,而应该用接口:

function sum() {
    let args: {
        [index: number]: number;
        length: number;
        callee: Function;
    } = arguments;
}
1
2
3
4
5
6
7

在这个例子中,我们除了约束当索引的类型是数字时,值的类型必须是数字之外,也约束了它还有 length 和 callee 两个属性。

事实上常用的类数组都有自己的接口定义,如 IArguments, NodeList, HTMLCollection 等:

function sum() {
    let args: IArguments = arguments;
}
1
2
3

其中 IArguments 是 TypeScript 中定义好了的类型,它实际上就是:

interface IArguments {
    [index: number]: any;
    length: number;
    callee: Function;
}
1
2
3
4
5

关于内置对象,可以参考内置对象一章。

# any 在数组中的应用

一个比较常见的做法是,用 any 表示数组中允许出现任意类型:

let list: any[] = ['xcatliu', 25, { website: 'http://xcatliu.com' }];
1

# 元组(Tuple)

数组合并了相同类型的对象,而元组(Tuple)合并了不同类型的对象。

元组起源于函数编程语言(如 F#),这些语言中会频繁使用元组。

# 简单的例子

定义一对值分别为 string 和 number 的元组:

let tom: [string, number] = ['Tom', 25];
1

当赋值或访问一个已知索引的元素时,会得到正确的类型:

let tom: [string, number];
tom[0] = 'Tom';
tom[1] = 25;

tom[0].slice(1);
tom[1].toFixed(2);
也可以只赋值其中一项:

let tom: [string, number];
tom[0] = 'Tom';
1
2
3
4
5
6
7
8
9
10

但是当直接对元组类型的变量进行初始化或者赋值的时候,需要提供所有元组类型中指定的项。

let tom: [string, number];
tom = ['Tom', 25];
let tom: [string, number];
tom = ['Tom'];

// Property '1' is missing in type '[string]' but required in type '[string, number]'.
1
2
3
4
5
6

# 越界的元素

当添加越界的元素时,它的类型会被限制为元组中每个类型的联合类型:

let tom: [string, number];
tom = ['Tom', 25];
tom.push('male');
tom.push(true);

// Argument of type 'true' is not assignable to parameter of type 'string | number'.
1
2
3
4
5
6

# 枚举(Enum)类型

枚举(Enum)类型用于取值被限定在一定范围内的场景,比如一周只能有七天,颜色限定为红绿蓝等。

# 简单的例子

枚举使用 enum 关键字来定义:

enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};
1

枚举成员会被赋值为从 0 开始递增的数字,同时也会对枚举值到枚举名进行反向映射:

enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};

console.log(Days["Sun"] === 0); // true
console.log(Days["Mon"] === 1); // true
console.log(Days["Tue"] === 2); // true
console.log(Days["Sat"] === 6); // true

console.log(Days[0] === "Sun"); // true
console.log(Days[1] === "Mon"); // true
console.log(Days[2] === "Tue"); // true
console.log(Days[6] === "Sat"); // true
1
2
3
4
5
6
7
8
9
10
11

事实上,上面的例子会被编译为:

var Days;
(function (Days) {
    Days[Days["Sun"] = 0] = "Sun";
    Days[Days["Mon"] = 1] = "Mon";
    Days[Days["Tue"] = 2] = "Tue";
    Days[Days["Wed"] = 3] = "Wed";
    Days[Days["Thu"] = 4] = "Thu";
    Days[Days["Fri"] = 5] = "Fri";
    Days[Days["Sat"] = 6] = "Sat";
})(Days || (Days = {}));
1
2
3
4
5
6
7
8
9
10

# 手动赋值

我们也可以给枚举项手动赋值:

enum Days {Sun = 7, Mon = 1, Tue, Wed, Thu, Fri, Sat};

console.log(Days["Sun"] === 7); // true
console.log(Days["Mon"] === 1); // true
console.log(Days["Tue"] === 2); // true
console.log(Days["Sat"] === 6); // true
1
2
3
4
5
6

上面的例子中,未手动赋值的枚举项会接着上一个枚举项递增。

如果未手动赋值的枚举项与手动赋值的重复了,TypeScript 是不会察觉到这一点的:

enum Days {Sun = 3, Mon = 1, Tue, Wed, Thu, Fri, Sat};

console.log(Days["Sun"] === 3); // true
console.log(Days["Wed"] === 3); // true
console.log(Days[3] === "Sun"); // false
console.log(Days[3] === "Wed"); // true
1
2
3
4
5
6

上面的例子中,递增到 3 的时候与前面的 Sun 的取值重复了,但是 TypeScript 并没有报错,导致 Days[3] 的值先是 "Sun",而后又被 "Wed" 覆盖了。编译的结果是:

var Days;
(function (Days) {
    Days[Days["Sun"] = 3] = "Sun";
    Days[Days["Mon"] = 1] = "Mon";
    Days[Days["Tue"] = 2] = "Tue";
    Days[Days["Wed"] = 3] = "Wed";
    Days[Days["Thu"] = 4] = "Thu";
    Days[Days["Fri"] = 5] = "Fri";
    Days[Days["Sat"] = 6] = "Sat";
})(Days || (Days = {}));
1
2
3
4
5
6
7
8
9
10

所以使用的时候需要注意,最好不要出现这种覆盖的情况。

手动赋值的枚举项可以不是数字,此时需要使用类型断言来让 tsc 无视类型检查 (编译出的 js 仍然是可用的):

enum Days {Sun = 7, Mon, Tue, Wed, Thu, Fri, Sat = <any>"S"};
var Days;
(function (Days) {
    Days[Days["Sun"] = 7] = "Sun";
    Days[Days["Mon"] = 8] = "Mon";
    Days[Days["Tue"] = 9] = "Tue";
    Days[Days["Wed"] = 10] = "Wed";
    Days[Days["Thu"] = 11] = "Thu";
    Days[Days["Fri"] = 12] = "Fri";
    Days[Days["Sat"] = "S"] = "Sat";
})(Days || (Days = {}));
1
2
3
4
5
6
7
8
9
10
11

当然,手动赋值的枚举项也可以为小数或负数,此时后续未手动赋值的项的递增步长仍为 1:

enum Days {Sun = 7, Mon = 1.5, Tue, Wed, Thu, Fri, Sat};

console.log(Days["Sun"] === 7); // true
console.log(Days["Mon"] === 1.5); // true
console.log(Days["Tue"] === 2.5); // true
console.log(Days["Sat"] === 6.5); // true
1
2
3
4
5
6

# 常数项和计算所得项

枚举项有两种类型:常数项(constant member)和计算所得项(computed member)。

前面我们所举的例子都是常数项,一个典型的计算所得项的例子:

enum Color {Red, Green, Blue = "blue".length};
1

上面的例子中,"blue".length 就是一个计算所得项。

上面的例子不会报错,但是如果紧接在计算所得项后面的是未手动赋值的项,那么它就会因为无法获得初始值而报错:

enum Color {Red = "red".length, Green, Blue};

// index.ts(1,33): error TS1061: Enum member must have initializer.
// index.ts(1,40): error TS1061: Enum member must have initializer.
1
2
3
4

下面是常数项和计算所得项的完整定义,部分引用自中文手册 - 枚举 (opens new window):

当满足以下条件时,枚举成员被当作是常数:

  • 不具有初始化函数并且之前的枚举成员是常数。在这种情况下,当前枚举成员的值为上一个枚举成员的值加 1。但第一个枚举元素是个例外。如果它没有初始化方法,那么它的初始值为 0。
  • 枚举成员使用常数枚举表达式初始化。常数枚举表达式是 TypeScript 表达式的子集,它可以在编译阶段求值。当一个表达式满足下面条件之一时,它就是一个常数枚举表达式:
    • 数字字面量
    • 引用之前定义的常数枚举成员(可以是在不同的枚举类型中定义的)如果这个成员是在同一个枚举类型中定义的,可以使用非限定名来引用
    • 带括号的常数枚举表达式
    • +, -, ~ 一元运算符应用于常数枚举表达式
    • +, -, *, /, %, <<, >>, >>>, &, |, ^ 二元运算符,常数枚举表达式做为其一个操作对象。若常数枚举表达式求值后为 NaN 或 Infinity,则会在编译阶段报错

所有其它情况的枚举成员被当作是需要计算得出的值。

# 常数枚举

常数枚举是使用 const enum 定义的枚举类型:

const enum Directions {
    Up,
    Down,
    Left,
    Right
}

let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
1
2
3
4
5
6
7
8

常数枚举与普通枚举的区别是,它会在编译阶段被删除,并且不能包含计算成员。

上例的编译结果是:

var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];
1

假如包含了计算成员,则会在编译阶段报错:

const enum Color {Red, Green, Blue = "blue".length};

// index.ts(1,38): error TS2474: In 'const' enum declarations member initializer must be constant expression.
1
2
3

# 外部枚举

外部枚举(Ambient Enums)是使用 declare enum 定义的枚举类型:

declare enum Directions {
    Up,
    Down,
    Left,
    Right
}

let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
1
2
3
4
5
6
7
8

之前提到过,declare 定义的类型只会用于编译时的检查,编译结果中会被删除。

上例的编译结果是:

var directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
1

外部枚举与声明语句一样,常出现在声明文件中。

同时使用 declare 和 const 也是可以的:

declare const enum Directions {
    Up,
    Down,
    Left,
    Right
}

let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
1
2
3
4
5
6
7
8

编译结果:

var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];
1

TypeScript 的枚举类型的概念来源于 C#。

# 任意值(Any)类型

任意值(Any)用来表示允许赋值为任意类型。

# 什么是任意值类型

如果是一个普通类型,在赋值过程中改变类型是不被允许的:

let myFavoriteNumber: string = 'seven';
myFavoriteNumber = 7;

// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.
1
2
3
4

有时候,我们会想要为那些在编程阶段还不清楚类型的变量指定一个类型。 这些值可能来自于动态的内容,比如来自用户输入或第三方代码库。 这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。 那么我们可以使用 any类型来标记这些变量:

let myFavoriteNumber: any = 'seven';
myFavoriteNumber = 7;
1
2

# 任意值的属性和方法

在任意值上访问任何属性都是允许的:

let anyThing: any = 'hello';
console.log(anyThing.myName);
console.log(anyThing.myName.firstName);
1
2
3

也允许调用任何方法:

let anyThing: any = 'Tom';
anyThing.setName('Jerry');
anyThing.setName('Jerry').sayHello();
anyThing.myName.setFirstName('Cat');
1
2
3
4

可以认为,声明一个变量为任意值之后,对它的任何操作,返回的内容的类型都是任意值。

在对现有代码进行改写的时候,any类型是十分有用的,它允许你在编译时可选择地包含或移除类型检查。 你可能认为 Object有相似的作用,就像它在其它语言中那样。 但是 Object类型的变量只是允许你给它赋任意值 - 但是却不能够在它上面调用任意的方法,即便它真的有这些方法:

let notSure: any = 4;
notSure.ifItExists(); // okay, ifItExists might exist at runtime
notSure.toFixed(); // okay, toFixed exists (but the compiler doesn't check)

let prettySure: Object = 4;
prettySure.toFixed(); // Error: Property 'toFixed' doesn't exist on type 'Object'.
1
2
3
4
5
6

# 未声明类型的变量

变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型:

let something;
something = 'seven';
something = 7;

something.setName('Tom');
1
2
3
4
5

等价于

let something: any;
something = 'seven';
something = 7;

something.setName('Tom');
1
2
3
4
5

# 空值(Void)类型

JavaScript 没有空值(Void)的概念,在 TypeScript 中,可以用 void 表示没有任何返回值的函数,某种程度上来说,void类型像是与any类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是 void:

function alertName(): void {
    alert('My name is Tom');
}
1
2
3

声明一个 void 类型的变量没有什么用,因为你只能将它赋值为 undefined 和 null(只在 --strictNullChecks 未指定时):

let unusable: void = undefined;
1

# Never类型

never类型表示的是那些永不存在的值的类型。 例如, never类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型; 变量也可能是 never类型,当它们被永不为真的类型保护所约束时。

never类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是never的子类型或可以赋值给never类型(除了never本身之外)。 即使 any也不可以赋值给never。

下面是一些返回never类型的函数:

// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
    throw new Error(message);
}

// 推断的返回值类型为never
function fail() {
    return error("Something failed");
}

// 返回never的函数必须存在无法达到的终点
function infiniteLoop(): never {
    while (true) {
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

在 TypeScript 中,可以利用 never 类型的特性来实现全面性检查,具体示例如下:

type Foo = string | number;

function controlFlowAnalysisWithNever(foo: Foo) {
  if (typeof foo === "string") {
    // 这里 foo 被收窄为 string 类型
  } else if (typeof foo === "number") {
    // 这里 foo 被收窄为 number 类型
  } else {
    // foo 在这里是 never
    const check: never = foo;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

注意在 else 分支里面,我们把收窄为 never 的 foo 赋值给一个显示声明的 never 变量。如果一切逻辑正确,那么这里应该能够编译通过。但是假如后来有一天你的同事修改了 Foo 的类型:

type Foo = string | number | boolean;
1

然而他忘记同时修改 controlFlowAnalysisWithNever 方法中的控制流程,这时候 else 分支的 foo 类型会被收窄为 boolean 类型,导致无法赋值给 never 类型,这时就会产生一个编译错误。通过这个方式,我们可以确保controlFlowAnalysisWithNever 方法总是穷尽了 Foo 的所有可能类型。 通过这个示例,我们可以得出一个结论:使用 never 避免出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码。

# Object类型

object表示非原始类型,也就是除number,string,boolean,symbol,null或undefined之外的类型。

使用object类型,就可以更好的表示像Object.create这样的API。例如:


let obj:object;
obj = {name: 'Wang', age: 25};

declare function create(o: object | null): void;

create({ prop: 0 }); // OK
create(null); // OK

create(42); // Error
create("string"); // Error
create(false); // Error
create(undefined); // Error
1
2
3
4
5
6
7
8
9
10
11
12
13

# Unknown

TypeScript 3.0中引入的 unknown 类型也被认为是 top type ,但它更安全。与 any 一样,所有类型都可以分配给unknown。

let uncertain: unknown = 'Hello'!;
uncertain = 12;
uncertain = { hello: () => 'Hello!' };
1
2
3

我们只能将 unknown 类型的变量赋值给 any 和 unknown。

let uncertain: unknown = 'Hello'!;
let notSure: any = uncertain;
1
2

它确实在很多方面不同于 any 类型。如果不缩小类型,就无法对 unknown 类型执行任何操作。

function getDog() {
 return '22'
}

const dog: unknown = getDog();
dog.hello(); //Object is of type 'unknown'
1
2
3
4
5
6

# 使用类型断言缩小未知范围.

上述机制具有很强的预防性,但对我们的限制过于有限。要对未知类型执行某些操作,首先需要使用类型断言来缩小范围。

const getDogName = () => {
 let x: unknown;
 return x;
};

const dogName = getDogName();
console.log((dogName as string).toLowerCase()); 
1
2
3
4
5
6
7

在上面的代码中,我们强制TypeScript编译器相信我们知道自己在做什么。

以上的一个重要缺点是它只是一个假设。它没有运行时效果,也不能防止我们在不小心的情况下造成错误。 比如下面的代码, 他实际上是错误的, 但却可以通过 typescript 的检测.

const number: unknown = 15;
(number as string).toLowerCase();

1
2
3

TypeScript编译器接收到我们的数字是一个字符串的假设,因此它并不反对这样处理它。

# 使用类型收缩

一种更类型安全的缩小未知类型的方法是使用 类型收缩 。TypeScript 编译器会分析我们的代码,并找出一个更窄的类型。

const dogName = getDogName();
 if (typeof dogName === 'string') {

  console.log(dogName.toLowerCase());

}
1
2
3
4
5
6

在上面的代码中,我们在运行时检查了 dogName 变量的类型。因此,我们可以确保只在 dogName 是变量时调用 toLowerCase函数。

除了使用 typeof,我们还可以使用 instanceof 来缩小变量的类型。

type getAnimal = () => unknown;

const dog = getAnimal();
 
if (dog instanceof Dog) {
 console.log(dog.name.toLowerCase());
}
1
2
3
4
5
6
7

在上面的代码中,我们确保只有在变量是某个原型的实例时才调用dog.name.toLowerCase。TypeScript编译器理解这一点,并假设类型。

在 GitHub 上编辑此页 ! (opens new window)
#TypeScript
上次更新: 2021/09/03, 09:17:45
Hello TypeScript
类型断言

← Hello TypeScript 类型断言→

最近更新
01
TypeScript泛型
08-26
02
TypeScript 类
08-26
03
TypeScript 接口
08-26
更多文章>
Theme by Vdoing | Copyright © 2021-2022 xiaoyu | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式