官网对TypeScript的定义:
Typed JavaScript at Any Scale.
添加了类型系统的JavaScript,适用于任何规模的项目。
JavaScrip的特性:
TypeScript 的类型系统,在很大程度上弥补了 JavaScript 的缺点。
类型系统按照【类型检查的时机】来分类,可以分为动态类型和静态类型:
类型系统按照【是否允许隐士转换】来分类,可以分为强类型和弱类型。
let foo = 1;
foo.split(' ');
// Uncaught TypeError: foo.split is not a function
// 运行时会报错(foo.split 不是一个函数),造成线上 bug`
let foo = 1;
foo.split(' ');
// Property 'split' does not exist on type 'number'.
// 编译时会报错(数字没有 split 方法),无法通过编译
'1'
,加号+
被识别为字符串拼接,打印结果是字符串'11'
console.log(1 + '1') // 打印出字符串 '11'
print(1 + '1') # TypeError: unsupported operand type(s) for +: 'int' and 'str'
print(str(1) + '1') # 打印出字符串 '11'
TypeScript的类型系统体现了它的核心设计理念:在完整保留 JavaScript 运行时行为的基础上,通过引入静态类型系统来提高代码的可维护性,减少可能出现的 bug
命令行工具安装TypeScript:
npm install -g typescript
编译一个TypeScript文件:
tsc hello.ts
编辑器:
JavaScript的类型分为两种:
null
、undefined
以及ES6中的新类型symbol
和ES10中的新类型Bright
返回的是布尔值:
boolean
定义布尔值类型:let isDone: boolean = false;
Boolean
也可以返回一个boolean
类型:let createdByBoolean: boolean = Boolean(1);
返回的是一个对象(包装对象):
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.
new Boolean
返回的是一个Boolean
对象:let createdByNewBoolean: Boolean = new Boolean(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;
编译结果:
var decLiteral = 6;
var hexLiteral = 0xf00d;
// ES6 中的二进制表示法
var binaryLiteral = 10;
// ES6 中的八进制表示法
var octalLiteral = 484;
var notANumber = NaN;
var infinityNumber = Infinity;
其中, 0b1010
和0o744
是ES6中的二进制和八进制表示法,它们会被编译为十进制数字。
使用string
定义字符串类型:
添加链接描述
let myName: string = 'Jerry';
let myAge: number = 15;
//模板字符串
let sentence:string = `hello,this is ${myName}.I'll be ${myAge + 1} years old next month.`;
编译结果:
var myName = 'Jerry';
var myAge = 15;
// 模板字符串
var sentence = "Hello, my name is " + myName + ".
I'll be " + (myAge + 1) + " years old next month.";
其中, 反引号
用来定义ES6中的模板字符串,${expr}
用来在模板字符串中嵌入表达式。
JavaScript中没有空值(Void)的概念,在TypeScript中,可以用void
表示没有任何返回值的函数:
function alertName(): void {
alert('My name is Tom');
}
声明一个void
类型的变量没有什么用,因为你只能将他赋值给undefined
和null
(只在 strictNullChecks——TypeScript中严格的空校验 未指定时):
let unusable: void = undefined;
在TypeScript中,可以使用null
和undefined
来定义这两个原始数据类型:
let u: undefined = undefined;
let n: null = null;
与void
的区别:undefined
和null
是所有类型的子类型,可以把undefined
和null
赋值给其他类型的变量
也就是说,undefined
类型的变量,可以赋值给number
类型的变量:
// 这样不会报错
let num: number = undefined; //非严格模式
let num: number = undefined; // 严格模式下,会报错: Type 'undefined' is not assignable to type 'number'
// 这样也不会报错
let u: undefined; //非严格模式
let num: number = u; //非严格模式
/**
非严格模式下,变量的值可以为 undefined 或 null
而严格模式下,变量的值只能为 undefined
**/
而void
类型的变量不能赋值给number
类型的变量:
let u: void;
let num: number = u;
// Type 'void' is not assignable to type 'number'.
任意值(any
)用来表示允许赋值为任意类型。
如果是一个普通类型,在赋值过程中改变类型是不被允许的:
let myFavoriteNumber: string = 'Jerry';
myFavoriteNumber = 8;
// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.
但如果是any
类型,则允许被赋值为任意类型:
let myFavoriteNumber: any = 'Jerry';
myFavoriteNumber = 8;
在任意值上访问任何属性都是允许的:
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 = 'Jerry';
something = 7;
something.setName('Tom');
等价于:
let something: any;
something = 'Jerry';
something = 7;
something.setName('Tom');
如果没有明确地指定类型,那么TypeScript会依照类型推论(type Inference)的规则推断出一个类型。
TypeScript会在没有明确的指定类型的时候推测出一个类型,这就是类型推论。
以下代码虽然没有指定类型,但会在编译的时候报错:
TS会自动推测出myFavoriteNumber的类型为string
,所以给它赋值为number
会报错
let myFavoriteNumber = 'seven';
myFavoriteNumber = 7;
// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.
实际上,它等价于:
let myFavoriteNumber: string = 'seven';
myFavoriteNumber = 7;
// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.
如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成any类型而完全不被类型检查。
let myFavoriteNumber; //等价于 let myFavoriteNumber : any ;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;
联合类型(Union Type)表示取值可以为多种类型中的一种。
let myFavoriteNumber: string | number;
myFavoriteNumber = 'Jerry';
myFavoriteNumber = 8;
myFavoriteNumber = true;
// index.ts(2,1): error TS2322: Type 'boolean' is not assignable to type 'string | number'.
// Type 'boolean' is not assignable to type 'number'.
/**
let myFavoriteNumber: string | number 的含义是,允许 myFavoriteNumber 的类型是 string 或者 number,但是不能是其他类型
**/
联合类型使用|
分隔每个类型。
length
不是 string
和 number
的共有属性,所以会报错function getLength(something: string | number): number {
return something.length;
}
// index.ts(2,22): error TS2339: Property 'length' does not exist on type 'string | number'.
// Property 'length' does not exist on type 'number'.
string
和 number
的共有属性是没问题的function getString(something: string | number): string {
return something.toString();
}
let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
//myFavoriteNumber 被推断成了 string,访问它的 length 属性不会报错
console.log(myFavoriteNumber.length); // 5
myFavoriteNumber = 7;
// myFavoriteNumber 被推断成了 number,访问它的 length 属性会报错
console.log(myFavoriteNumber.length); // 编译时报错
// index.ts(5,30): error TS2339: Property 'length' does not exist on type 'number'.
在TS中,我们使用接口(Interfaces)来定义对象的类型。
I
前缀。这是一个简单的例子
//定义一个接口Person
interface Person {
name: string;
age: number;
}
/**
定义一个变量tom,它的类型是Person
约定tom的形状必须和接口Person一致
**/
let tom: Person = {
name: 'Tom',
age: 25
};
定义的变量比接口少/多一些属性是不允许的:
interface Person {
name: string;
age: number;
}
/**
变量tom比接口Person少一个age属性,这是不允许的
**/
let tom: Person = {
name: 'Tom'
};
/**
报错:
index.ts(6,5): error TS2322: Type '{ name: string; }' is not assignable to type 'Person'.
Property 'age' is missing in type '{ name: string; }'.
**/
// 变量jerry比接口多一个gender属性,这是不允许的
let jerry: Person = {
name: 'Tom',
age: 25,
gender: 'male'
};
/**
报错:
index.ts(9,5): error TS2322: Type '{ name: string; age: number; gender: string; }' is not assignable to type 'Person'.
Object literal may only specify known properties, and 'gender' does not exist in type 'Person'.
**/
可见,赋值的时候,变量的形状必须和接口的形状保持一致!
可选属性的含义是:该属性可以不存在
有时我们希望不要完全匹配一个形状,那么可以用可选属性。
interface Person {
name: string;
age?: number;
}
let tom: Person = {
name: 'Tom'
};
let jerry: Person = {
name: 'Tom',
age: 25
};
但是,仍然不允许添加未定义的属性:
interface Person {
name: string;
age?: number;
}
let tom: Person = {
name: 'Tom',
age: 25,
gender: 'male' //添加了接口Person中未定义的属性gender
};
/**
报错:
examples/playground/index.ts(9,5): error TS2322: Type '{ name: string; age: number; gender: string; }' is not assignable to type 'Person'.
Object literal may only specify known properties, and 'gender' does not exist in type 'Person'.
**/
有时候我们希望一个接口允许有任意的属性,可以使用如下方式:
interface Person {
name: string;
age?: number;
[propName: string]: any; //使用 [propName: string] 定义了任意属性取 string 类型的值
}
let tom: Person = {
name: 'Tom',
gender: 'male'
};
需要注意的是,一旦定义了任意属性,那么确定属性和可选属性的类型都必须是任意属性的类型的子集
interface Person {
name: string;
age?: number;
[propName: string]: string; //任意属性的值允许string类型的属性
}
let tom: Person = {
name: 'Tom',
age: 25, //age属性是number类型,number不是string的子属性,所以报错
gender: 'male'
};
/**
报错:
index.ts(3,5): error TS2411: Property 'age' of type 'number' is not assignable to string index type 'string'.
index.ts(7,5): error TS2322: Type '{ [x: string]: string | number; name: string; age: number; gender: string; }' is not assignable to type 'Person'.
Index signatures are incompatible.
Type 'string | number' is not assignable to type 'string'.
Type 'number' is not assignable to type 'string'.
**/
在报错信息中可以看出,此时 { name: 'Tom', age: 25, gender: 'male' }
的类型被推断成了 { [x: string]: string | number; name: string; age: number; gender: string; }
,这是联合类型和接口的结合。
一个接口只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型:
interface Person {
name: string;
age?: number;
[propName: string]: string | number; //联合类型
}
let tom: Person = {
name: 'Tom',
age: 25,
gender: 'male'
};
因为一旦定义了任意属性,确定属性和可选属性的类型必须是任意属性的子集,所以当接口有多种类型的属性时,可以将任意属性定义为联合类型。
有时候我们希望对象中的一些字段只能在创建的时候被赋值,那么可以用readonly
定义只读属性
interface Person {
readonly id: number; //定义只读属性
name: string;
age?: number;
[propName: string]: any;
}
let tom: Person = {
id: 89757,
name: 'Tom',
gender: 'male'
};
tom.id = 9527; //给只读属性赋值,会报错
//index.ts(14,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.
interface Person {
readonly id: number;
name: string;
age?: number;
[propName: string]: any;
}
//第一次给对象赋值,会报错,因为没有给id属性赋值(报错1)
let tom: Person = {
name: 'Tom',
gender: 'male'
};
//给只读属性赋值,会报错(报错2)
tom.id = 89757;
/**
报错1:
index.ts(8,5): error TS2322: Type '{ name: string; gender: string; }' is not assignable to type 'Person'.
Property 'id' is missing in type '{ name: string; gender: string; }'.
报错2:
index.ts(13,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.
**/
在TS中,数组类型有多种定义方式,比较灵活。
最简单的方法是使用[类型+方括号]来表示数组:
//[类型+方括号]表示法
let fibonacci: number[] = [1, 1, 2, 3, 5];
//数组的项中不允许出现其他的类型
let fibonacci2: number[] = [1, '1', 2, 3, 5];
//数组的一些方法的参数也会根据数组在定义时约定的类型进行限制
let fibonacci3: number[] = [1, 1, 2, 3, 5];
fibonacci3.push('8'); //Argument of type '"8"' is not assignable to parameter of type 'number'.
我们也可以使用数组泛型(Array Generic)**Array
**来表示数组
let fibonacci: Array<number> = [1, 1, 2, 3, 5];
也可以用接口来描述数组:
interface NumberArray {
[index: number]: number; //任意属性
}
//NumberArray 表示:只要索引的类型是数字时,那么值的类型必须是数字
let fibonacci: NumberArray = [1, 1, 2, 3, 5];
虽然接口也可以用来描述数组,但我们一般不会这么做,因为这种方式比前两种复杂多了。
不过有一种情况例外,那就是用它来表示类数组(伪数组)。
length
属性,其他属性(索引)为非负整数(对象中的索引会被当作字符串来处理,可以当作是个非负整数串来理解)length
属性,也有0
、1
、2
等属性的对象,看起来就像数组一样,但不是数组Object
,而真实的数组是一个Array
arguments
Array.isArrar()
来判断类数组(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.
}
arguments
实际上是一个类数组,不能用普通的数组的方式来描述,而应该用接口
function sum() {
let args: {
[index: number]: number; //约束当索引的类型、值的类型必须是数字
length: number; //约束类数组必须存在length属性
callee: Function; //约束类数组存在callee属性
} = arguments;
}
事实上,常用的类数组都有自己的接口定义,如IArguments
、NodeList
、HTMLCollection
等
function sum() {
let args: IArguments = arguments;
}
IArguments
是 TypeScript 中定义好了的类型,它实际上就是:interface IArguments {
[index: number]: any;
length: number;
callee: Function;
}
疑问(还是不理解)
interface IArguments {
[index: number]: number;
length: number;
callee: Function;
}
string
时,那么确定属性和可选属性的类型都必须为它的类型的子集。number
类型的任意属性签名不会影响其他 string 类型的属性签名number
类型的签名指定的值类型必须是 string
类型的签名指定的值类型的子集。
/**
虽然指定了 number 类型的任意属性的类型是 string,
但 length 属性是 string 类型的签名,所以不受前者的影响。
**/
type Arg = {
[index: number]: string
length: number
}
/**
如果接口定义了 string 类型的任意属性签名,
它不仅会影响其他 string 类型的签名,也会影响其他 number 类型的签名。
**/
interface Person {
name: string;
age?: number;
[propName: string]: string; //任意属性的值允许string类型的属性
}
一个比较常见的做法是:用any
表示数组中允许出现任意类型:
let list: any[] = ['xcatliu', 25, { website: 'http://xcatliu.com' }];
一个函数有输入和输出,要在TS中对其进行约束,需要把输入和输出都考虑到。
在JavaScript中,有两种常见的定函数的方式:
// 函数声明(Function Declaration)
function sum(x, y) {
return x + y;
}
// 函数表达式(Function Expression)
let mySum = function (x, y) {
return x + y;
};
函数声明的类型定义如下:
function sum(x: number, y: number): number {
return x + y;
}
注意,输入多余的(或少于要求的)参数,是不被允许的。
function sum(x: number, y: number): number {
return x + y;
}
sum(1, 2, 3); //输入多余的参数:error TS2346: Supplied parameters do not match any signature of call target.
sum(1); //输入少于要求的参数:error TS2346: Supplied parameters do not match any signature of call target.
易错
对一个函数表达式(Function Expression)类型不正确的定义:
let mySum = function (x: number, y: number): number {
return x + y;
};
muSum
,是通过赋值操作进行类型推论而推断出来的正确的写法
//手动给mySum添加类型
let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
return x + y;
};
注意,不要混淆了TS中的=>
和ES6中的=>
:
=>
用来表示函数的定义,左边是输入的类型,需要用括号括起来,右边是输出的类型。=>
是箭头函数,具体参考ES6中的箭头函数。我们也可以使用接口 的方式来定义一个函数需要符合的形状:
interface SearchFunc {
(source: string, subString: string): boolean;
}
//函数表达式+接口定义函数的类型
let mySearch: SearchFunc; //对等号左边进行类型限制
mySearch = function(source: string, subString: string) {
return source.search(subString) !== -1;
}
采用函数表达式+接口定义函数类型的方式时,对等号左边进行类型限制,可以保证以后对函数名赋值时,保证参数个数、参数类型、返回值类型不变。
用?
表示可选的参数。
//lastName为可选的参数
function buildName(firstName: string, lastName?: string) {
if (lastName) {
return firstName + ' ' + lastName;
} else {
return firstName;
}
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');
注意:可选参数必须接在必选参数后面。
// 可选参数firstName应该写在最后面
function buildName(firstName?: string, lastName: string) {
if (firstName) {
return firstName + ' ' + lastName;
} else {
return lastName;
}
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName(undefined, 'Tom');
// 报错: error TS1016: A required parameter cannot follow an optional parameter.
在ES6中,我们允许给函数的参数添加默认值,TS会将添加了默认值的参数识别为可选参数。
//将添加了默认值的参数lastName识别为可选参数
function buildName(firstName: string, lastName: string = 'Cat') {
return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');
添加了默认值的参数被TS识别为可选参数,但不受【可选参数必须接在必须参数后面】的限制:
//添加了默认值的参数不受【可选参数位置】的约束
function buildName(firstName: string = 'Tom', lastName: string) {
return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let cat = buildName(undefined, 'Cat');
默认参数更多用法,参考ES6中函数参数的默认值
在ES6中,可以使用...rest
的方式获取函数中的剩余参数(rest参数):
function push(array, ...items) {
items.forEach(function(item) {
array.push(item);
});
}
let a: any[] = [];
push(a, 1, 2, 3);
在TS中的写法:
// item一个数组,所以用数组的类型来定义它
function push(array: any[], ...items: any[]) {
items.forEach(function(item) {
array.push(item);
});
}
let a = [];
push(a, 1, 2, 3);
注意:rest参数只能是最后一个参数
ES6中的剩余参数详解:
定义
...变量名
),用于获取函数的多余参数,这样就不需要使用arguments
对象了...val
),该变量(val
)将多余的参数放入数组中rest参数
与arguments
对象
arguments
对象不是数组,而是一个类似数组的对象。为了使用数组的方法,必须使用Array.from
先将其转为数组rest参数
是一个真正的数组,数组特有的方法都可以使用rest参数
和函数的length
属性
length
属性,不包括rest参数
(function(a) {}).length // 1
(function(...a) {}).length // 0
(function(a, ...b) {}).length // 1
函数重载:允许一个函数接受不同数量或类型的参数时,作出不同的处理。(类似C++的重载)
注意,TS 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。
//函数定义
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('');
}
}
类型断言(Type Assertion)可以用来手动指定一个值的类型。
两种声明方式:
值 as 类型
<类型>值
的语法在tsx中表示的是一个ReactNode
,在ts中除了表示类型断言外,也可能表示的是一泛型
。值 as 类型
的语法。类型断言的常见用途有以下几种:
当TS不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问该联合类型的所有类型中共有的属性或方法:
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
function getName(animal: Cat | Fish) {
return animal.name; //访问所有类型中共有的属性
}
应用
有时候,我们需要在还不确定类型时候就访问其中一个类型特有的属性或方法:
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
function isFish(animal: Cat | Fish) {
if (typeof animal.swim === 'function') { //获取animal.swim会报错
return true;
}
return false;
}
/**
报错信息:
error TS2339: Property 'swim' does not exist on type 'Cat | Fish'.
Property 'swim' does not exist on type 'Cat'.
**/
解决方法——使用类型断言,将animal
断言为Fish
:
function isFish(animal: Cat | Fish) {
if (typeof (animal as Fish).swim === 'function') { //将animal断言为Fish
return true;
}
return false;
}
注意:类型断言只能够【欺骗】TS编译器,但无法避免运行时的错误,滥用类型断言可能会导致运行时的错误:
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
function swim(animal: Cat | Fish) {
(animal as Fish).swim();
}
const tom: Cat = {
name: 'Tom',
run() { console.log('run') }
};
swim(tom);
// 运行时报错
//Uncaught TypeError: animal.swim is not a function`
(animal as Fish).swim()
这段代码隐藏了animal
可能为Cat
的情况,将animal
直接断言为Fish
。TS编译器相信了我们的断言,所以在调用swim()
的时候没有编译错误。但是swim
函数接受的参数类型是Cat | Fish
,一旦传入了Cat
类型的变量,由于Cat
上没有swim
方法,就会导致运行时的错误了。在使用类型断言时,一定要格外小心,尽量避免断言后调用方法或引用深层属性,以减少不必要的运行时错误。
当类之间有继承关系时,类型断言也是很常见的:
isApiError
用来判断传入的参数是不是 ApiError
类型Error
,这样的话这个函数就能接受 Error
或它的子类作为参数Error
中没有 code
属性,故直接获取 error.code
会报错,需要使用类型断言获取 (error as ApiError).code
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;
}
return false;
}
上面的栗子使用instanceof
更加合适,因为ApiError
是一个JS的类,能够通过instanceof
来判断 error
是否是它的实例。
但有的情况下, ApiError
和HttpError
不是一个真正的类,而只是一个TS的接口,接口是一个类型,不是一个真正的值,它在编译结果中会被删除,无法使用instanceof
来做运行时的判断
function isApiError(error: Error) {
if (error instanceof ApiError) {
return true;
}
return false;
}
// 报错: error TS2693: 'ApiError' only refers to a type, but is being used as a value here.
一个小栗子:
window
添加一个属性foo
,我们写的代码如下:window.foo = 1;
// 报错啦: - error TS2339: Property 'foo' does not exist on type 'Window & typeof globalThis'.
window
上不存在属性foo
as any
临时将window
断言为any
类型:(window as any).foo = 1;
在any
类型的变量上,访问任何属性都是允许的。
需要注意的是:将一个变量断言为any
可以说是解决TS类型问题的最后一个手段。
总之,一方面不能滥用as any
,另一方面也不要完全否认它的作用,我们需要在类型的严格性和开发的便利性之间掌握平衡。
遇到any
类型的变量时,我们可以选择无视他,任由它滋生出更多的any
,也可以选择改进它,通过类型断言及时的把any
断言为精确的类型。
举个栗子:
getCacheData
,它的返回值是any
:function getCacheData(key: string): any {
return (window as any).cache[key];
}
function getCacheData(key: string): any {
return (window as any).cache[key];
}
interface Cat {
name: string;
run(): void;
}
// 我们调用完 getCacheData 之后,立即将它断言为 Cat 类型
const tom = getCacheData('tom') as Cat;
tom.run();
写在最前面——若你使用了这种双重断言,那么十有八九是非常错误的,它很可能会导致运行时错误。除非迫不得已,千万别用双重断言
既然,任何类型都可以被断言为any,any可以被断言为任何类型,那我们是不是可以用双重断言来将任何一个类型断言为任何另一个类型
举个栗子:
interface Cat {
run(): void;
}
interface Fish {
swim(): void;
}
function testCat(cat: Cat) {
return (cat as any as Fish); //如果直接使用 cat as Fish 肯定会报错,因为 Cat 和 Fish 互相都不兼容
类型断言只会影响TS编译时的类型,类型断言语句在编译结果中会被删除
类型断言不是类型转换,它不会真的影响到变量的类型
几个栗子:
something
断言为boolean
,虽然可以通过编译,但是并没有什么用function toBoolean(something: any): boolean {
return something as boolean;
}
toBoolean(1);
// 返回值为 1
编译后的:
function toBoolean(something) {
return something;
}
toBoolean(1); //类型断言不会真的影响变量的类型
// 返回值为 1
function toBoolean(something: any): boolean {
return Boolean(something);
}
toBoolean(1); //浅浅做个对比
// 返回值为 true
举个栗子说说类型断言和类型声明的区别:
as Cat
将 any
类型断言为了 Cat
类型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();
//将 tom 声明为 Cat,然后再将 any 类型的 getCacheData('tom') 赋值给 Cat 类型的 tom
const tom: Cat = getCacheData('tom');
tom.run();
interface Animal {
name: string;
}
interface Cat {
name: string;
run(): void;
}
const animal: Animal = {
name: 'tom'
};
// 由于 Animal 兼容 Cat,故可以将 animal 断言为 Cat 赋值给 tom
let tom = animal as Cat;
//报错原因:Animal 可以看作是 Cat 的父类,当然不能将父类的实例(animal)赋值给类型为子类(Cat)的变量
let tom: Cat = animal;
// - error TS2741: Property 'run' is missing in type 'Animal' but required in type 'Cat'.
animal
断言为Cat
,只需要满足 Animal
兼容Cat
或Cat
兼容 Animal
即可animal
赋值给tom
,需要满足Cat
兼容 Animal
才行getCacheData('tom')
是any
类型,any
兼容Cat
,Cat
也兼容any
,所以const tom = getCacheData('tom') as Cat;
//等价于
//const tom: Cat = getCacheData('tom');
通过上面的例子,我们知道了类型声明比类型断言更加严格,所以为了增加代码的质量,我们最好优先使用类型声明,这也比类型断言的as
语法更优雅。
当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。
declare var
声明全局变量declare function
声明全局方法declare class
声明全局类declare enum
声明全局枚举类型declare namespace
声明(含有子属性的)全局对象interface
和 type
声明全局类型export
导出变量export namespace
导出(含有子属性的)对象export default
ES6 默认导出export =
commonjs 导出模块export as namespace
UMD 库声明全局变量declare global
扩展全局变量declare module
扩展模块///
三斜线指令假如我们想要使用第三方库jQuery,一种常见的方式是在html中通过标签引入jQuery,然后就可以使用全局变量
$
或jQuery
了。
我们通常这样获取一个id
为foo
的元素:
$('#foo');
// or
jQuery('#foo');
但在TS中,编译器并不知道$
或jQuery
是什么:
jQuery('#foo');
// ERROR: Cannot find name 'jQuery'.
这时,我们需要使用declare var
来定义它的类型:
declare var jQuery: (selector: string) => any;
jQuery('#foo');
注意:上例中,declare var
并没有真的定义一个变量,只是定义了全局变量 jQuery
的类型,仅仅会用于编译时的检查,在编译结果中会被删除。
上例的编译结果为:jQuery('#foo');
除了 declare var
之外,还有其他很多种声明语句,将会在后面详细介绍。
.d.ts
为后缀一般来说,ts会解析项目中所有的*.ts
文件,当然也包括.d.ts
结尾的文件。加入无法解析,那么可以检查下tsconfig.json
中的files
、include
和exclude
配置,确保其包含了.d.ts
文件。
推荐使用@types
统一管理第三方库的声明文件。
@types
的使用方式很简单,直接使用npm
安装对应的声明模块即可。
以jQuery举例:npm install @types/jquery --save-dev
当一个第三方库没有提供声明文件时,我们就需要自己书写声明文件了。
在不同的场景下,声明文件的内容和使用方式会有所区别。
库的使用场景主要有以下几种:
标签引入第三方库,注入全局变量import foo from 'foo'
导入,符合 ES6 模块规范
标签引入,又可以通过 import
导入
标签引入后,改变一个全局变量的结构npm
包或 UMD
库后,改变一个全局变量的结构