添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
Loading ...

# TypeScript 学习笔记

# 什么是 TypeScript?

TypeScript 简称 TS
TS 和 typescript 之间的关系其实就是 Less/Sass 和 CSS 之间的关系
就像 Less/Sass 是对 CSS 进行扩展一样,TS 也是对 typescript 进行扩展
就像 Less/Sass 最终会转换成 CSS 一样,我们编写好的 TS 代码最终也会换成 typescript
TypeScript 是 JavaScript 的超集,因为它扩展了 JavaScript,有 JavaScript 没有的东西。

# 为什么需要 TypeScript?

typescript 语言本身的特性,决定了该语言无法适应大型的复杂的项目 弱类型:某个变量,可以随时更换类型 解释型:错误发生的时间是在运行时。简单来说就是因为 JavaScript 是弱类型,很多错误只有在运行时才会被发现,可以扩展 JavaScript

而 TypeScript 提供了一套静态检测机制,可以帮助我们在编译时就发现错误,它提高了 JavaScript 的可用性,更适用于工程。

ts 语言的特点 ts 是 typescript 的超集,是一个可选的,静态的类型系统。ts 不参与任何运行时的类型检查

  1. 超集:整数,正整数,整数是正整数的超集

  2. 类型系统:对代码中所有的标识符 (变量,函数,参数,返回值) 进行类型检查

  3. 可选的:我们对于类型检查可以选择用也可以不用

  4. 静态的:无论是浏览器环境还是 node 环境,无法直接识别 ts 代码。类习惯检查的时间,是在编译的 时候而非运行时

    tsc:ts 编译器,将 ts 转换为 es

# TypeScript 特点

支持最新的 JavaScript 新特特性
支持代码静态检查
支持 Java 等后端语言中的特性 (枚举、泛型、类型转换、命名空间、声明文件、类、接口等)

# 官方 playground & 练习

官方也提供了一个在线开发 TypeScript 的云环境 —— Playground 。可以通过它来练习。

typescript 练习题

# 基础数据类型

# 内置类型

typescript 中的基础数据类型基本都是支持的,并且可以通过 :类型 的方式指定类型

let str: string = "jimmy";
let num: number = 24;
let bool: boolean = false;
let u: undefined = undefined;
let n: null = null;
let obj: object = {x:




    
 1};
let sym: symbol = Symbol("me");

# 注意点

# null 和 undefined

默认情况下 null undefined 是所有类型的子类型。 就是说你可以把 null undefined 赋值给其他类型。除非在 tsconfig.typescripton 指定了 "strictNullChecks":true null undefined 只能赋值给 void 和它们各自的类型。

//null 和 undefined 赋值给 string
let str:string = "666";
str = null
str= undefined
                                 
//null 和 undefined 赋值给 number
let num:number = 666;
num = null
num= undefined
                                 
//null 和 undefined 赋值给 object
let obj:object ={};
obj = null
obj= undefined
                                 
//null 和 undefined 赋值给 Symbol
let sym: symbol = Symbol("me"); 
sym = null
sym= undefined
                                 
//null 和 undefined 赋值给 boolean
let isDone: boolean = false;
isDone = null
isDone= undefined

"strictNullChecks":true

报错

# 其他类型

# Array

对数组类型的定义有两种方式:

let arr:string[] = ["1","2"];
let arr2:Array<string> = ["1","2"];

定义联合类型数组

let arr:(number | string)[];
// 表示定义了一个名称叫做 arr 的数组,
// 这个数组中将来既可以存储数值类型的数据,也可以存储字符串类型的数据
arr = [1, 'b', 2, 'c'];

定义指定对象成员的数组:

//interface 是接口,感觉就是定义一种数据类型
interface




    
 Person{
    name:string,
    age:number
}
let arr:Person[]=[{name:'tom',age:22}]

# 函数

# 函数声明

可以限制参数的类型,返回值类型

function sum(x: number, y: number): number {
    return x + y;
}

# 函数表达式

let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
    return x + y;
};

对应的 typescript

let mySum = function (x, y) {
    return x + y;
};

# 用接口定义函数类型

interface SearchFunc{
  (source: string, subString: string): boolean;
}

采用函数表达式接口定义函数的方式时,对等号左侧进行类型限制,可以保证以后对函数名赋值时保证参数个数、参数类型、返回值类型不变。

# 可选参数

