1.什么是TS

2.ts的优势

3.ts版本选择

没有选择最新版,而是按照教程一致选择tsc 5.1.6

可以使用ts-node 运行ts文件

4.数据类型

4.1 八种数据类型

4.2 any类型和unknown类型区别

any 类型表示没有任何限制,该类型的变量可以赋予任意类型的值。

1
2
3
4
5
let x: any;

x = 1; // 正确
x = "foo"; // 正确
x = true; // 正确

变量类型一旦设为any,TypeScript 实际上会关闭这个变量的类型检查。即使有明显的类型错误,只要句法正确,都不会报错。

1
2
3
4
let x: any = "hello";

x(1); // 不报错
x.foo = 100; // 不报错

实际开发中,any类型主要适用以下两个场合。

(1)出于特殊原因,需要关闭某些变量的类型检查,就可以把该变量的类型设为any

(2)为了适配以前老的 JavaScript 项目,让代码快速迁移到 TypeScript,可以把变量类型设为any。有些年代很久的大型 JavaScript 项目,尤其是别人的代码,很难为每一行适配正确的类型,这时你为那些类型复杂的变量加上any,TypeScript 编译时就不会报错。

总之,TypeScript 认为,只要开发者使用了any类型,就表示开发者想要自己来处理这些代码,所以就不对any类型进行任何限制,怎么使用都可以。

unknown

unknownany的相似之处,在于所有类型的值都可以分配给unknown类型。

1
2
3
4
5
let x: unknown;

x = true; // 正确
x = 42; // 正确
x = "Hello World"; // 正确

unknown类型跟any类型的不同之处在于,它不能直接使用。主要有以下几个限制。

首先,unknown类型的变量,不能直接赋值给其他类型的变量(除了any类型和unknown类型)。

1
2
3
4
let v: unknown = 123;

let v1: boolean = v; // 报错
let v2: number = v; // 报错

上面示例中,变量vunknown类型,赋值给anyunknown以外类型的变量都会报错,这就避免了污染问题,从而克服了any类型的一大缺点。

其次,不能直接调用unknown类型变量的方法和属性。

1
2
3
4
5
6
7
8
let v1: unknown = { foo: 123 };
v1.foo; // 报错

let v2: unknown = "hello";
v2.trim(); // 报错

let v3: unknown = (n = 0) => n + 1;
v3(); // 报错

上面示例中,直接调用unknown类型变量的属性和方法,或者直接当作函数执行,都会报错。

再次,unknown类型变量能够进行的运算是有限的,只能进行比较运算(运算符=====!=!==||&&?)、取反运算(运算符!)、typeof运算符和instanceof运算符这几种,其他运算都会报错。

1
2
3
4
let a: unknown = 1;

a + 1; // 报错
a === 1; // 正确

4.3 void和never的区别

void会返回值,但只返回underfined或null

为了保持与集合论的对应关系,以及类型运算的完整性,TypeScript 还引入了“空类型”的概念,即该类型为空,不包含任何值。

由于不存在任何属于“空类型”的值,所以该类型被称为never,即不可能有这样的值。

上面示例中,变量x的类型是never,就不可能赋给它任何值,否则都会报错。

never类型的使用场景,主要是在一些类型运算之中,保证类型运算的完整性,详见后面章节。另外,不可能返回值的函数,返回值的类型就可以写成never,详见《函数》一章。

如果一个变量可能有多种类型(即联合类型),通常需要使用分支处理每一种类型。这时,处理所有可能的类型之后,剩余的情况就属于never类型。

1
2
3
4
5
6
7
8
9
function fn(x: string | number) {
if (typeof x === "string") {
// ...
} else if (typeof x === "number") {
// ...
} else {
x; // never 类型
}
}

never类型的一个重要特点是,可以赋值给任意其他类型。

1
2
3
4
5
6
7
function f(): never {
throw new Error("Error");
}

let v1: number = f(); // 不报错
let v2: string = f(); // 不报错
let v3: boolean = f(); // 不报错

上面示例中,函数f()会抛错,所以返回值类型可以写成never,即不可能返回任何值。各种其他类型的变量都可以赋值为f()的运行结果(never类型)。

总之,TypeScript 有两个“顶层类型”(anyunknown),但是“底层类型”只有never唯一一个。

4.4 null和underfined的区别

undefined 类型,null 类型

undefined 和 null 是两种独立类型,它们各自都只有一个值。

undefined 类型只包含一个值undefined,表示未定义(即还未给出定义,以后可能会有定义)。

1
let x: undefined = undefined;

上面示例中,变量x就属于 undefined 类型。两个undefined里面,第一个是类型,第二个是值。

null 类型也只包含一个值null,表示为空(即此处没有值)。

1
const x: null = null;

上面示例中,变量x就属于 null 类型。

注意,如果没有声明类型的变量,被赋值为undefinednull,它们的类型会被推断为any

1
2
3
4
5
let a = undefined; // any
const b = undefined; // any

let c = null; // any
const d = null; // any

如果希望避免这种情况,则需要打开编译选项strictNullChecks

1
2
3
4
5
6
// 打开编译设置 strictNullChecks
let a = undefined; // undefined
const b = undefined; // undefined

let c = null; // null
const d = null; // null

上面示例中,打开编译设置strictNullChecks以后,赋值为undefined的变量会被推断为undefined类型,赋值为null的变量会被推断为null类型。

5.数组和元组

1
2
3
4
5
6
7
8
9
10
11
//数组的使用
let l3_a: number[] = [1,2,5,4,2]
l3_a.push(3)
//类数组
function test(){
arguments.length
HTMLCollection.length
}
//元组
let l3_b: [number,string] = [1,'s'];
l3_b.push('3334',3,666)

6.可访问性修饰符

类的内部成员的外部可访问性,由三个可访问性修饰符(access modifiers)控制:publicprivateprotected

这三个修饰符的位置,都写在属性或方法的最前面。

public

public修饰符表示这是公开成员,外部可以自由访问。

1
2
3
4
5
6
7
8
class Greeter {
public greet() {
console.log("hi!");
}
}

const g = new Greeter();
g.greet();

上面示例中,greet()方法前面的public修饰符,表示该方法可以在类的外部调用,即外部实例可以调用。

