JavaScript之TypeScript

本文最后更新于 2025年7月30日 下午

进阶部分:http://ts.xcatliu.com/advanced/index.html。

基础部分:
(参考:http://ts.xcatliu.com/basics/index.html)

原始数据类型

JS 的数据类型两大类:原始数据类型和对象数据类型。

原始数据类型共七种:NullUndefinedBooleanNumberStringSymbolBigInt

这里主要介绍前五种原始数据类型在 TS 中的应用。

布尔值

使用 boolean 来定义布尔值类型。直接调用 Boolean() 返回 Boolean 类型。但是,使用构造函数 new Boolean() 创造的对象不是布尔值,而是 Boolean 对象

这方面来说,其他类型的构造函数同理(除 null 和 undefined)。

1
2
3
let isDone: boolean = false;
let createdByBoolean: boolean = Boolean(1);
let createdBynewBoolean: boolean = new Boolean(1); // Type 'Boolean' is not assignable to type 'boolean'

数值

使用 number 来定义数值类型。

1
2
3
4
5
6
let decLiteral: number = 6;
let hexLiteral: number = 0xf00d; // ES6 中的二进制表示法,会被编译为十进制数字。
let binaryLinteral: number = 0b1010; // ES6 中的八进制表示法,会被编译为十进制数字。
let octalLiteral: number = 0o744;
let notANumber: number = NaN;
let infinityNumber: number = Infinity;

字符串

使用 string 来定义字符串类型。

1
2
3
4
5
let myName: string = "Tom";
let myAge: number = 25;
let sentence: string = `Hello, I am ${myName}.I will be ${
myAge + 1
} years old tomorrow.`;

空值

JS 中没有空值(Void)的概念,在 TS 中可用 void 表示没有任何返回值的函数。

1
2
3
4
function alertName(): void {
alert("Now is alerting!");
}
let unusable: void = undefined;

NullUndefined

使用 null 和 undefined 来定义这两个类型。

void、null 和 undefined, 这三个类型的变量,都不能赋值给其他类型的变量。

1
2
3
4
5
6
7
let u: undefined = undefined;
let n: null = null;
let v: void;
let num: number = undefined;
let num1: number = u; // Type 'undefined' is not assignable to type 'number'
let num2: number = n; // Type 'null' is not assignable to type 'number'
let num4: number = v; // Type 'void' is not assignable to type 'number'

任意值

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

什么是任意值类型

如果是普通类型,在赋值过程中改变类型是不被允许的。如果是 any 类型,则允许被赋值为任意类型。

1
2
3
4
let myNum1: string = "seven";
let myNum2: any = "seven";
myNum1 = 7; // error TS2322: Type 'number' is not assignable to type 'string'
myNum2 = 7;

任意值的属性和方法

在任意值上访问任何属性和调用任何方法但是允许的。

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

1
2
3
4
5
6
let anyThing: any = "hello";
console.log(anyThing.myName); // undefined
console.log(anyThing.myName.firstName); // Uncaught TypeError: Cannot read properties of undefined (reading 'firstName')
anyThing.setName("Jerry");
anyThing.setName("Jerry").sayHello();
anyThing.myName.setFirstName("Tom");

未声明类型的变量

如果在声明变量时未指定其类型,则它会被识别为任意类型。

1
2
3
4
let something;
something = "seven";
something = 7;
something.setName("Tom");

类型推论

如果没有明确的指定类型,那么 TS 会按照类型推论的规则推断出一个类型。

什么是类型推论

TS 会在没有明确的指定类型的时候推测出一个类型,这就是类型推论。

如果定义时没有赋值,不管之后有无赋值,该变量都会被推断为 any 类型而不被类型检查。

1
2
3
4
5
6
7
let favoriteNum1 = "seven";
let favoriteNum2: string = "seven"; // 这两个赋值是等价的
let favoriteNum;
favoriteNum1 = 7; // error TS2322: Type 'number' is not assignable to type 'string'
favoriteNum2 = 7; // error TS2322: Type 'number' is not assignable to type 'string'
favoriteNum = "seven"; // 没问题
favoriteNum = 7; // 没问题

联合类型

联合类型表示取值可以为多种类型中的一种。

简单的例子

联合类型使用 | 分隔每个类型。

下面的 let myFavoriteNumber: string | number 的含义是,允许 myFavoriteNumber 的类型是 string 或者 number,但是不能是其他类型。

1
2
3
4
5
6
let myFavoriteNumber: string | number;
myFavoriteNumber = "seven";
myFavoriteNumber = 7;
myFavoriteNumber = true;
// error TS2322: Type 'boolean' is not assignable to type 'string | number'.
// Type 'boolean' is not assignable to type 'number'.

联合类型的属性或方法

当 TS 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法。

1
2
3
4
5
6
7
8
9
10
function getString(something: string | number): string {
return something.toString();
}
// 没问题

function getLength(something: string | number): number {
return something.length;
}
// error TS2339: Property 'length' does not exist on type 'string | number'
// Property 'length' does not exist on type 'number'

联合类型的变量在被赋值的时候,会根据类型推论的规则推断出一个类型。

1
2
3
4
5
let myFavoriteNumber: string | number;
myFavoriteNumber = "seven";
console.log(myFavoriteNumber.length); // 5
myFavoriteNumber = 7;
console.log(myFavoriteNumber.length); // error TS2339: Property 'length' does not exist on type 'number'

接口

在 TS 中,我们使用接口来定义对象的类型。

什么是接口

接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体的行动需要类(classes)去实现(implement)。

TS 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。

简单的例子

接口一般首字母大写。定义的变量比接口少或是多了,都是不被允许的。

赋值的时候,变量的形状必须和接口的形状保持一致。少了或多了一些属性都是不允许的。

1
2
3
4
5
6
7
8
interface Person {
name: string;
age: number;
}
let tomPerson: Person = {
name: "Tom",
age: 25,
};

可选属性

不希望完全匹配一个形状,可以用可选属性。

可选属性的含义是该属性可以不存在,但仍然不允许添加未定义的属性。

1
2
3
4
5
6
7
interface Person1 {
name: string;
age?: number;
}
let tom1: Person1 = {
name: "Tom",
};

任意属性

希望接口允许有任意的属性,可以用任意属性。

使用 [propName: string] 定义任意属性取 string 类型的值。

1
2
3
4
5
6
7
8
9
interface Person2 {
name: string;
age?: number;
[propName: string]: any;
}
let tom2: Person2 = {
name: "Tom",
gender: "male",
};

一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集。

一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,可在任意属性中使用联合类型。

如果同时存在任意属性和可选属性,那么任意属性的数据类型要带 undefined

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
interface Person3 {
name: string;
age?: number; // 类型“number | undefined”的属性“age”不能赋给“string”索引类型“string”。ts(2411)
[propName: string]: string;
}
let tom3: Person3 = {
name: "Tom",
age: 25,
gender: "male",
};
// 不能将类型“{ name: string; age: number; gender: string; }”分配给类型“Person3”。
// 属性“age”与索引签名不兼容。
// 不能将类型“number”分配给类型“string”。ts(2322)
interface Person4 {
name: string;
age?: number; // 类型“number | undefined”的属性“age”不能赋给“string”索引类型“string | number”。ts(2411)
[propName: string]: string | number;
}
let tom4: Person4 = {
name: "Tom",
age: 25,
gender: "male",
};
interface Persons {
name: string;
age?: number;
[propName: string]: string | number | undefined;
}
let toms: Persons = {
name: "Tom",
age: 25,
gender: "male",
};
// 没问题

只读属性

希望对象中的一些字段只能在创建的时候被赋值,可以用 readonly 定义只读属性。

只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface Person5 {
readonly id: number;
name: string;
age?: number;
[propName: string]: any;
}

let tom5: Person5 = {
id: new Date().getTime(),
name: "Tom",
};
tom5.id = 1; // 无法为“id”赋值,因为它是只读属性。ts(2540)

let tom6: Person5 = {
// 类型 "{ name: string; }" 中缺少属性 "id",但类型 "Person5" 中需要该属性。ts(2741)
name: "Tom",
};
tom6.id = 1; // 无法为“id”赋值,因为它是只读属性。ts(2540)

数组的类型

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

「类型 + 方括号」

最简单的方法是使用「类型 + 方括号」来表示数组。数组的项中不允许出现其他的类型。

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

1
2
3
4
let arr1: number[] = [1, 2, 4];
let arr2: string[] = ["1", "5"];
let arr3: number[] = [1, "2", 4]; // 不能将类型“string”分配给类型“number”。ts(2322)
arr1.push("arr"); // 类型“string”的参数不能赋给类型“number”的参数。ts(2345)

数组泛型

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

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

用接口表示数组

虽然接口可以用来被描述数组,但因为很复杂,一般不这么做。

1
2
3
4
5
6
7
8
9
interface NumberArray {
[index: number]: number;
}
let numberArray: NumberArray = [1, 2, 4];

interface NumberArray2 {
[index: number]: number | string;
}
let numberArray2: NumberArray2 = [1, "5", 4];

any 在数组中

常见的做法是,用 any 表示数组中允许出现任意类型。

1
let list: any[] = [1, "2", 4, true, { name: "kai" }];

类数组

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

1
2
3
4
function sum1() {
let args: number[] = arguments; // 类型“IArguments”缺少类型“number[]”的以下属性: pop, push, concat, join 及其他 26 项。ts(2740)
return args;
}

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

事实上常用的类数组都有自己的接口定义,如 IArgumentsNodeListHTMLCollection 等。

1
2
3
4
5
6
7
8
9
10
11
12
function sum2() {
let args: {
[index: number]: number;
length: number;
callee: Function;
} = arguments;
}
interface IArguments {
[index: number]: any;
length: number;
callee: Function;
}

关于内置对象,可以参考下方的内置对象章节。

函数的类型

函数声明

在 JavaScript 中,有两种常见的定义函数的方式——函数声明(Function Declaration)和函数表达式(Function Expression)。

一个函数有输入和输出,要在 TS 中对其约束,输入和输出都要考虑到。

其中,函数声明的类型定义较简单。但是,输入多余的(或者少于要求的)参数,是不被允许的。

1
2
3
4
5
6
function sum1(x: number, y: number): number {
return x + y;
}
sum1(1, 2);
sum1(1, 2, 3); // 应有 0-2 个参数,但获得 3 个。ts(2554)
sum1(1); // 没有需要 1 参数的重载,但存在需要 0 或 2 参数的重载。ts(2575)

函数表达式

1
2
3
4
5
6
7
8
9
let sum2 = function (x: number, y: number): number {
return x + y;
};
let sum3: (x: number, y: number) => number = function (
x: number,
y: number
): number {
return x + y;
};

sum2 的代码只对等号右侧的匿名函数就行了类型定义,而等号左侧的 sum2 是通过赋值操作进行类型推论而推断出来的。如果需要手动添加类型,应该是 sum3 的代码。

在 TS 中,=> 用来表示函数的定义,左侧是输入类型,需要用括号括起来,右侧是输出类型。

用接口定义函数

采用函数表达式或接口定义函数的方式时,对等号左侧进行类型限制,

可以保证以后对函数名赋值时保证参数个数、参数类型、返回值类型不变。

1
2
3
4
5
6
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc = function (source: string, subString: string) {
return source.search(subString) !== -1;
};

可选参数

与接口中的可选属性类似,用 ? 表示可选的参数。

可选参数必须接在必需参数后面,就是,可选参数后面不允许再出现必需参数。

1
2
3
4
5
6
7
8
9
function buildNmae1(firstName: string, lastName?: string) {
if (lastName) {
return firstName + " " + lastName;
} else {
return firstName;
}
}
let tomcatName = buildNmae1("Tom", "Cat");
let tomName = buildNmae1("Tom");

参数默认值

ES6 允许给函数添加默认值,TS 也会将添加了默认值的参数识别为可选参数。

此时,其不受「可选参数必须接在必需参数后面」的限制。

1
2
3
4
5
6
function buildName2(firstName: string, lastName: string = "Cat") {
return firstName + " " + lastName;
}
function buildName3(firstName: string = "Tom", lastName: string) {
return firstName + " " + lastName;
}

剩余参数

ES6 中可以使用 ...reat 的方式获取函数找那个的剩余参数。

事实上,rest 是一个数组,所以我们可以用数组的类型来定义它。

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

重载

重载允许一个函数接受不同数量或类型的参数时,作出不同的处理。

比如,我们需要实现一个函数 reverse,输入数字 123 的时候,输出反转的数字 321,输入字符串 'hello' 的时候,输出反转的字符串 'olleh'。利用联合类型,可以这么实现:

1
2
3
4
5
6
7
function reverse(x: number | string): number | string | void {
if (typeof x === "number") {
return Number(x.toString().split("").reverse().join(""));
} else if (typeof x === "string") {
return x.split("").reverse().join("");
}
}

然而这样有一个缺点,就是不能够精确的表达,输入为数字的时候,输出也应该为数字,输入为字符串的时候,输出也应该为字符串。这时,可以使用重载定义多个 reverse 的函数类型:

1
2
3
4
5
6
7
8
9
function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string | void {
if (typeof x === "number") {
return Number(x.toString().split("").reverse().join(""));
} else if (typeof x === "string") {
return x.split("").reverse().join("");
}
}

上面当中,多次重复定义了 reverse 函数,前几次都是函数定义,最后一次是函数实现。

TS 优先从最前面的函数定义开始匹配,故多个函数定义如果有包含关系,需要优先把精确的定义写在前面。

类型断言

类型断言(Type Assertion)可以用来手动指定一个值的类型。

语法

方式一:值 as 类型;方式二:<类型>值。在 tsx 中,必须使用前者:值 as 类型

形如 <Foo> 的语法在 tsx 中表示的是一个 ReactNode,在 TS 中除了表示类型断言之外,也可能是表示一个泛型。因此,建议统一使用 值 as 类型 语法。

用途

将一个联合类型断言为其中一个类型

当 TS 不确定一个联合类型的变量是哪个类型的时候,我们只能访问该联合类型的所有类型中共有的属性或方法。但有时,我们确实需要在还不确定类型的时候就访问其中一个类型特有的属性或方法。此时可以使用类型断言。

需要注意的是,类型断言只能够欺骗 TS 编译器,无法避免运行时的错误,滥用类型断言可能会导致运行错误。使用类型断言时一定要格外小心,尽量避免断言后调用方法或引用深层属性,以减少不必要的运行时错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
function isFish1(animal: Cat | Fish) {
if (typeof animal.swim === "function") return true;
else return false;
}
// 类型“Cat | Fish”上不存在属性“swim”。
// 类型“Cat”上不存在属性“swim”。ts(2339)
function isFish2(animal: Cat | Fish) {
if (typeof (animal as Fish).swim === "function") return true;
else return false;
}

将一个父类断言为更加具体的子类

当类之间有继承关系时,类型断言也很常见。

1
2
3
4
5
6
7
8
9
10
class ApiError extends Error {
code: number = 0;
}
class HttpError extends Error {
statusCode: number = 200;
}
function isApiError(error: Error) {
if (typeof (error as ApiError).code === "number") return true;
else return false;
}

将任何一个类型断言为 any

any 类型的变量上,访问任何属性都是允许的。将一个变量断言为 any 是解决 TS 中类型问题的最后一个手段。它极有可能掩盖了真正的类型错误,所以不是非常确定,就不要使用 as any

一方面不能滥用 as any,另一方面也不要完全否定它的作用,我们需要在类型的严格性和开发的便利性之间掌握平衡。

1
2
3
4
window.foo = 1(
// 类型“Window & typeof globalThis”上不存在属性“foo”。ts(2339)
window as any
).foo = 1;

将 any 断言为一个具体的类型

在开发中,非常有可能遇到 any 类型的变量。我们可以选择改进它,任由其滋生更多的 any,也可以选择改进它,通过断言类型及时的吧 any 断言为精确的类型,让我们的代码向着可维护性的目标发展。

1
2
3
4
5
6
7
8
9
function getCacheData(key: string): any {
return (window as any).cache[key];
}
interface Cat {
name: string;
run(): void;
}
const tom = getCacheData("tom") as Cat;
tom.run();

限制

由上,已知:

  • 联合类型可以被断言为其中一个类型
  • 父类可以被断言为子类
  • 任何类型都可以被断言为 any
  • any 可以被断言为任何类型

类型断言的限制,就是,并不是任何一个类型都可以被断言为另一个类型。具体而言,若 A 兼容 B,那么 A 能够被断言为 B,B 也能被断言为 A。

下面例子当中,Cat 包含了 Animal 中的所有属性,初此之外,Cat 还有额外的方法,这等价于 Cat extends Animal,Cat 类型的 tomCat 可以赋值给 Animal 类型的 animal。

专业的说法是,Animal 兼容 Cat。这时,它们就可以相互进行类型断言。允许 animal as Cat,是因为「父类可以被断言为子类」。允许 cat as Animal,是因为子类拥有父类的属性和方法,则被断言为父类,获取父类的属性、调用父类的方法,不会有问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface Animal {
name: string;
}
interface Cat {
name: string;
run(): void;
}
interface Cat extends Animal {
run(): void;
}
let tomCat: Cat = {
name: "Tom",
run: () => {
console.log("run");
},
};
let animal: Animal = tomCat;
function testAniaml(animal: Animal) {
return animal as Cat;
}
function testCat(cat: Cat) {
return cat as Animal;
}

所以,综上所述:要使得 A 能够内断言为 B,只需要 A 兼容 B 或 B 兼容 A 即可。

双重断言

既然,任何类型都可以被断言为 any,且 any 可以被断言为任何类型。那么,我们能够将任何一个类型断言为任何另一个类型。但这种双重断言,十有八九十错误的。

除非迫不得已,千万别用双重断言。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Cat {
run(): void;
}
interface Fish {
swim(): void;
}

function testCat1(cat: Cat) {
return cat as Fish;
}
// 类型 "Cat" 到类型 "Fish" 的转换可能是错误的,因为两种类型不能充分重叠。如果这是有意的,请先将表达式转换为 "unknown"。
// 类型 "Cat" 中缺少属性 "swim",但类型 "Fish" 中需要该属性。ts(2352)
function testCat2(cat: Cat) {
return cat as any as Fish;
}

类型断言 VS 类型转换

类型断言只会影响编译时的类型,类型断言语句在编译结果中会被删除。

类型断言不是类型转换,不会真的影响到变量的类型。要进行类型转换,需要直接调用类型转换的方法:

1
2
3
4
function toBoolean(something: any): boolean {
return Boolean(something);
}
toBoolean(1);

类型断言 VS 类型声明

1
2
3
4
5
6
7
8
9
function getCaCheData(key: string): any {
return (window as any).cache[key];
}
interface Cat {
name: string;
run(): void;
}
const tomCat1 = getCaCheData("tom") as Cat;
const tomCat2: Cat = getCaCheData("tom");

tomCat1 是用 as Cat 将 any 类型的 getCaCheData('tom') 断言为了 Cat 类型。
tomCat2 是用类型声明的方式,将 tomCat2 声明为 Cat,再将 any 类型的 getCaCheData('tom') 赋值给 Cat 类型的 tomCat2。

这两者是非常相似的,而且产生的结果也是一样的:tom 在接下来的代码中都变为了 Cat 类型。

1
2
3
4
5
6
7
8
9
10
11
12
interface Animal {
name: string;
}
interface Cat {
name: string;
run(): void;
}
const animalCat: Animal = {
name: "tom",
};
let tom111 = animalCat as Cat;
let tom222: Cat = animalCat; // 类型 "Animal" 中缺少属性 "run",但类型 "Cat" 中需要该属性。ts(2741)

上方不允许将 animalCat 赋值为 Cat 类型的 tom222,是因为不能把父类的实例赋值给类型为子类的变量。

将 animalCat 断言为 Cat 类型,只需要满足任何一个兼容另一个即可。
将 animalCat 赋值为 Cat 类型的 tom222,需要满足 Cat 兼容 Animal。

因此,类型声明比类型断言更加严格。为了增加代码质量,优先那使用类型声明。

类型断言 vs 泛型

对于

1
2
3
4
5
6
7
8
9
10
11
function getCacheData(key: string): any {
return (window as any).cache[key];
}

interface Cat {
name: string;
run(): void;
}

const tom = getCacheData("tom") as Cat;
tom.run();

还有第三种方式可以解决这个问题,那就是泛型:

1
2
3
4
5
6
7
8
9
10
11
function getCacheData<T>(key: string): T {
return (window as any).cache[key];
}

interface Cat {
name: string;
run(): void;
}

const tom = getCacheData<Cat>("tom");
tom.run();

通过给 getCacheData 函数添加了一个泛型 <T>,我们可以更加规范的实现对 getCacheData 返回值的约束,这也同时去除掉了代码中的 any,是最优的一个解决方案。


JavaScript之TypeScript
https://xuekeven.github.io/2022/12/25/JavaScript之TypeScript/
作者
Keven
发布于
2022年12月25日
更新于
2025年7月30日
许可协议