function buildName(firstName: string, lastName?: string) {
    if (lastName) {
        return firstName + ' ' + lastName;
    } else {
        return firstName;
    }
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');// 此时 lastName=undefined

注意点:可选参数后面不允许再出现必需参数

# 参数默认值

function buildName(firstName: string, lastName: string = 'Cat') {
    return firstName + ' ' + lastName;
}
let




    
 tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');

# 剩余参数

function push(array: any[], ...items: any[]) {
    items.forEach(function(item) {
        array.push(item);
    });
}
let a = [];
push(a, 1, 2, 3);

注意:

noImplicitAny :设置为 true 的时候,此处会报错 Parameter 'a' implicitly has an 'any' type.

# 函数重载

由于 JavaScript 是一个动态语言,我们通常会使用不同类型的参数来调用同一个函数,该函数会根据不同的参数而返回不同的类型的调用结果:

function add(x, y) {
 return x + y;
}
add(1, 2); // 3
add("1", "2"); //"12"

由于 TypeScript 是 JavaScript 的超集,因此以上的代码可以直接在 TypeScript 中使用,但当 TypeScript 编译器开启 noImplicitAny 的配置项时,以上代码会提示以下错误信息:

Parameter 'x' implicitly has an 'any' type.
Parameter 'y' implicitly has an 'any' type.

该信息告诉我们参数 x 和参数 y 隐式具有 any 类型。为了解决这个问题,我们可以为参数设置一个类型。因为我们希望 add 函数同时支持 string 和 number 类型,因此我们可以定义一个 string | number 联合类型,同时我们为该联合类型取个别名:

type Combinable = string | number;

在定义完 Combinable 联合类型后,我们来更新一下 add 函数:

function add(a: Combinable, b: Combinable) {
    if (typeof a === 'string' || typeof b === 'string') {
     return a.toString() + b.toString();
    }
    return a + b;
}

add 函数的参数显式设置类型之后,之前错误的提示消息就消失了。那么此时的 add 函数就完美了么,我们来实际测试一下:

const result = add('Semlinker', ' Kakuqo');
result.split(' ');

在上面代码中,我们分别使用 'Semlinker' ' Kakuqo' 这两个字符串作为参数调用 add 函数,并把调用结果保存到一个名为 result 的变量上,这时候我们想当然的认为此时 result 的变量的类型为 string,所以我们就可以正常调用字符串对象上的 split 方法。但这时 TypeScript 编译器又出现以下错误信息了:

Property 'split' does not exist on type 'string | number'.
  Property 'split' does not exist on type 'number'.

很明显 number 类型的对象上并不存在 split 属性。问题又来了,那如何解决呢?这时我们就可以利用 TypeScript 提供的函数重载特性。

函数重载或方法重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力。 要解决前面遇到的问题,方法就是为同一个函数提供多个函数类型定义来进行函数重载,编译器会根据这个列表去处理函数的调用。

type Types = number | string
function add(a:number,b:number):number;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
function add(a:Types, b:Types) {
  if (typeof a === 'string' || typeof b === 'string') {
    return a.toString() + b.toString();
  }
  return a + b;
}
const result = add('Semlinker', ' Kakuqo');
result.split(' ');

在以上代码中,我们为 add 函数提供了多个函数类型定义,从而实现函数的重载。之后,可恶的错误消息又消失了,因为这时 result 变量的类型是 string 类型。

# Tuple (元组)

# 元祖定义

众所周知,数组一般由同种类型的值组成,但有时我们需要在单个变量中存储不同类型的值,这时候我们就可以使用元组。在 JavaScript 中是没有元组的,元组是 TypeScript 中特有的类型,其工作方式类似于数组。

元组最重要的特性是可以限制 数组元素的个数和类型 ,它特别适合用来实现多值返回。

元祖用于保存定长定数据类型的数据

let x: [string, number]; 
// 类型必须匹配且个数必须为 2
                                 
x = ['hello', 10]; // OK 
x = ['hello', 10,10]; // Error 
//Type '[string, number, number]' is not assignable to type '[string, number]'.
  Source has 3 element(s) but target allows only 2.
x = [10, 'hello']; // Error
Type 'number' is not assignable to type 'string'.
Type 'string' is not assignable to type 'number'.

注意,元组类型只能表示一个已知元素数量和类型的数组,长度已指定,越界访问会提示错误。如果一个数组中可能有多种类型,数量和类型都不确定,那就直接 any[]

# 元祖类型的解构赋值

我们可以通过下标的方式来访问元组中的元素,当元组中的元素较多时,这种方式并不是那么便捷。其实元组也是支持解构赋值的:

let




    
 employee: [number, string] = [1, "Semlinker"];
let [id, username] = employee;
console.log(`id: ${id}`);
console.log(`username: ${username}`);

以上代码成功运行后,控制台会输出以下消息:

id: 1
username: Semlinker

# 元组类型的可选元素

与函数签名类型,在定义元组类型时,我们也可以通过 ? 号来声明元组类型的可选元素,具体的示例如下:

let optionalTuple: [string, boolean?];
optionalTuple = ["Semlinker", true];
console.log(`optionalTuple : ${optionalTuple}`); //ok
optionalTuple = ["Kakuqo"]
console.log(`optionalTuple : ${optionalTuple}`); //ok
optionalTuple = ["Kakuqo",12]; //Error

在上面代码中,我们定义了一个名为 optionalTuple 的变量,该变量的类型要求包含一个必须的字符串属性和一个可选布尔属性,该代码运行后,控制台会输出以下内容:

optionalTuple : Semlinker,true
optionalTuple : Kakuqo
Type 'number' is not assignable to type 'boolean | undefined'.

那么在实际工作中,声明可选的元组元素有什么作用?这里我们来举一个例子,在三维坐标轴中,一个坐标点可以使用 (x, y, z) 的形式来表示,对于二维坐标轴来说,坐标点可以使用 (x, y) 的形式来表示,而对于一维坐标轴来说,只要使用 (x) 的形式来表示即可。针对这种情形,在 TypeScript 中就可以利用元组类型可选元素的特性来定义一个元组类型的坐标点,具体实现如下:

type Point = [number, number?, number?];
                                 
const x: Point = [10]; // 一维坐标点
const xy: Point = [10, 20]; // 二维坐标点
const xyz: Point = [10, 20, 10]; // 三维坐标点
                                 
console.log(x.length); // 1
console.log(xy.length); // 2
console.log(xyz.length); // 3

# 元组类型的剩余元素

元组类型里最后一个元素可以是剩余元素,形式为 ...X ,这里 X 是数组类型。 剩余元素代表元组类型是开放的,可以有零个或多个额外的元素。 例如, [number, ...string[]] 表示带有一个 number 元素和任意数量 string 类型元素的元组类型。为了能更好的理解,我们来举个具体的例子:

type RestTupleType = [number, ...string[]];
let restTuple: RestTupleType = [666, "Semlinker", "Kakuqo", "Lolo"];
console.log(restTuple[0]);
console.log(restTuple[1]);

# 只读的元组类型

TypeScript 3.4 还引入了对只读元组的新支持。我们可以为任何元组类型加上 readonly 关键字前缀,以使其成为只读元组。具体的示例如下:

const point: readonly [number, number] = [10, 20];

在使用 readonly 关键字修饰元组类型之后,任何企图修改元组中元素的操作都会抛出异常:

// Cannot assign to '0' because it is a read-only property.
point[0] = 1;
// Property 'push' does not exist on type 'readonly [number, number]'.
point.push(0);
// Property 'pop' does not exist on type 'readonly [number, number]'.
point.pop();
// Property 'splice' does not exist on type 'readonly [number, number]'.
point.splice(1, 1);

# void

void 表示没有任何类型,和其他类型是平等关系,不能直接赋值:

let a: void; 
let b: number = a; // Error

你只能为它赋予 null undefined (在 strictNullChecks 未指定为 true 时)。声明一个 void 类型的变量没有什么大用,我们一般也只有在函数没有返回值时去声明。

值得注意的是,方法没有返回值将得到 undefined ,但是我们需要定义成 void 类型,而不是 undefined 类型。否则将报错:

function fun(): undefined {
  console.log("this is TypeScript");
};
fun(); // Error

# never

never 类型表示的是那些永不存在的值的类型。

值会永不存在的两种情况:

  1. 如果一个函数执行时抛出了 异常 ,那么这个函数永远不存在返回值(因为抛出异常会直接中断程序运行,这使得程序运行不到返回值那一步,即具有不可达的终点,也就永不存在返回了);
  2. 函数中执行无限循环的代码( 死循环 ),使得程序永远无法运行到函数返回值那一步,永不存在返回。
// 异常
function err(msg: string): never { // OK
  throw new Error(msg); 
}
                                 
// 死循环




    
function loopForever(): never { // OK
  while (true) {};
}

never 类型同 null undefined 一样,也是任何类型的子类型,也可以赋值给任何类型。

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

let ne: never;
let nev: never;
let an: any;
                                 
ne = 123; // Error
ne = nev; // OK
ne = an; // Error
ne = (() => { throw new Error("异常"); })(); // OK
ne = (() => { while(true) {} })(); // OK

在 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;
  }
}

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