public修饰符是默认修饰符,如果省略不写,实际上就带有该修饰符。因此,类的属性和方法默认都是外部可访问的。

正常情况下,除非为了醒目和代码可读性,public都是省略不写的。

private

private修饰符表示私有成员,只能用在当前类的内部,类的实例和子类都不能使用该成员。

1
2
3
4
5
6
7
8
9
10
11
12
class A {
private x: number = 0;
}

const a = new A();
a.x; // 报错

class B extends A {
showX() {
console.log(this.x); // 报错
}
}

上面示例中,属性x前面有private修饰符,表示这是私有成员。因此,实例对象和子类使用该成员,都会报错。

注意,子类不能定义父类私有成员的同名成员。

1
2
3
4
5
6
7
class A {
private x = 0;
}

class B extends A {
x = 1; // 报错
}

上面示例中,A类有一个私有属性x,子类B就不能定义自己的属性x了。

如果在类的内部,当前类的实例可以获取私有成员。

1
2
3
4
5
6
7
8
9
10
class A {
private x = 10;

f(obj: A) {
console.log(obj.x);
}
}

const a = new A();
a.f(a); // 10

上面示例中,在类A内部,A的实例对象可以获取私有成员x

严格地说,private定义的私有成员,并不是真正意义的私有成员。一方面,编译成 JavaScript 后,private关键字就被剥离了,这时外部访问该成员就不会报错。另一方面,由于前一个原因,TypeScript 对于访问private成员没有严格禁止,使用方括号写法([])或者in运算符,实例对象就能访问该成员。

1
2
3
4
5
6
7
8
9
10
11
class A {
private x = 1;
}

const a = new A();
a["x"]; // 1

if ("x" in a) {
// 正确
// ...
}

上面示例中,A类的属性x是私有属性,但是实例使用方括号,就可以读取这个属性,或者使用in运算符检查这个属性是否存在,都可以正确执行。

由于private存在这些问题,加上它是 ES6 标准发布前出台的,而 ES6 引入了自己的私有成员写法#propName。因此建议不使用private,改用 ES6 的写法,获得真正意义的私有成员。

1
2
3
4
5
6
class A {
#x = 1;
}

const a = new A();
a["x"]; // 报错