type Foo = string | number | boolean;

然而他忘记同时修改 controlFlowAnalysisWithNever 方法中的控制流程,这时候 else 分支的 foo 类型会被收窄为 boolean 类型,导致无法赋值给 never 类型,这时就会产生一个编译错误。 Type 'boolean' is not assignable to type 'never'.

通过这个方式,我们可以确保 controlFlowAnalysisWithNever 方法总是穷尽了 Foo 的所有可能类型。 通过这个示例,我们可以得出一个结论: 使用 never 避免出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码。

# any

在 TypeScript 中,任何类型都可以被归为 any 类型。这让 any 类型成为了类型系统的顶级类型.

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

let a: string = 'seven';
a = 7;
// TS2322: Type 'number' is not assignable to type 'string'.

但如果是 any 类型,则允许被赋值为任意类型。

let a: any = 666;
a = "Semlinker";
a = false;
a = 66
a = undefined
a = null
a = []
a = {}

在 any 上访问任何属性都是允许的,也允许调用任何方法.

let anyThing: any = 'hello';
console.log(anyThing.myName);
console.log(anyThing.myName.firstName);
let anyThing: any = 'Tom';
anyThing.setName('Jerry');
anyThing.setName('Jerry').sayHello();
anyThing.myName.setFirstName('Cat');

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

let something;
something = 'seven';
something = 7;
something.setName('Tom');

等价于

let something: any;
something = 'seven';
something = 7;
something.setName('Tom');

在许多场景下,这太宽松了。使用 any 类型,可以很容易地编写类型正确但在运行时有问题的代码。如果我们使用 any 类型,就无法使用 TypeScript 提供的大量的保护机制。

为了解决 any 带来的问题,TypeScript 引入了 unknown 类型。

# unknown

unknown any 一样,所有类型都可以分配给 unknown :

let notSure: unknown = 4;
notSure = "maybe a string instead"; // OK
notSure = false; // OK