上面示例中,采用了 ES6 的私有成员写法(属性名前加#),TypeScript 就正确识别了实例对象没有属性x,从而报错。

构造方法也可以是私有的,这就直接防止了使用new命令生成实例对象,只能在类的内部创建实例对象。

这时一般会有一个静态方法,充当工厂函数,强制所有实例都通过该方法生成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Singleton {
private static instance?: Singleton;

private constructor() {}

static getInstance() {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
}

const s = Singleton.getInstance();

上面示例使用私有构造方法,实现了单例模式。想要获得 Singleton 的实例,不能使用new命令,只能使用getInstance()方法。

protected

protected修饰符表示该成员是保护成员,只能在类的内部使用该成员,实例无法使用该成员,但是子类内部可以使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A {
protected x = 1;
}

class B extends A {
getX() {
return this.x;
}
}

const a = new A();
const b = new B();

a.x; // 报错
b.getX(); // 1

上面示例中,类A的属性x是保护成员,直接从实例读取该属性(a.x)会报错,但是子类B内部可以读取该属性。

子类不仅可以拿到父类的保护成员,还可以定义同名成员。

1
2
3
4
5
6
7
class A {
protected x = 1;
}

class B extends A {
x = 2;
}

上面示例中,子类B定义了父类A的同名成员x,并且父类的x是保护成员,子类将其改成了公开成员。B类的x属性前面没有修饰符,等同于修饰符是public,外界可以读取这个属性。

在类的外部,实例对象不能读取保护成员,但是在类的内部可以。

1
2
3
4
5
6
7
8
9
10
11
12
class A {
protected x = 1;

f(obj: A) {
console.log(obj.x);
}
}

const a = new A();

a.x; // 报错
a.f(a); // 1

上面示例中,属性x是类A的保护成员,在类的外部,实例对象a拿不到这个属性。但是,实例对象a传入类A的内部,就可以从a拿到x

readonly 修饰符

属性名前面加上 readonly 修饰符,就表示该属性是只读的。实例对象不能修改这个属性。

1
2
3
4
5
6
class A {
readonly id = "foo";
}

const a = new A();
a.id = "bar"; // 报错

上面示例中,id属性前面有 readonly 修饰符,实例对象修改这个属性就会报错。

readonly 属性的初始值,可以写在顶层属性,也可以写在构造方法里面。

1
2
3
4
5
6
7
class A {
readonly id: string;

constructor() {
this.id = "bar"; // 正确
}
}

上面示例中,构造方法内部设置只读属性的初值,这是可以的。

1
2
3
4
5
6
7
class A {
readonly id: string = "foo";

constructor() {
this.id = "bar"; // 正确
}
}

上面示例中,构造方法修改只读属性的值也是可以的。或者说,如果两个地方都设置了只读属性的值,以构造方法为准。在其他方法修改只读属性都会报错。

7.静态成员–static

静态成员是只能通过类本身使用的成员,不能通过实例对象使用。

1
2
3
4
5
6
7
8
9
class MyClass {
static x = 0;
static printX() {
console.log(MyClass.x);
}
}

MyClass.x; // 0
MyClass.printX(); // 0

上面示例中,x是静态属性,printX()是静态方法。它们都必须通过MyClass获取,而不能通过实例对象调用。

static关键字前面可以使用 public、private、protected 修饰符。

1
2
3
4
5
class MyClass {
private static x = 0;
}

MyClass.x; // 报错

上面示例中,静态属性x前面有private修饰符,表示只能在MyClass内部使用,如果在外部调用这个属性就会报错。

静态私有属性也可以用 ES6 语法的#前缀表示,上面示例可以改写如下。

1
2
3
class MyClass {
static #x = 0;
}

publicprotected的静态成员可以被继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
class A {
public static x = 1;
protected static y = 1;
}

class B extends A {
static getY() {
return B.y;
}
}

B.x; // 1
B.getY(); // 1

上面示例中,类A的静态属性xy都被B继承,公开成员x可以在B的外部获取,保护成员y只能在B的内部获取。

单例模式在ts的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//单例模式
class Singleton {
private static instance: Singleton | null = null;
private constructor() {} // 私有构造函数,防止外部实例化


static make(): Singleton{
if (Singleton.instance == null) { // 检查是否已经创建了实例
console.log('创建实例');
Singleton.instance = new Singleton(); // 创建实例并赋值给静态属性instance

}
return Singleton.instance; // 返回实例
}


}

const instance1 = Singleton.make(); // 创建实例1
const instance2 = Singleton.make(); // 创建实例1

8.抽象类

抽象类,抽象成员

TypeScript 允许在类的定义前面,加上关键字abstract,表示该类不能被实例化,只能当作其他类的模板。这种类就叫做“抽象类”(abastract class)。

1
2
3
4
5
abstract class A {
id = 1;
}

const a = new A(); // 报错

上面示例中,直接新建抽象类的实例,会报错。

抽象类只能当作基类使用,用来在它的基础上定义子类。

1
2
3
4
5
6
7
8
9
10
11
12
abstract class A {
id = 1;
}

class B extends A {
amount = 100;
}

const b = new B();

b.id; // 1
b.amount; // 100

上面示例中,A是一个抽象类,BA的子类,继承了A的所有成员,并且可以定义自己的成员和实例化。

抽象类的子类也可以是抽象类,也就是说,抽象类可以继承其他抽象类。

1
2
3
4
5
6
7
abstract class A {
foo: number;
}

abstract class B extends A {
bar: string;
}

抽象类的内部可以有已经实现好的属性和方法,也可以有还未实现的属性和方法。后者就叫做“抽象成员”(abstract member),即属性名和方法名有abstract关键字,表示该方法需要子类实现。如果子类没有实现抽象成员,就会报错。

1
2
3
4
5
6
7
8
abstract class A {
abstract foo: string;
bar: string = "";
}

class B extends A {
foo = "b";
}

上面示例中,抽象类A定义了抽象属性foo,子类B必须实现这个属性,否则会报错。

下面是抽象方法的例子。

如果抽象类的属性前面加上abstract,就表明子类必须给出该方法的实现。

1
2
3
4
5
6
7
8
9
abstract class A {
abstract execute(): string;
}

class B extends A {
execute() {
return `B executed`;
}
}

这里有几个注意点。

(1)抽象成员只能存在于抽象类,不能存在于普通类。

(2)抽象成员不能有具体实现的代码。也就是说,已经实现好的成员前面不能加abstract关键字。

(3)抽象成员前也不能有private修饰符,否则无法在子类中实现该成员。

(4)一个子类最多只能继承一个抽象类。

总之,抽象类的作用是,确保各种相关的子类都拥有跟基类相同的接口,可以看作是模板。其中的抽象成员都是必须由子类实现的成员,非抽象成员则表示基类已经实现的、由所有子类共享的成员。

6.接口

类的 interface 接口

implements 关键字

interface 接口或 type 别名,可以用对象的形式,为 class 指定一组检查条件。然后,类使用 implements 关键字,表示当前类满足这些外部类型条件的限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Country {
name: string;
capital: string;
}
// 或者
type Country = {
name: string;
capital: string;
};

class MyCountry implements Country {
name = "";
capital = "";
}

上面示例中,interfacetype都可以定义一个对象类型。类MyCountry使用implements关键字,表示该类的实例对象满足这个外部类型。

interface 只是指定检查条件,如果不满足这些条件就会报错。它并不能代替 class 自身的类型声明。

1
2
3
4
5
6
7
8
9
10
interface A {
get(name: string): boolean;
}

class B implements A {
get(s) {
// s 的类型是 any
return true;
}
}

上面示例中,类B实现了接口A,但是后者并不能代替B的类型声明。因此,Bget()方法的参数s的类型是any,而不是stringB类依然需要声明参数s的类型。

1
2
3
4
5
class B implements A {
get(s: string) {
return true;
}
}

下面是另一个例子。

1
2
3
4
5
6
7
8
9
10
11
interface A {
x: number;
y?: number;
}

class B implements A {
x = 0;
}

const b = new B();
b.y = 10; // 报错

上面示例中,接口A有一个可选属性y,类B没有声明这个属性,所以可以通过类型检查。但是,如果给B的实例对象的属性y赋值,就会报错。所以,B类还是需要声明可选属性y

1
2
3
4
class B implements A {
x = 0;
y?: number;
}

同理,类可以定义接口没有声明的方法和属性。

1
2
3
4
5
6
7
8
9
10
interface Point {
x: number;
y: number;
}

class MyPoint implements Point {
x = 1;
y = 1;
z: number = 1;
}

上面示例中,MyPoint类实现了Point接口,但是内部还定义了一个额外的属性z,这是允许的,表示除了满足接口给出的条件,类还有额外的条件。

implements关键字后面,不仅可以是接口,也可以是另一个类。这时,后面的类将被当作接口。

1
2
3
4
5
6
7
8
9
class Car {
id: number = 1;
move(): void {}
}

class MyCar implements Car {
id = 2; // 不可省略
move(): void {} // 不可省略
}

上面示例中,implements后面是类Car,这时 TypeScript 就把Car视为一个接口,要求MyCar实现Car里面的每一个属性和方法,否则就会报错。所以,这时不能因为Car类已经实现过一次,而在MyCar类省略属性或方法。

注意,interface 描述的是类的对外接口,也就是实例的公开属性和公开方法,不能定义私有的属性和方法。这是因为 TypeScript 设计者认为,私有属性是类的内部实现,接口作为模板,不应该涉及类的内部代码写法。

1
2
3
interface Foo {
member: {}; // 报错
}

上面示例中,接口Foo有一个私有属性,结果就报错了。

实现多个接口

类可以实现多个接口(其实是接受多重限制),每个接口之间使用逗号分隔。

1
2
3
class Car implements MotorVehicle, Flyable, Swimmable {
// ...
}

上面示例中,Car类同时实现了MotorVehicleFlyableSwimmable三个接口。这意味着,它必须部署这三个接口声明的所有属性和方法,满足它们的所有条件。

但是,同时实现多个接口并不是一个好的写法,容易使得代码难以管理,可以使用两种方法替代。

第一种方法是类的继承。

1
2
3
class Car implements MotorVehicle {}

class SecretCar extends Car implements Flyable, Swimmable {}

上面示例中,Car类实现了MotorVehicle,而SecretCar类继承了Car类,然后再实现FlyableSwimmable两个接口,相当于SecretCar类同时实现了三个接口。

第二种方法是接口的继承。

1
2
3
4
5
6
7
interface A {
a: number;
}

interface B extends A {
b: number;
}

上面示例中,接口B继承了接口A,类只要实现接口B,就相当于实现AB两个接口。

前一个例子可以用接口继承改写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface MotorVehicle {
// ...
}
interface Flyable {
// ...
}
interface Swimmable {
// ...
}

interface SuperCar extends MotoVehicle, Flyable, Swimmable {
// ...
}

class SecretCar implements SuperCar {
// ...
}

上面示例中,接口SuperCar通过SuperCar接口,就间接实现了多个接口。

注意,发生多重实现时(即一个接口同时实现多个接口),不同接口不能有互相冲突的属性。

1
2
3
4
5
6
7
interface Flyable {
foo: number;
}

interface Swimmable {
foo: string;
}

上面示例中,属性foo在两个接口里面的类型不同,如果同时实现这两个接口,就会报错。

类与接口的合并

TypeScript 不允许两个同名的类,但是如果一个类和一个接口同名,那么接口会被合并进类。

1
2
3
4
5
6
7
8
9
10
11
12
13
class A {
x: number = 1;
}

interface A {
y: number;
}

let a = new A();
a.y = 10;

a.x; // 1
a.y; // 10

上面示例中,类A与接口A同名,后者会被合并进前者的类型定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface IPerson{
name : string;
//可有可无属性?:
age?: number;
//只读属性
readonly sex :string;
}

let xzj: IPerson = {
name:'xzj',
age: 21,
sex:'男',
}

//xzj.sex = '女' 做不到,因为是只读属性

7.函数

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
35
// 定义一个函数add,参数x,y,z,z可选,返回值为number类型
function add(x:number,y:number,z?:number) : number{
// 如果z是number类型,则返回x+y+z
if(typeof z === 'number'){
return x+y+z
}
// 否则,返回x+y
else {
return x+y
}
}

// 定义一个函数add2,参数x,y,z,z可选,返回值为number类型
const add2 = (x:number,y:number,z?:number):number =>{
// 如果z是number类型,则返回x+y+z
if(typeof z === 'number'){
return x+y+z
}
// 否则,返回x+y
else {
return x+y
}
}

// 将add2赋值给add3,add3的参数x,y,z,z可选,返回值为number类型
let add3:(x:number,y:number,z?:number) => number = add2


// 定义一个接口ISum,参数x,y,z,z可选,返回值为number类型
interface ISum {
(x:number,y:number,z?:number):number
}

// 将add2赋值给add4,add4的参数x,y,z,z可选,返回值为number类型
let add4:ISum = add2

8.类型推断、联合类型和类型断言和type guard

9.类型断言

对于没有类型声明的值,TypeScript 会进行类型推断,很多时候得到的结果,未必是开发者想要的。

1
2
3
4
type T = "a" | "b" | "c";
let foo = "a";

let bar: T = foo; // 报错

上面示例中,最后一行报错,原因是 TypeScript 推断变量foo的类型是string,而变量bar的类型是'a'|'b'|'c',前者是后者的父类型。父类型不能赋值给子类型,所以就报错了。

TypeScript 提供了“类型断言”这样一种手段,允许开发者在代码中“断言”某个值的类型,告诉编译器此处的值是什么类型。TypeScript 一旦发现存在类型断言,就不再对该值进行类型推断,而是直接采用断言给出的类型。

这种做法的实质是,允许开发者在某个位置“绕过”编译器的类型推断,让本来通不过类型检查的代码能够通过,避免编译器报错。这样虽然削弱了 TypeScript 类型系统的严格性,但是为开发者带来了方便,毕竟开发者比编译器更了解自己的代码。

回到上面的例子,解决方法就是进行类型断言,在赋值时断言变量foo的类型。

1
2
3
4
type T = "a" | "b" | "c";

let foo = "a";
let bar: T = foo as T; // 正确

上面示例中,最后一行的foo as T表示告诉编译器,变量foo的类型断言为T,所以这一行不再需要类型推断了,编译器直接把foo的类型当作T,就不会报错了。

总之,类型断言并不是真的改变一个值的类型,而是提示编译器,应该如何处理这个值。

类型断言有两种语法。

1
2
3
4
5
// 语法一:<类型>值
<Type>value;

// 语法二:值 as 类型
value as Type;

上面两种语法是等价的,value表示值,Type表示类型。早期只有语法一,后来因为 TypeScript 开始支持 React 的 JSX 语法(尖括号表示 HTML 元素),为了避免两者冲突,就引入了语法二。目前,推荐使用语法二。

1
2
3
4
5
// 语法一
let bar: T = <T>foo;

// 语法二
let bar: T = foo as T;

上面示例是两种类型断言的语法,其中的语法一因为跟 JSX 语法冲突,使用时必须关闭 TypeScript 的 React 支持,否则会无法识别。由于这个原因,现在一般都使用语法二。

下面看一个例子。《对象》一章提到过,对象类型有严格字面量检查,如果存在额外的属性会报错。

1
2
// 报错
const p: { x: number } = { x: 0, y: 0 };

上面示例中,等号右侧是一个对象字面量,多出了属性y,导致报错。解决方法就是使用类型断言,可以用两种不同的断言。

1
2
3
4
5
// 正确
const p0: { x: number } = { x: 0, y: 0 } as { x: number };

// 正确
const p1: { x: number } = { x: 0, y: 0 } as { x: number; y: number };

上面示例中,两种类型断言都是正确的。第一种断言将类型改成与等号左边一致,第二种断言使得等号右边的类型是左边类型的子类型,子类型可以赋值给父类型,同时因为存在类型断言,就没有严格字面量检查了,所以不报错。

下面是一个网页编程的实际例子。

1
2
3
4
5
const username = document.getElementById("username");

if (username) {
(username as HTMLInputElement).value; // 正确
}

上面示例中,变量username的类型是HTMLElement | null,排除了null的情况以后,HTMLElement 类型是没有value属性的。如果username是一个输入框,那么就可以通过类型断言,将它的类型改成HTMLInputElement,就可以读取value属性。

注意,上例的类型断言的圆括号是必需的,否则username会被断言成HTMLInputElement.value,从而报错。

类型断言不应滥用,因为它改变了 TypeScript 的类型检查,很可能埋下错误的隐患。

1
2
3
4
5
6
7
8
9
const data: object = {
a: 1,
b: 2,
c: 3,
};

data.length; // 报错

(data as Array<string>).length; // 正确

上面示例中,变量data是一个对象,没有length属性。但是通过类型断言,可以将它的类型断言为数组,这样使用length属性就能通过类型检查。但是,编译后的代码在运行时依然会报错,所以类型断言可以让错误的代码通过编译。

类型断言的一大用处是,指定 unknown 类型的变量的具体类型。

1
2
3
4
const value: unknown = "Hello World";

const s1: string = value; // 报错
const s2: string = value as string; // 正确

上面示例中,unknown 类型的变量value不能直接赋值给其他类型的变量,但是可以将它断言为其他类型,这样就可以赋值给别的变量了。

另外,类型断言也适合指定联合类型的值的具体类型。

1
2
const s1: number | string = "hello";
const s2: number = s1 as number;

上面示例中,变量s1是联合类型,可以断言其为联合类型里面的一种具体类型,再将其赋值给变量s2

类型断言的条件

类型断言并不意味着,可以把某个值断言为任意类型。

1
2
const n = 1;
const m: string = n as string; // 报错

上面示例中,变量n是数值,无法把它断言成字符串,TypeScript 会报错。

类型断言的使用前提是,值的实际类型与断言的类型必须满足一个条件。

1
expr as T;

上面代码中,expr是实际的值,T是类型断言,它们必须满足下面的条件:exprT的子类型,或者Texpr的子类型。

也就是说,类型断言要求实际的类型与断言的类型兼容,实际类型可以断言为一个更加宽泛的类型(父类型),也可以断言为一个更加精确的类型(子类型),但不能断言为一个完全无关的类型。

但是,如果真的要断言成一个完全无关的类型,也是可以做到的。那就是连续进行两次类型断言,先断言成 unknown 类型或 any 类型,然后再断言为目标类型。因为any类型和unknown类型是所有其他类型的父类型,所以可以作为两种完全无关的类型的中介。

1
2
// 或者写成 <T><unknown>expr
expr as unknown as T;

上面代码中,expr连续进行了两次类型断言,第一次断言为unknown类型,第二次断言为T类型。这样的话,expr就可以断言成任意类型T,而不报错。

下面是本小节开头那个例子的改写。

1
2
const n = 1;
const m: string = n as unknown as string; // 正确

上面示例中,通过两次类型断言,变量n的类型就从数值,变成了完全无关的字符串,从而赋值时不会报错。

as const 断言

如果没有声明变量类型,let 命令声明的变量,会被类型推断为 TypeScript 内置的基本类型之一;const 命令声明的变量,则被推断为值类型常量。

1
2
3
4
5
// 类型推断为基本类型 string
let s1 = "JavaScript";

// 类型推断为字符串 “JavaScript”
const s2 = "JavaScript";

上面示例中,变量s1的类型被推断为string,变量s2的类型推断为值类型JavaScript。后者是前者的子类型,相当于 const 命令有更强的限定作用,可以缩小变量的类型范围。

有些时候,let 变量会出现一些意想不到的报错,变更成 const 变量就能消除报错。

1
2
3
4
5
6
7
8
9
let s = "JavaScript";

type Lang = "JavaScript" | "TypeScript" | "Python";

function setLang(language: Lang) {
/* ... */
}

setLang(s); // 报错

上面示例中,最后一行报错,原因是函数setLang()的参数language类型是Lang,这是一个联合类型。但是,传入的字符串s的类型被推断为string,属于Lang的父类型。父类型不能替代子类型,导致报错。

一种解决方法就是把 let 命令改成 const 命令。

1
const s = "JavaScript";

这样的话,变量s的类型就是值类型JavaScript,它是联合类型Lang的子类型,传入函数setLang()就不会报错。

另一种解决方法是使用类型断言。TypeScript 提供了一种特殊的类型断言as const,用于告诉编译器,推断类型时,可以将这个值推断为常量,即把 let 变量断言为 const 变量,从而把内置的基本类型变更为值类型。

1
2
let s = "JavaScript" as const;
setLang(s); // 正确

上面示例中,变量s虽然是用 let 命令声明的,但是使用了as const断言以后,就等同于是用 const 命令声明的,变量s的类型会被推断为值类型JavaScript

使用了as const断言以后,let 变量就不能再改变值了。

1
2
let s = "JavaScript" as const;
s = "Python"; // 报错

上面示例中,let 命令声明的变量s,使用as const断言以后,就不能改变值了,否则报错。

注意,as const断言只能用于字面量,不能用于变量。

1
2
let s = "JavaScript";
setLang(s as const); // 报错

上面示例中,as const断言用于变量s,就报错了。下面的写法可以更清晰地看出这一点。

1
2
let s1 = "JavaScript";
let s2 = s1 as const; // 报错

另外,as const也不能用于表达式。

1
let s = ("Java" + "Script") as const; // 报错

上面示例中,as const用于表达式,导致报错。

as const也可以写成前置的形式。

1
2
3
4
5
// 后置形式
expr as const

// 前置形式
<const>expr

as const断言可以用于整个对象,也可以用于对象的单个属性,这时它的类型缩小效果是不一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const v1 = {
x: 1,
y: 2,
}; // 类型是 { x: number; y: number; }

const v2 = {
x: 1 as const,
y: 2,
}; // 类型是 { x: 1; y: number; }

const v3 = {
x: 1,
y: 2,
} as const; // 类型是 { readonly x: 1; readonly y: 2; }

上面示例中,第二种写法是对属性x缩小类型,第三种写法是对整个对象缩小类型。

总之,as const会将字面量的类型断言为不可变类型,缩小成 TypeScript 允许的最小类型。

下面是数组的例子。

1
2
3
4
5
// a1 的类型推断为 number[]
const a1 = [1, 2, 3];

// a2 的类型推断为 readonly [1, 2, 3]
const a2 = [1, 2, 3] as const;

上面示例中,数组字面量使用as const断言后,类型推断就变成了只读元组。

由于as const会将数组变成只读元组,所以很适合用于函数的 rest 参数。

1
2
3
4
5
6
function add(x: number, y: number) {
return x + y;
}

const nums = [1, 2];
const total = add(...nums); // 报错

上面示例中,变量nums的类型推断为number[],导致使用扩展运算符...传入函数add()会报错,因为add()只能接受两个参数,而...nums并不能保证参数的个数。

事实上,对于固定参数个数的函数,如果传入的参数包含扩展运算符,那么扩展运算符只能用于元组。只有当函数定义使用了 rest 参数,扩展运算符才能用于数组。

解决方法就是使用as const断言,将数组变成元组。

1
2
const nums = [1, 2] as const;
const total = add(...nums); // 正确

上面示例中,使用as const断言后,变量nums的类型会被推断为readonly [1, 2],使用扩展运算符展开后,正好符合函数add()的参数类型。

Enum 成员也可以使用as const断言。

1
2
3
4
5
6
enum Foo {
X,
Y,
}
let e1 = Foo.X; // Foo
let e2 = Foo.X as const; // Foo.X

上面示例中,如果不使用as const断言,变量e1的类型被推断为整个 Enum 类型;使用了as const断言以后,变量e2的类型被推断为 Enum 的某个成员,这意味着它不能变更为其他成员。

非空断言

对于那些可能为空的变量(即可能等于undefinednull),TypeScript 提供了非空断言,保证这些变量不会为空,写法是在变量名后面加上感叹号!

1
2
3
4
5
6
7
8
function f(x?: number | null) {
validateNumber(x); // 自定义函数,确保 x 是数值
console.log(x!.toFixed());
}

function validateNumber(e?: number | null) {
if (typeof e !== "number") throw new Error("Not a number");
}

上面示例中,函数f()的参数x的类型是number|null,即可能为空。如果为空,就不存在x.toFixed()方法,这样写会报错。但是,开发者可以确认,经过validateNumber()的前置检验,变量x肯定不会为空,这时就可以使用非空断言,为函数体内部的变量x加上后缀!x!.toFixed()编译就不会报错了。

非空断言在实际编程中很有用,有时可以省去一些额外的判断。

1
2
3
4
5
6
const root = document.getElementById("root");

// 报错
root.addEventListener("click", (e) => {
/* ... */
});

上面示例中,getElementById()有可能返回空值null,即变量root可能为空,这时对它调用addEventListener()方法就会报错,通不过编译。但是,开发者如果可以确认root元素肯定会在网页中存在,这时就可以使用非空断言。

1
const root = document.getElementById("root")!;

上面示例中,getElementById()方法加上后缀!,表示这个方法肯定返回非空结果。

不过,非空断言会造成安全隐患,只有在确定一个表达式的值不为空时才能使用。比较保险的做法还是手动检查一下是否为空。

1
2
3
4
5
6
7
8
9
const root = document.getElementById("root");

if (root === null) {
throw new Error("Unable to find DOM element #root");
}

root.addEventListener("click", (e) => {
/* ... */
});

上面示例中,如果root为空会抛错,比非空断言更保险一点。

非空断言还可以用于赋值断言。TypeScript 有一个编译设置,要求类的属性必须初始化(即有初始值),如果不对属性赋值就会报错。

1
2
3
4
5
6
7
8
class Point {
x: number; // 报错
y: number; // 报错

constructor(x: number, y: number) {
// ...
}
}

上面示例中,属性xy会报错,因为 TypeScript 认为它们没有初始化。

这时就可以使用非空断言,表示这两个属性肯定会有值,这样就不会报错了。

1
2
3
4
5
6
7
8
class Point {
x!: number; // 正确
y!: number; // 正确

constructor(x: number, y: number) {
// ...
}
}

另外,非空断言只有在打开编译选项strictNullChecks时才有意义。如果不打开这个选项,编译器就不会检查某个变量是否可能为undefinednull

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
//类型推断
let l6_str = 'str'

//联合类型
let numberOrString: number | string


//类型断言
function l6_getLength(input:string | number):number{
const str = input as string
if(str.length){
return str.length
}else{
const l6_number = input as number
return l6_number.toString().length
}
}

//type guard
function l6_getLength2(input: string | number) :number{
if(typeof input === 'string'){
return input.length
}else{
return input.toString().length
}
}

9.枚举

实际开发中,经常需要定义一组相关的常量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const RED = 1;
const GREEN = 2;
const BLUE = 3;

let color = userInput();

if (color === RED) {
/* */
}
if (color === GREEN) {
/* */
}
if (color === BLUE) {
/* */
}

throw new Error("wrong color");

上面示例中,常量REDGREENBLUE是相关的,意为变量color的三个可能的取值。它们具体等于什么值其实并不重要,只要不相等就可以了。

TypeScript 就设计了 Enum 结构,用来将相关常量放在一个容器里面,方便使用。

1
2
3
4
5
enum Color {
Red, // 0
Green, // 1
Blue, // 2
}

上面示例声明了一个 Enum 结构Color,里面包含三个成员RedGreenBlue。第一个成员的值默认为整数0,第二个为1,第二个为2,以此类推。

使用时,调用 Enum 的某个成员,与调用对象属性的写法一样,可以使用点运算符,也可以使用方括号运算符。

1
2
3
let c = Color.Green; // 1
// 等同于
let c = Color["Green"]; // 1

Enum 结构本身也是一种类型。比如,上例的变量c等于1,它的类型可以是 Color,也可以是number

1
2
let c: Color = Color.Green; // 正确
let c: number = Color.Green; // 正确

上面示例中,变量c的类型写成Colornumber都可以。但是,Color类型的语义更好。

Enum 结构的特别之处在于,它既是一种类型,也是一个值。绝大多数 TypeScript 语法都是类型语法,编译后会全部去除,但是 Enum 结构是一个值,编译后会变成 JavaScript 对象,留在代码中。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 编译前
enum Color {
Red, // 0
Green, // 1
Blue, // 2
}

// 编译后
let Color = {
Red: 0,
Green: 1,
Blue: 2,
};

上面示例是 Enum 结构编译前后的对比。

由于 TypeScript 的定位是 JavaScript 语言的类型增强,所以官方建议谨慎使用 Enum 结构,因为它不仅仅是类型,还会为编译后的代码加入一个对象。

Enum 结构比较适合的场景是,成员的值不重要,名字更重要,从而增加代码的可读性和可维护性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
enum Operator {
ADD,
DIV,
MUL,
SUB,
}

function compute(op: Operator, a: number, b: number) {
switch (op) {
case Operator.ADD:
return a + b;
case Operator.DIV:
return a / b;
case Operator.MUL:
return a * b;
case Operator.SUB:
return a - b;
default:
throw new Error("wrong operator");
}
}

compute(Operator.ADD, 1, 3); // 4

很大程度上,Enum 结构可以被对象的as const断言替代。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum Foo {
A,
B,
C,
}

const Bar = {
A: 0,
B: 1,
C: 2,
} as const;

if (x === Foo.A){}
// 等同于
if (x === Bar.A) {}

上面示例中,对象Bar使用了as const断言,作用就是使得它的属性无法修改。这样的话,FooBar的行为就很类似了,前者完全可以用后者替代,而且后者还是 JavaScript 的原生数据结构。

10.泛型

有些时候,函数返回值的类型与参数类型是相关的。

1
2
3
function getFirst(arr) {
return arr[0];
}

上面示例中,函数getFirst()总是返回参数数组的第一个成员。参数数组是什么类型,返回值就是什么类型。

这个函数的类型声明只能写成下面这样。

1
2
3
function f(arr: any[]): any {
return arr[0];
}

上面的类型声明,就反映不出参数与返回值之间的类型关系。

为了解决这个问题,TypeScript 就引入了“泛型”(generics)。泛型的特点就是带有“类型参数”(type parameter)。

1
2
3
function getFirst<T>(arr: T[]): T {
return arr[0];
}

上面示例中,函数getFirst()的函数名后面尖括号的部分<T>,就是类型参数,参数要放在一对尖括号(<>)里面。本例只有一个类型参数T,可以将其理解为类型声明需要的变量,需要在调用时传入具体的参数类型。

上例的函数getFirst()的参数类型是T[],返回值类型是T,就清楚地表示了两者之间的关系。比如,输入的参数类型是number[],那么 T 的值就是number,因此返回值类型也是number

函数调用时,需要提供类型参数。

1
getFirst<number>([1, 2, 3]);

上面示例中,调用函数getFirst()时,需要在函数名后面使用尖括号,给出类型参数T的值,本例是<number>

不过为了方便,函数调用时,往往省略不写类型参数的值,让 TypeScript 自己推断。

1
getFirst([1, 2, 3]);

上面示例中,TypeScript 会从实际参数[1, 2, 3],推断出类型参数 T 的值为number

有些复杂的使用场景,TypeScript 可能推断不出类型参数的值,这时就必须显式给出了。

1
2
3
function comb<T>(arr1: T[], arr2: T[]): T[] {
return arr1.concat(arr2);
}

上面示例中,两个参数arr1arr2和返回值都是同一个类型。如果不给出类型参数的值,下面的调用会报错。

1
comb([1, 2], ["a", "b"]); // 报错

上面示例会报错,TypeScript 认为两个参数不是同一个类型。但是,如果类型参数是一个联合类型,就不会报错。

1
comb<number | string>([1, 2], ["a", "b"]); // 正确

上面示例中,类型参数是一个联合类型,使得两个参数都符合类型参数,就不报错了。这种情况下,类型参数是不能省略不写的。

类型参数的名字,可以随便取,但是必须为合法的标识符。习惯上,类型参数的第一个字符往往采用大写字母。一般会使用T(type 的第一个字母)作为类型参数的名字。如果有多个类型参数,则使用 T 后面的 U、V 等字母命名,各个参数之间使用逗号(“,”)分隔。

下面是多个类型参数的例子。

1
2
3
4
5
6
function map<T, U>(arr: T[], f: (arg: T) => U): U[] {
return arr.map(f);
}

// 用法实例
map<string, number>(["1", "2", "3"], (n) => parseInt(n)); // 返回 [1, 2, 3]

上面示例将数组的实例方法map()改写成全局函数,它有两个类型参数TU。含义是,原始数组的类型为T[],对该数组的每个成员执行一个处理函数f,将类型T转成类型U,那么就会得到一个类型为U[]的数组。

总之,泛型可以理解成一段类型逻辑,需要类型参数来表达。有了类型参数以后,可以在输入类型与输出类型之间,建立一一对应关系。

泛型的写法

泛型主要用在四个场合:函数、接口、类和别名。

函数的泛型写法

上一节提到,function关键字定义的泛型函数,类型参数放在尖括号中,写在函数名后面。

1
2
3
function id<T>(arg: T): T {
return arg;
}

那么对于变量形式定义的函数,泛型有下面两种写法。

1
2
3
4
5
// 写法一
let myId: <T>(arg: T) => T = id;

// 写法二
let myId: { <T>(arg: T): T } = id;

接口的泛型写法

interface 也可以采用泛型的写法。

1
2
3
4
5
interface Box<Type> {
contents: Type;
}

let box: Box<string>;

上面示例中,使用泛型接口时,需要给出类型参数的值(本例是string)。

下面是另一个例子。

1
2
3
4
5
6
7
8
9
interface Comparator<T> {
compareTo(value: T): number;
}

class Rectangle implements Comparator<Rectangle> {
compareTo(value: Rectangle): number {
// ...
}
}

上面示例中,先定义了一个泛型接口,然后将这个接口用于一个类。

泛型接口还有第二种写法。

1
2
3
4
5
6
7
8
9
interface Fn {
<Type>(arg: Type): Type;
}

function id<Type>(arg: Type): Type {
return arg;
}

let myId: Fn = id;

上面示例中,Fn的类型参数Type的具体类型,需要函数id在使用时提供。所以,最后一行的赋值语句不需要给出Type的具体类型。

此外,第二种写法还有一个差异之处。那就是它的类型参数定义在某个方法之中,其他属性和方法不能使用该类型参数。前面的第一种写法,类型参数定义在整个接口,接口内部的所有属性和方法都可以使用该类型参数。

类的泛型写法

泛型类的类型参数写在类名后面。

1
2
3
4
class Pair<K, V> {
key: K;
value: V;
}

下面是继承泛型类的例子。

1
2
3
4
5
class A<T> {
value: T;
}

class B extends A<any> {}

上面示例中,类A有一个类型参数T,使用时必须给出T的类型,所以类B继承时要写成A<any>

泛型也可以用在类表达式。

1
2
3
4
5
6
const Container = class<T> {
constructor(private readonly data: T) {}
};

const a = new Container<boolean>(true);
const b = new Container<number>(0);

上面示例中,新建实例时,需要同时给出类型参数T和类参数data的值。

下面是另一个例子。

1
2
3
4
5
6
7
8
9
10
11
class C<NumType> {
value!: NumType;
add!: (x: NumType, y: NumType) => NumType;
}

let foo = new C<number>();

foo.value = 0;
foo.add = function (x, y) {
return x + y;
};

上面示例中,先新建类C的实例foo,然后再定义示例的value属性和add()方法。类的定义中,属性和方法后面的感叹号是非空断言,告诉 TypeScript 它们都是非空的,后面会赋值。

JavaScript 的类本质上是一个构造函数,因此也可以把泛型类写成构造函数。

1
2
3
4
5
6
7
8
9
10
11
type MyClass<T> = new (...args: any[]) => T;

// 或者
interface MyClass<T> {
new (...args: any[]): T;
}

// 用法实例
function createInstance<T>(AnyClass: MyClass<T>, ...args: any[]): T {
return new AnyClass(...args);
}

上面示例中,函数createInstance()的第一个参数AnyClass是构造函数(也可以是一个类),它的类型是MyClass<T>,这里的TcreateInstance()的类型参数,在该函数调用时再指定具体类型。

注意,泛型类描述的是类的实例,不包括静态属性和静态方法,因为这两者定义在类的本身。因此,它们不能引用类型参数。

1
2
3
4
class C<T> {
static data: T; // 报错
constructor(public value: T) {}
}

上面示例中,静态属性data引用了类型参数T,这是不可以的,因为类型参数只能用于实例属性和实例方法,所以报错了。

类型别名的泛型写法

type 命令定义的类型别名,也可以使用泛型。

1
type Nullable<T> = T | undefined | null;

上面示例中,Nullable<T>是一个泛型,只要传入一个类型,就可以得到这个类型与undefinednull的一个联合类型。

下面是另一个例子。

1
2
3
4
type Container<T> = { value: T };

const a: Container<number> = { value: 0 };
const b: Container<string> = { value: "b" };

下面是定义树形结构的例子。

1
2
3
4
5
type Tree<T> = {
value: T;
left: Tree<T> | null;
right: Tree<T> | null;
};

上面示例中,类型别名Tree内部递归引用了Tree自身。

泛型类

类也可以写成泛型,使用类型参数。

1
2
3
4
5
6
7
8
9
class Box<Type> {
contents: Type;

constructor(value: Type) {
this.contents = value;
}
}

const b: Box<string> = new Box("hello!");

上面示例中,类Box有类型参数Type,因此属于泛型类。新建实例时,变量的类型声明需要带有类型参数的值,不过本例等号左边的Box<string>可以省略不写,因为可以从等号右边推断得到。

注意,静态成员不能使用泛型的类型参数。

1
2
3
class Box<Type> {
static defaultContents: Type; // 报错
}
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
35
36
37
38
39
//泛型基本用法
function echo<T> (arg: T):T{
return arg
}

const result = echo(true)

function swap<T,U>(tuple:[T,U]):[U,T]{
return [tuple[1],tuple[0]]
}

const result2 =swap(['string',123])
console.log(result2[1].length)
console.log(result2[0].toString())


//约束泛型
//第一种方式 泛型+Array => T[]
function echoWithArr<T>(arg:T[]):T[]{
console.log(arg.length);
return arg
}
const arrs = echoWithArr([1,2,3])

//第二种方式 interface+extends
interface IWithLength{
length: number
}

function echoWithLength<T extends IWithLength>(arg:T):T{
console.log(arg.length);
return arg

}
//这样任何有length属性的类型都可以泛用
const str = echoWithLength('str') //3
const obj = echoWithLength({length:10}) //10
const arr2 = echoWithLength([1,2,3,4]) //4
//const num3 = echoWithLength(3) 不行,因为没有length

11.泛型在类和接口中的使用

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
class Queue<T>{
private data:T[] =[];
push(item: T){
return this.data.push(item)
}
pop(){
return this.data.shift()
}
}
const queue = new Queue<number>()

queue.push(1)
const poped = queue.pop()

if(poped){
poped.toFixed()
}

interface KeyPair<T,U>{
key:T;
value:U;
}
let kp1:KeyPair<number,string> = {key:1,value:'str'}
let kp2:KeyPair<string,number> = {key:'str',value:123}
let arr:number[] = [1, 2, 3]
let arrTwo: Array<number> = [1,2]

12.类型别名、字面量和交叉类型

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
//类型别名
type PlusType = (x:number,y:number) => number
//声明并具体实现
let l8_sum2: PlusType = (x,y) => x+y
const l8_result2 = l8_sum2(2,3)

//类型别名
type StrOrNumber = string | number
let l8_result3 : StrOrNumber = '123'
l8_result3 = 321


//字面量
const l8_str:'name' = 'name'
const number: 1 = 1
//字面量
type Directions = 'Up' |'Down'|'Left'|'Right'
let toWhere:Directions ='Left'

//交叉类型
interface Iname{
name: string
}
type Person = Iname & {age:number}
let person:Person = {name:'xzj',age:17}

13.声明文件

声明文件里面没有任何的实际实现代码,只有类型声明,比如interface,class,function等

axios.d.ts中:

1
2
3
4
5
interface IAxios{
get:(url:string) => string;
post:(url:string,data:any) => string;
}
declare const axios: IAxios

test.ts中:

1
2
axios.get('url')
axios.post('url',{name:'xzj'})

我们做项目有两种情况,有不需要引入声明文件的包,如axios,只需要npm i axios 就可

另一种为需要引入声明文件,比如jquery,就需要

14.ts内置类型

如Array,Date,Math,Dom和Bom等

15.配置文件–tsconfig.json中

1
2
"noImplicitAny": true, 
//不允许使用any类型