unknown any 的最大区别是: 任何类型的值可以赋值给 any ,同时 any 类型的值也可以赋值给任何类型。 unknown 任何类型的值都可以赋值给它,但它只能赋值给 unknown 和 `any

let notSure: unknown = 4;
let uncertain: any = notSure; // OK
                                 
let




    
 notSure: any = 4;
let uncertain: unknown = notSure; // OK
                                 
let notSure: unknown = 4;
let uncertain: number = notSure; // Error

如果不缩小类型,就无法对 unknown 类型执行任何操作:

function getDog() {
 return '123'
}
 
const dog: unknown = {hello: getDog};
dog.hello(); // Error

这种机制起到了很强的预防性,更安全,这就要求我们必须缩小类型,我们可以使用 typeof 类型断言 等方式来缩小未知范围:

function getDogName() {
 let x: unknown;
 return x;
};
const dogName = getDogName();
// 直接使用
const upName = dogName.toLowerCase(); // Error
// typeof
if (typeof dogName === 'string') {
  const upName = dogName.toLowerCase(); // OK
}
// 类型断言 
const upName = (dogName as string).toLowerCase(); // OK

# Number、String、Boolean、Symbol

首先,我们来回顾一下初学 TypeScript 时,很容易和原始类型 number、string、boolean、symbol 混淆的首字母大写的 Number、String、Boolean、Symbol 类型,后者是相应原始类型的 包装对象 ,姑且把它们称之为对象类型。

从类型兼容性上看,原始类型兼容对应的对象类型,反过来对象类型不兼容对应的原始类型。

下面我们看一个具体的示例:

let num: number;
let Num: Number;
Num = num; // ok
num = Num; // 报错

在示例中的第 3 行,我们可以把 number 赋给类型 Number,但在第 4 行把 Number 赋给 number 就会提示 错误。

因此,我们需要铭记不要使用对象类型来注解值的类型,因为这没有任何意义。

# object、Object 和 {}

另外,object(首字母小写,以下称 “小 object”)、Object(首字母大写,以下称 “大 Object”)和 {}(以下称 “空对象”)

小 object 代表的是所有非原始类型,也就是说我们不能把 number、string、boolean、symbol 等 原始类型赋值给 object。在严格模式下, null undefined 类型也不能赋给 object。

JavaScript 中以下类型被视为原始类型: string boolean number bigint symbol null undefined

下面我们看一个具体示例:

let




    
 lowerCaseObject: object;
lowerCaseObject = 1; // error
lowerCaseObject = 'a'; // error
lowerCaseObject = true; // error
lowerCaseObject = null; // error(strictNullChecks)
lowerCaseObject = undefined; // error(strictNullChecks)
lowerCaseObject = {}; // ok

在示例中的第 2~6 行都会提示错误,但是我们在第 7 行把一个空对象赋值给 object 后,则可以通过静态类型检测。

大 Object 代表所有拥有 toString、hasOwnProperty 方法的类型,所以所有原始类型、非原始类型都可以赋给 Object。同样,在严格模式下,null 和 undefined 类型也不能赋给 Object。

下面我们也看一个具体的示例:

let upperCaseObject: Object;
upperCaseObject = 1; // ok
upperCaseObject = 'a'; // ok
upperCaseObject = true; // ok
upperCaseObject = null; // error
upperCaseObject = undefined; // error
upperCaseObject = {}; // ok

在示例中的第 2 到 4 行、第 7 行都可以通过静态类型检测,而第 5~6 行则会提示错误。

从上面示例可以看到,大 Object 包含原始类型,小 object 仅包含非原始类型,所以大 Object 似乎是小 object 的父类型。实际上,大 Object 不仅是小 object 的父类型,同时也是小 object 的子类型。

下面我们还是通过一个具体的示例进行说明。

let lowerCaseObject: object;
let upperCaseObject: Object;
lowerCaseObject = {}; // ok
upperCaseObject = {}; // ok
type isLowerCaseObjectExtendsUpperCaseObject = object extends Object ? true : false; // true
type isUpperCaseObjectExtendsLowerCaseObject = Object extends object ? true : false; // true
upperCaseObject = lowerCaseObject; // ok
lowerCaseObject = upperCaseObject; // ok

在示例中的第 1 行和第 2 行返回的类型都是 true,第 3 行和第 4 行的 upperCaseObject 与 lowerCaseObject 可以互相赋值。

注意:尽管官方文档说可以使用小 object 代替大 Object,但是我们仍要明白大 Object 并不完全等价于小 object。

{} 空对象类型和大 Object 一样,也是表示原始类型和非原始类型的集合,并且在严格模式下,null 和 undefined 也不能赋给 {} ,如下示例:

let ObjectLiteral: {};
ObjectLiteral = 1; // ok
ObjectLiteral = 'a'; // ok
ObjectLiteral = true; // ok
ObjectLiteral = null; // error
ObjectLiteral = undefined; // error
ObjectLiteral = {}; // ok
type isLiteralCaseObjectExtendsUpperCaseObject = {} extends Object ? true : false; // true
type isUpperCaseObjectExtendsLiteralCaseObject = Object extends {} ? true : false; // true
upperCaseObject = ObjectLiteral;
ObjectLiteral =




    
 upperCaseObject;

在示例中的第 8 行和第 9 行返回的类型都是 true,第 10 行和第 11 行的 ObjectLiteral 与 upperCaseObject 可以互相赋值,第 2~4 行、第 7 行的赋值操作都符合静态类型检测;而第 5 行、第 6 行则会提示错误。

综上结论:{}、大 Object 是比小 object 更宽泛的类型(least specific),{} 和大 Object 可以互相代替,用来表示原始类型(null、undefined 除外)和非原始类型;而小 object 则表示非原始类型。

# 类型推断

{
  let str: string = 'this is string';
  let num: number = 1;
  let bool: boolean = true;
}
{
  const str: string = 'this is string';
  const num: number = 1;
  const bool: boolean = true;
}

看着上面的示例,可能你已经在嘀咕了:定义基础类型的变量都需要写明类型注解,TypeScript 太麻烦了吧?在示例中,使用 let 定义变量时,我们写明类型注解也就罢了,毕竟值可能会被改变。可是,使用 const 常量时还需要写明类型注解,那可真的很麻烦。

实际上,TypeScript 早就考虑到了这么简单而明显的问题。

在很多情况下,TypeScript 会根据上下文环境自动推断出变量的类型,无须我们再写明类型注解。因此,上面的示例可以简化为如下所示内容:

{
  let str = 'this is string'; // 等价
  let num = 1; // 等价
  let bool = true; // 等价
}
{
  const str = 'this is string'; // 不等价
  const num = 1; // 不等价
  const bool = true; // 不等价
}

我们把 TypeScript 这种基于赋值表达式推断类型的能力称之为 类型推断

在 TypeScript 中,具有初始化值的变量、有默认值的函数参数、函数返回的类型都可以根据上下文推断出来。比如我们能根据 return 语句推断函数返回的类型,如下代码所示:

/** 根据参数的类型,推断出返回值的类型也是 number */
  function add1(a: number, b: number) {
    return a + b;
  }
  const x1= add1(1, 1); // 推断出 x1 的类型也是 number
  
  /** 推断参数 b 的类型是数字或者 undefined,返回值的类型也是数字 */
  function add2(a: number, b = 1) {
    return a + b;
  }
   const x2 = add2(1);
   
   const x3 = add2(1, '1'); // Argument of type '"1"' is not assignable to parameter of type 'number | undefined'.

如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查:

let




    
 myFavoriteNumber;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;

# 类型断言

有时候你会遇到这样的情况,你会比 TypeScript 更了解某个值的详细信息。通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。

通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。类型断言好比其他语言里的类型转换,但是不进行特殊的数据检查和解构。它没有运行时的影响,只是在编译阶段起作用。

TypeScript 类型检测无法做到绝对智能,毕竟程序不能像人一样思考。有时会碰到我们比 TypeScript 更清楚实际类型的情况,比如下面的例子:

const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = arrayNumber.find(num => num > 2);

会报如下错误

Type 'number | undefined' is not assignable to type 'number'.
  Type 'undefined' is not assignable to type 'number'.

其中,greaterThan2 一定一个数字(确切地讲是 3),因为 arrayNumber 中明显有大于 2 的成员,但静态类型对运行时的逻辑无能为力。

在 TypeScript 看来,greaterThan2 的类型既可能是数字,也可能是 undefined,所以上面的示例中提示了一个 ts (2322) 错误,此时我们不能把类型 undefined 分配给类型 number。

不过,我们可以使用一种笃定的方式 —— 类型断言 (类似仅作用在类型层面的强制类型转换)告诉 TypeScript 按照我们的方式做类型检查。

比如,我们可以使用 as 语法做类型断言,如下代码所示:

const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = arrayNumber.find(num => num > 2) as number;

# 语法

// 尖括号 语法
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
                                 
//as 语法
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

以上两种方式基本没有任何区别

# 非空断言

在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符 ! 可以用于断言操作对象是非 null 和非 undefined 类型。 具体而言,x! 将从 x 值域中排除 null 和 undefined 。

具体看以下示例:

let mayNullOrUndefinedOrString: null | undefined | string;
mayNullOrUndefinedOrString!.toString(); // ok
mayNullOrUndefinedOrString.toString(); //error Object is possibly 'null' or 'undefined'.
type NumGenerator = () => number;
                                 
function myFunc(numGenerator: NumGenerator | undefined) {
  const num1 = numGenerator(); // Error Cannot invoke an object which is possibly 'undefined'.
  const num2 = numGenerator!(); //OK
}

# 确定赋值断言

允许在实例属性和变量声明后面放置一个 ! 号,从而告诉 TypeScript 该属性会被明确地赋值。为了更好地理解它的作用,我们来看个具体的例子:

let




    
 x: number;
initialize();
console.log(2 * x); // Error Variable 'x' is used before being assigned.
function initialize() {
  x = 10;
}

很明显该异常信息是说变量 x 在赋值前被使用了,要解决该问题,我们可以使用确定赋值断言:

let x!: number;
initialize();
console.log(2 * x); // Ok
                                 
function initialize() {
  x = 10;
}

通过 let x!: number; 确定赋值断言,TypeScript 编译器就会知道该属性会被明确地赋值。

# 字面量类型

在 TypeScript 中,字面量不仅可以表示值,还可以表示类型,即所谓的字面量类型。

目前,TypeScript 支持 3 种字面量类型:字符串字面量类型、数字字面量类型、布尔字面量类型,对应的字符串字面量、数字字面量、布尔字面量分别拥有与其值一样的字面量类型,具体示例如下:

let specifiedStr: 'this is string' = 'this is string';
let specifiedNum: 1 = 1;
let specifiedBoolean: true = true;

比如 'this is string' (这里表示一个字符串字面量类型)类型是 string 类型(确切地说是 string 类型的子类型),而 string 类型不一定是 'this is string'(这里表示一个字符串字面量类型)类型,如下具体示例:

let specifiedStr: 'this is string' = 'this is string';
let str: string = 'any string';
specifiedStr = str; //  类型 '"string"' 不能赋值给类型 'this is string'
specifiedStr = 'this is string' ;//ok
str = specifiedStr; // ok

比如说我们用 “马” 比喻 string 类型,即 “黑马” 代指 'this is string' 类型,“黑马” 肯定是 “马”,但 “马” 不一定是 “黑马”,它可能还是 “白马”“灰马”。因此,'this is string' 字面量类型可以给 string 类型赋值,但是 string 类型不能给 'this is string' 字面量类型赋值,这个比喻同样适合于形容数字、布尔等其他字面量和它们父类的关系。

# 字符串字面量类型

一般来说,我们可以使用一个字符串字面量类型作为变量的类型,如下代码所示:

let hello: 'hello' = 'hello';
hello = 'hi'; // error

实际上,定义单个的字面量类型并没有太大的用处,它真正的应用场景是可以把多个字面量类型组合成一个联合类型(后面会讲解),用来描述拥有明确成员的实用的集合。

如下代码所示,我们使用字面量联合类型描述了一个明确、可 'up' 可 'down' 的集合,这样就能清楚地知道需要的数据结构了。

type Direction = 'up' | 'down';
                                 
function move(dir: Direction) {
  // ...
}
move('up'); // ok
move('right'); // Argument of type '"right"' is not assignable to parameter of type 'Direction'

通过使用字面量类型组合的联合类型,我们可以限制函数的参数为指定的字面量类型集合,然后编译器会检查参数是否是指定的字面量类型集合里的成员。

因此,相较于使用 string 类型,使用字面量类型(组合的联合类型)可以将函数的参数限定为更具体的类型。这不仅提升了程序的可读性,还保证了函数的参数类型,可谓一举两得。

# 数字字面量类型及布尔字面量类型

数字字面量类型和布尔字面量类型的使用与字符串字面量类型的使用类似,我们可以使用字面量组合的联合类型将函数的参数限定为更具体的类型,比如声明如下所示的一个类型 Config:

interface Config {
    size: 'small' | 'big';
    isEnable:  true | false;
    margin: 0 | 2 | 4;
}

在上述代码中,我们限定了 size 属性为字符串字面量类型'small' | 'big',isEnable 属性为布尔字面量类型 true | false(布尔字面量只包含 true 和 false,true | false 的组合跟直接使用 boolean 没有区别),margin 属性为数字字面量类型 0 | 2 | 4。

# let 和 const 分析

我们先来看一个 const 示例,如下代码所示:

const str = 'this is string'; // str: 'this is string'
const num = 1; // num: 1
const bool = true; // bool: true

在上述代码中,我们将 const 定义为一个不可变更的常量,在缺省类型注解的情况下,TypeScript 推断出它的类型直接由赋值字面量的类型决定,这也是一种比较合理的设计。

接下来我们看看如下所示的 let 示例:

let str = 'this is string'; // str: string
let num = 1; // num: number
let bool = true; // bool: boolean

在上述代码中,缺省显式类型注解的可变更的变量的类型转换为了赋值字面量类型的父类型,比如 str 的类型是 'this is string' 类型(这里表示一个字符串字面量类型)的父类型 string,num 的类型是 1 类型的父类型 number。

这种设计符合编程预期,意味着我们可以分别赋予 str 和 num 任意值(只要类型是 string 和 number 的子集的变量):

str = 'any string';
num = 2;
bool = false;

我们将 TypeScript 的字面量子类型转换为父类型的这种设计称之为 "literal widening",也就是字面量类型的拓宽,比如上面示例中提到的字符串字面量类型转换成 string 类型,下面我们着重介绍一下。

# 类型拓宽 (Type Widening)

所有通过 let 或 var 定义的变量、函数的形参、对象的非只读属性,如果满足指定了初始值且未显式添加类型注解的条件,那么它们推断出来的类型就是指定的初始值字面量类型拓宽后的类型,这就是字面量类型拓宽。

下面我们通过字符串字面量的示例来理解一下字面量类型拓宽:

let str = 'this is string'; // 类型是 string
  let strFun = (str = 'this is string') => str; // 类型是 (str?: string) => string;
  const specifiedStr = 'this is string'; // 类型是 'this is string'
  let str2 = specifiedStr; // 类型是'string'
  let strFun2 = (str = specifiedStr) => str; // 类型是 (str?: string) => string;

因为第 1~2 行满足了 let、形参且未显式声明类型注解的条件,所以变量、形参的类型拓宽为 string(形参类型确切地讲是 string | undefined)。

因为第 3 行的常量不可变更,类型没有拓宽,所以 specifiedStr 的类型是 'this is string' 字面量类型。

第 4~5 行,因为赋予的值 specifiedStr 的类型是字面量类型,且没有显式类型注解,所以变量、形参的类型也被拓宽了。其实,这样的设计符合实际编程诉求。我们设想一下,如果 str2 的类型被推断为 'this is string',它将不可变更,因为赋予任何其他的字符串类型的值都会提示类型错误。

基于字面量类型拓宽的条件,我们可以通过如下所示代码添加显示类型注解控制类型拓宽行为。

const specifiedStr: 'this is string' = 'this is string'; // 类型是 '"this is string"'
let str2 = specifiedStr; // 即便使用 let 定义,类型是 'this is string'

实际上,除了字面量类型拓宽之外,TypeScript 对某些特定类型值也有类似 "Type Widening" (类型拓宽)的设计,下面我们具体来了解一下。

比如对 null 和 undefined 的类型进行拓宽,通过 let、var 定义的变量如果满足未显式声明类型注解且被赋予了 null 或 undefined 值,则推断出这些变量的类型是 any:

let x = null; // 类型拓宽成 any
let y = undefined; // 类型拓宽成 any
/** ----- 分界线 ------- */
const z = null; // 类型是 null
/** ----- 分界线 ------- */
let




    
 anyFun = (param = null) => param; // 形参类型是 null
let z2 = z; // 类型是 null
let x2 = x; // 类型是 null
let y2 = y; // 类型是 undefined

注意:在严格模式下,一些比较老的版本中(2.0)null 和 undefined 并不会被拓宽成 “any”。

为了更方便的理解类型拓宽,下面我们举个例子,更加深入的分析一下

假设你正在编写一个向量库,你首先定义了一个 Vector3 接口,然后定义了 getComponent 函数用于获取指定坐标轴的值:

interface Vector3 {
  x: number;
  y: number;
  z: number;
}
                                 
function getComponent(vector: Vector3, axis: "x" | "y" | "z") {
  return vector[axis];
}

但是,当你尝试使用 getComponent 函数时,TypeScript 会提示以下错误信息:

let x = "x";
let vec = { x: 10, y: 20, z: 30 };
getComponent(vec, x); // Error Argument of type 'string' is not assignable to parameter of type '"x" | "y" | "z"'.

为什么会出现上述错误呢?通过 TypeScript 的错误提示消息,我们知道是因为变量 x 的类型被推断为 string 类型,而 getComponent 函数期望它的第二个参数有一个更具体的类型。这在实际场合中被拓宽了,所以导致了一个错误。

这个过程是复杂的,因为对于任何给定的值都有许多可能的类型。例如:

const arr = ['x', 1];

上述 arr 变量的类型应该是什么?这里有一些可能性:

  • ('x' | 1)[]
  • ['x', 1]
  • [string, number]
  • readonly [string, number]
  • (string | number)[]
  • readonly (string|number)[]
  • [any, any]
  • any[]

没有更多的上下文,TypeScript 无法知道哪种类型是 “正确的”,它必须猜测你的意图。尽管 TypeScript 很聪明,但它无法读懂你的心思。它不能保证 100% 正确,正如我们刚才看到的那样的疏忽性错误。

在下面的例子中,变量 x 的类型被推断为字符串,因为 TypeScript 允许这样的代码:

let x = 'semlinker';
x = 'kakuqo';
x = 'lolo';

对于 JavaScript 来说,以下代码也是合法的:

let x = 'x';
x = /x|y|z/;
x = ['x', 'y', 'z'];

在推断 x 的类型为字符串时,TypeScript 试图在特殊性和灵活性之间取得平衡。一般规则是,变量的类型在声明之后不应该改变,因此 string 比 string|RegExp 或 string|string [] 或任何字符串更有意义。

TypeScript 提供了一些控制拓宽过程的方法。其中一种方法是使用 const 。如果用 const 而不是 let 声明一个变量,那么它的类型会更窄。事实上,使用 const 可以帮助我们修复前面例子中的错误:

const x = "x"; // type is "x" 
let vec = { x: 10, y: 20, z: 30 };
getComponent(vec, x); // OK

因为 x 不能重新赋值,所以 TypeScript 可以推断更窄的类型,就不会在后续赋值中出现错误。因为字符串字面量型 “x” 可以赋值给 "x"|"y"|"z",所以代码会通过类型检查器的检查。

然而, const 并不是万灵药。对于对象和数组,仍然会存在问题

以下这段代码在 JavaScript 中是没有问题的:

const obj = { 
  x: 1,
}; 
                                 
obj.x = 6; 
obj.x = '6';
                                 
obj.y = 8;
obj.name = 'semlinker';

而在 TypeScript 中,对于 obj 的类型来说,它可以是 {readonly x:1} 类型,或者是更通用的 {x:number} 类型。当然也可能是 {[key: string]: number} 或 object 类型。对于对象,TypeScript 的拓宽算法会将其内部属性视为将其赋值给 let 关键字声明的变量,进而来推断其属性的类型。因此 obj 的类型为 {x:number} 。这使得你可以将 obj.x 赋值给其他 number 类型的变量,而不是 string 类型的变量,并且它还会阻止你添加其他属性。

因此最后三行的语句会出现错误:

const obj = { 
  x: 1,
};
                                 
obj.x = 6; // OK 
                                 
                                 
// Type '"6"' is not assignable to type 'number'.
obj.x = '6'; // Error
                                 
// Property 'y' does not exist on type '{ x: number; }'.
obj.y = 8; // Error
                                 
// Property 'name' does not exist on type '{ x: number; }'.
obj.name = 'semlinker'; // Error

TypeScript 试图在具体性和灵活性之间取得平衡。它需要推断一个足够具体的类型来捕获错误,但又不能推断出错误的类型。它通过属性的初始化值来推断属性的类型,当然有几种方法可以覆盖 TypeScript 的默认行为。一种是提供显式类型注释:

// Type is { x: 1 | 3 | 5; }
const obj: { x: 1 | 3 | 5 } = {
  x: 1 
};

另一种方法是使用 const 断言。不要将其与 let 和 const 混淆,后者在值空间中引入符号。这是一个纯粹的类型级构造。让我们来看看以下变量的不同推断类型:

// Type is { x: number; y: number; }
const obj1 = { 
  x: 1, 
  y: 2 
}; 
                                 
// Type is { x: 1; y: number; }
const obj2 = {
  x: 1 as const,
  y: 2,
}; 
                                 
// Type is { readonly x: 1; readonly y: 2; }
const




    
 obj3 = {
  x: 1, 
  y: 2 
} as const;

当你在一个值之后使用 const 断言时,TypeScript 将为它推断出最窄的类型,没有拓宽。对于真正的常量,这通常是你想要的。当然你也可以对数组使用 const 断言:

// Type is number[]
const arr1 = [1, 2, 3]; 
                                 
// Type is readonly [1, 2, 3]
const arr2 = [1, 2, 3] as const;

既然有类型拓宽,自然也会有类型缩小,下面我们简单介绍一下 Type Narrowing。

# 类型缩小 (Type Narrowing)

在 TypeScript 中,我们可以通过某些操作将变量的类型由一个较为宽泛的集合缩小到相对较小、较明确的集合,这就是 "Type Narrowing"。

比如,我们可以使用类型守卫(后面会讲到)将函数参数的类型从 any 缩小到明确的类型,具体示例如下:

{
  let func = (anything: any) => {
    if (typeof anything === 'string') {
      return anything; // 类型是 string 
    } else if (typeof anything === 'number') {
      return anything; // 类型是 number
    }
    return null;
  };
}

在 VS Code 中 hover 到第 4 行的 anything 变量提示类型是 string,到第 6 行则提示类型是 number。

同样,我们可以使用类型守卫将联合类型缩小到明确的子类型,具体示例如下:

{
  let func = (anything: string | number) => {
    if (typeof anything === 'string') {
      return anything; // 类型是 string 
    } else {
      return anything; // 类型是 number
    }
  };
}

当然,我们也可以通过字面量类型等值判断(===)或其他控制流语句(包括但不限于 if、三目运算符、switch 分支)将联合类型收敛为更具体的类型,如下代码所示:

{
  type Goods = 'pen' | 'pencil' |'ruler';
  const getPenCost = (item: 'pen') => 2;
  const getPencilCost = (item: 'pencil') => 4;
  const getRulerCost = (item: 'ruler') => 6;
  const getCost = (item:




    
 Goods) =>  {
    if (item === 'pen') {
      return getPenCost(item); // item => 'pen'
    } else if (item === 'pencil') {
      return getPencilCost(item); // item => 'pencil'
    } else {
      return getRulerCost(item); // item => 'ruler'
    }
  }
}

在上述 getCost 函数中,接受的参数类型是字面量类型的联合类型,函数内包含了 if 语句的 3 个流程分支,其中每个流程分支调用的函数的参数都是具体独立的字面量类型。

那为什么类型由多个字面量组成的变量 item 可以传值给仅接收单一特定字面量类型的函数 getPenCost、getPencilCost、getRulerCost 呢?这是因为在每个流程分支中,编译器知道流程分支中的 item 类型是什么。比如 item === 'pencil' 的分支,item 的类型就被收缩为 “pencil”。

事实上,如果我们将上面的示例去掉中间的流程分支,编译器也可以推断出收敛后的类型,如下代码所示:

const getCost = (item: Goods) =>  {
    if (item === 'pen') {
      item; // item => 'pen'
    } else {
      item; // => 'pencil' | 'ruler'
    }
  }

一般来说 TypeScript 非常擅长通过条件来判别类型,但在处理一些特殊值时要特别注意 —— 它可能包含你不想要的东西!例如,以下从联合类型中排除 null 的方法是错误的:

const el = document.getElementById("foo"); // Type is HTMLElement | null
if (typeof el === "object") {
  el; // Type is HTMLElement | null
}

因为在 JavaScript 中 typeof null 的结果是 "object" ,所以你实际上并没有通过这种检查排除 null 值。除此之外,falsy 的原始值也会产生类似的问题:

function foo(x?: number | string | null) {
  if (!x) {
    x; // Type is string | number | null | undefined\
  }
}

因为空字符串和 0 都属于 falsy 值,所以在分支中 x 的类型可能是 string 或 number 类型。帮助类型检查器缩小类型的另一种常见方法是在它们上放置一个明确的 “标签”:

interface UploadEvent {
  type: "upload";
  filename: string;
  contents: string;
}
                                 
interface DownloadEvent {
  type: "download";
  filename: string;
}




    
                                 
type AppEvent = UploadEvent | DownloadEvent;
                                 
function handleEvent(e: AppEvent) {
  switch (e.type) {
    case "download":
      e; // Type is DownloadEvent 
      break;
    case "upload":
      e; // Type is UploadEvent 
      break;
  }
}

这种模式也被称为 ” 标签联合 “或” 可辨识联合 “,它在 TypeScript 中的应用范围非常广。

# 联合类型

联合类型表示取值可以为多种类型中的一种,使用 | 分隔每个类型。

let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven'; // OK
myFavoriteNumber = 7; // OK

联合类型通常与 null undefined 一起使用:

const sayHello = (name: string | undefined) => {
  /* ... */
};

例如,这里 name 的类型是 string | undefined 意味着可以将 string undefined 的值传递给 sayHello 函数。

sayHello("semlinker"); 
sayHello(undefined);

通过这个示例,你可以凭直觉知道类型 A 和类型 B 联合后的类型是同时接受 A 和 B 值的类型。此外,对于联合类型来说,你可能会遇到以下的用法:

let num: 1 | 2 = 1;
type EventNames = 'click' | 'scroll' | 'mousemove';

以上示例中的 1 2 'click' 被称为字面量类型,用来约束取值只能是某几个值中的一个。

# 类型别名

类型别名用来给一个类型起个新名字。类型别名常用于联合类型。

type Message = string | string[];
let greet = (message: Message) => {
  // ...
};

注意:类型别名,诚如其名,即我们仅仅是给类型取了一个新的名字,并不是创建了一个新的类型。

# 交叉类型

交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性,使用 & 定义交叉类型。

{
  type




    
 Useless = string & number;
}

很显然,如果我们仅仅把原始类型、字面量类型、函数类型等原子类型合并成交叉类型,是没有任何用处的,因为任何类型都不能满足同时属于多种原子类型,比如既是 string 类型又是 number 类型。因此,在上述的代码中,类型别名 Useless 的类型就是个 never。

交叉类型真正的用武之地就是将多个接口类型合并成一个类型,从而实现等同接口继承的效果,也就是所谓的合并接口类型,如下代码所示:

type IntersectionType = { id: number; name: string; } & { age: number };
  const mixed: IntersectionType = {
    id: 1,
    name: 'name',
    age: 18
  }

在上述示例中,我们通过交叉类型,使得 IntersectionType 同时拥有了 id、name、age 所有属性,这里我们可以试着将合并接口类型理解为求并集。

# 思考

这里,我们来发散思考一下:如果合并的多个接口类型存在同名属性会是什么效果呢?

如果同名属性的类型不兼容,比如上面示例中两个接口类型同名的 name 属性类型一个是 number,另一个是 string,合并后,name 属性的类型就是 number 和 string 两个原子类型的交叉类型,即 never,如下代码所示:

type IntersectionTypeConfict = { id: number; name: string; } 
  & { age: number; name: number; };
  const mixedConflict: IntersectionTypeConfict = {
    id: 1,
    name: 2, //ts (2322) 错误,'number' 类型不能赋给 'never' 类型
    age: 2
  };

此时,我们赋予 mixedConflict 任意类型的 name 属性值都会提示类型错误。而如果我们不设置 name 属性,又会提示一个缺少必选的 name 属性的错误。在这种情况下,就意味着上述代码中交叉出来的 IntersectionTypeConfict 类型是一个无用类型。

如果同名属性的类型兼容,比如一个是 number,另一个是 number 的子类型、数字字面量类型,合并后 name 属性的类型就是两者中的子类型。

如下所示示例中 name 属性的类型就是数字字面量类型 2,因此,我们不能把任何非 2 之外的值赋予 name 属性。

type IntersectionTypeConfict = { id: number; name: 2; } 
  & { age: number; name: number; };
                                 
  let mixedConflict: IntersectionTypeConfict = {
    id: 1,
    name: 2, // ok
    age: 2
  };
  mixedConflict = {
    id: 1,
    name: 22, // '22' 类型不能赋给 '2' 类型
    age: 2
  };

那么如果同名属性是非基本数据类型的话,又会是什么情形。我们来看个具体的例子:

interface A {
  x:{d:true},
}
interface B {
  x:{e:string},
}
interface C {
  x:{f:number},
}
type ABC = A & B & C
let abc:ABC = {
  x:{
    d:true,
    e:'',
    f:666
  }
}

以上代码成功运行后,会输出以下结果:

[LOG]: {
  "x": {
    "d": true,
    "e": "",
    "f": 666