目录
- 1.原始类型
- 2.任意值any
- 3.类型推断
- 4. 联合对象
- 5.对象类型--接口(interfaces)
- 可选属性
- 任意属性
- 只读属性
- 6.数组类型
- [类型+方括号]定义
- 数组泛型定义
- 用接口定义()
- 类数组
- any在数组中的应用
- 7.函数的类型
- 函数声明
- 函数表达式
- 用接口定义函数的形状
- 可选参数
- 参数默认值
- 剩余参数
- 重载
- 8.类型断言
- 类型断言的用途
- 1. 将一个联合类型断言为其中一个类型
- 将一个父类断言为更加具体的子类
- 将任何一个类型断言为any
- 类型断言的限制
- 双重断言
- 类型断言 vs 类型转换
- 类型断言 vs 类型声明
- 类型断言 vs 泛型
- 9.内置对象
- ECMAScript 的内置对象
- DOM 和 BOM 的内置对象
- 用 TypeScript 写 Node.js
1.原始类型
ts基础类型:string、number、boolean、null、undefined、symbol、bigInt
ts中可以使用void表示无返回值的函数
null、undefined与void的区别: undefined和null是所有类型的子类型、所以undefined类型变量可以赋值给number类型变量;但是void类型变量不能赋值给number。
2.任意值any
在any类型上访问任何属性和方法都是允许的,返回的内容类型都是any。
3.类型推断
如果没有明确指定类型,那么ts会按照类型推论推断出一个类型。如:
let name = '小米';
// 等价于
let name: string = '小米'
※: 如果定义时没有赋值,不管之后有没有赋值,都会被推断成any类型,不会被类型检查。
4. 联合对象
联合对象 表示取值可以为多种类型中的一种,通过|
分割每个类型
let length: string | number
// 含义:允许length类型是string或者number,但是不能是其他类型
当不确定一个联合类型的变量到底是哪个类型时,我们只能访问此类型的所有类型共有的属性或者方法,如length.toString();如果不是共有属性则不行,如length.length,因为number类型并没有length属性。
但是联合类型的变量被赋值时,会根据类型推论的规则推断出一个类型。
5.对象类型–接口(interfaces)
在ts中,我们使用接口来定义对象类型,接口可用于对类的一部分行为进行抽象,也常用于对象的形状进行描述。
interface IPerson {
name: string;
age: number;
}
let tom: IPerson = {
name: 'Tom',
age: 18
}
// 接口一般首字母大写。有些编程语音会建议接口名加上`I`前缀
** 定义的变量不允许比接口少一些或者多一些属性,赋值是变量的形状必须与接口形状保持一致。
可选属性
当然,又是我们希望每个对象不是完全相同的,那么可以用可选属性(表示该属性可以不存在):
interface IPerson {
name: string;
age?: number;
}
let tom: IPerson = {
name: 'Tom'
}
let jerry: IPerson = {
name: 'Jerry',
age: 18
}
任意属性
有时希望一个接口允许有任意属性:
interface IPerson {
name: string;
age?: number;
[propName: string]: any;
}
let tom: IPerson = {
name: 'Tom',
gender: 'male'
};
注意: 一旦定义了任意属性,那么确定属性和可选属性的类型都必须是他的类型的子集:
interface IPerson {
name: string;
age?: number;
[propName: string]: string;
}
let tom: IPerson = {
name: 'Tom',
age: 25,
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 'IPerson'.
// Index signatures are incompatible.
// Type 'string | number' is not assignable to type 'string'.
// Type 'number' is not assignable to type 'string'.
上述中,任意属性的值允许是string,但是可选属性age的值却是number,number不是string类型的子属性,所以报错了。
一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型:[propNam: string]: string | number。
只读属性
有时候希望对象中某些字段只能在创建时被赋值,那么可以用readonly
定义只读属性:
interface IPerson {
readonly id: number;
name: string;
age?: number;
[propName: string]: any;
}
let tom: IPerson = {
id: 123,
name: 'Tom',
gender: 'male'
};
tom.id = 456;
注: 只读的约束存在于第一次初始化对象,而不是第一次给只读属性赋值,即使初始化对象时没有给只读属性赋值,后续也是不允许修改的。
6.数组类型
在ts中,数组类型定义有多种方式,比较灵活
[类型+方括号]定义
let arr: number[] = [1,1,1,3,5,6]
// 数组项中不允许出现其他类型, 如:
let arr: number[] = [1,1,'1',3,5,6]
// Type 'string' is not assignable to type 'number'.
// 同理,数组的一些方法也会受到数组类型的限制,如:
let arr: number[] = [1,2,3]arr.push('7')
// push 方法只允许传入 number 类型的参数,但是却传了一个字符串类型的‘7’当作参数,所以报错了
数组泛型定义
可以使用数组泛型Array<类型>
来表示数组:
let arr: Array = [1,2,54,73]
用接口定义()
interface IArr{
[index: number]: number
}
let arr: IArr = [1,2,5,8]
// IArr表示:"[index: number]"--只要索引类型是数字时,
// ":number"--那么值的类型必须是数字
虽然接口可以用来描述数组,但是一般不这么做,因为比较麻烦。
有一种情况例外,就是用它来表示类数组。
类数组
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;
callee: Function;
} = arguments;
}
// 这里的length和callee之所以不会报错,是因为虽然指定了'number'类型的任意属性类型时'length'
// 但是‘length’属性时’string‘类型的签名,所以不受前者影响。
除了约束当索引的类型是数字时,值的类型必须是数字外,还约束力他有一个length
属性和callee
两个属性。
事实上常用的类数组都有自己的接口定义,如 IArguments
, NodeList
, HTMLCollection
等:
function sum() {
let args: IArguments = arguments;
}
// IArguments是内置对象,TypeScript 中定义好了的类型,它实际上就是:
interface IArguments {
[index: number]: any;
length: number;
callee: Function;
}
any在数组中的应用
// 常见用法,用any表示数组中允许出现任意类型:
let arr: any[] = [1,'test',{link:'http://baidu.com'}]
7.函数的类型
函数声明
在 JavaScript 中,有两种常见的定义函数的方式——函数声明(Function Declaration)和函数表达式(Function Expression):
// 函数声明
function sum(x, y) {
return x + y;
}
// 函数表达式
let mySum = function (x, y) {
return x + y;
};
在ts中,函数的输入和输入,都要对其进行约束:
function sum(x: number, y: number): number {
return x + y;
}
注意: 输入多余(或者少于要求的)参数,都是不被允许的:
function sum(x: number, y: number): number {
return x + y;
}
sum(1, 2, 3);
function sum(x: number, y: number): number {
return x + y;
}
sum(1);
// 以上两种都是错误的使用方式,因为参数多余或缺少了
函数表达式
如果使用函数表达式定义,可能会写成这样:
let mySum = function (x: number, y: number): number {
return x + y;
};
编译是可以通过的,但是实际上面代码支队等号右侧的匿名函数进行了类型限制,而等号左面是通过赋值操作进行类型推论推断出来的。如果需要手动添加mySum类型,需要这样写:
let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
return x + y;
};
// =>表示函数的定义,左边是输入类型,需要用括号括起来,邮编是输出类型
用接口定义函数的形状
我们也可以使用接口的方式来定义一个函数需要符合的形状:
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;mySearch = function(source: string, subString: string) {
return source.search(subString) !== -1;
}
// 采用函数表达式|接口定义函数的方式时,对等号左侧进行类型限制
// 可以保证以后对函数名赋值时保证参数个数、参数类型、返回值类型不变。
可选参数
与接口中的可选属性类型,我们可以用?
表示可选的参数
function buildName(firstName: string, lastName?: string) {
if (lastName) {
return firstName + ' ' + lastName;
} else {
return firstName;
}
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');
注意: 可选参数必须再必须参数后面,换句话说,可选参数后面不能出现必须参数。
参数默认值
允许给函数的参数添加默认值,TypeScript 会将添加了默认值的参数识别为可选参数:
而且此时就不受「可选参数必须接在必需参数后面」的限制了,因为参数存在了默认值:
function buildName(firstName: string = 'Tom', lastName: string) {
return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let cat = buildName(undefined, 'Cat');
剩余参数
es6中,可以使用...参数名
的方式获取函数中剩余参数:
function push(array, ...items) {
items.forEach(function(item) {
array.push(item);
});
}
let a: any[] = [];push(a, 1, 2, 3);
注意,剩余参数只能是最后一个参数。
重载
重载允许一个函数接受不同数量或类型的参数时,作出不同的处理。
比如,我们需要实现一个函数 reverse
,输入数字 123
的时候,输出反转的数字 321
,输入字符串 'hello'
的时候,输出反转的字符串 'olleh'
。
可以通过联合类型来实现:
function reverse(x: number | string): number | string | void {
if (typeof x === 'number') {
return Number(x.toString().split('').reverse().join(''));
} else {
return x.split('').reverse().join('');
}
}
但是,这样有一个缺点,不能精确表达出,输入为数字时,输出也应该为数字;输入为字符串的时候,输出也应该为字符串。
那么,可以使用重载定义多个reverse的函数类型:
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 {
return x.split('').reverse().join('');
}
}
上例中,我们重复定义了多次函数 reverse
,前几次都是函数定义,最后一次是函数实现。在编辑器的代码提示中,可以正确的看到前两个提示。
注意,TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。
8.类型断言
类型断言可以用来手动指定一个值的类型。
语法:
值 as 类型
// 或
<类型>值
类型断言的用途
1. 将一个联合类型断言为其中一个类型
由于使用联合类型时,我们只能访问此联合类型的所有类型中共有的属性或方法:
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
function getName(animal: Cat | Fish) {
return animal.name;
}
// 我们只能访问animal的name属性,因为他是两种类型共有的属性
但是有时候,我们确实需要再不确定类型的时候就访问某个类型中特有的属性或者方法:
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
function isFish(animal: Cat | Fish) {
if (typeof animal.swim === 'function') {
return true;
}
return false;
}
// 访问’animal.swim‘时会报错,此时可以使用类型断言,将animal断言成Fish,如:
if (typeof (animal as Fish).swim();.swim === 'function') {
return true;
}
上述例子在编译时不会报错,但在运行时会报错,因为ts信任了我们的断言,所以调用swin时不会编译错误,但是如果传入参数时Cat类型变量,就会由于Cat上没有swin方法,导致运行错误。
总之,使用类型断言时一定要格外小心,尽量避免断言后调用方法或引用深层属性,以减少不必要的运行时错误。
将一个父类断言为更加具体的子类
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;
}
// 由于父类 Error 中没有 code 属性,故直接获取 error.code 会报错
// 需要使用类型断言获取 (error as ApiError).code
在这个例子中有一个更合适的方式来判断是不是 ApiError
,那就是使用 instanceof
:
function isApiError(error: Error) {
if (error instanceof ApiError) {
return true;
}
return false;
}
// 因为 ApiError 是一个 JavaScript 的类,能够通过 instanceof 来判断 error 是否是它的实例。
但是有的情况下 ApiError
和 HttpError
不是一个真正的类,而只是一个 TypeScript 的接口(interface
),接口是一个类型,不是一个真正的值,它在编译结果中会被删除,当然就无法使用 instanceof
来做运行时判断了,而需要使用断言:
interface ApiError extends Error {
code: number;
}
interface HttpError extends Error {
statusCode: number;
}
function isApiError(error: Error) {
if (typeof (error as ApiError).code === 'number') {
return true;
}
return false;
}
将任何一个类型断言为any
当我们引用一个在此类型上不存在的属性或方法时,就会报错:
但是有时,我们非常确定这段代码不会出错
window.foo = 1;
// 我们需要在window上添加一个属性foo但是ts在编译时会报错,提示我们window上不存在foo属性
// 所以此时可以使用as any临时将window断言为any类型:
(window as any).foo = 1;
// 因为在any类型的变量上,访问任何属性都是运行的。
注意: 将一个变量断言为 any
可以说是解决 TypeScript 中类型问题的最后一个手段,它极有可能掩盖了真正的类型错误,所以如果不是非常确定,就不要使用 as any
。
尽量不要滥用as any,非常不利于代码后续维护。
类型断言的限制
并不是任何一个类型都可以被断言为任何另一个类型
具体来说,若 A
兼容 B
,那么 A
能够被断言为 B
,B
也能被断言为 A
:
interface Animal {
name: string;
}
interface Cat {
name: string;
run(): void;
}
let tom: Cat = {
name: 'Tom',
run: () => { console.log('run')}
};
let animal: Animal = tom;
// Cat 包含了 Animal 中的所有属性,除此之外,它还有一个额外的方法 run
// TypeScript 并不关心 Cat 和 Animal 之间定义时是什么关系,而只会看它们最终的结构有什么关系
// 所以它与 Cat extends Animal 是等价的:
interface Cat extends Animal {
run(): void;
}
总之,要使得 A
能够被断言为 B
,只需要 A
兼容 B
或 B
兼容 A
即可。
综上所述:
- 联合类型可以被断言为其中一个类型
- 父类可以被断言为子类
- 任何类型都可以被断言为 any
- any 可以被断言为任何类型
- 要使得
A
能够被断言为B
,只需要A
兼容B
或B
兼容A
即可
其实前四种情况都是最后一个的特例。
双重断言
既然:
- 任何类型都可以被断言为 any
- any 可以被断言为任何类型
那么我们是不是可以使用双重断言 as any as Foo
来将任何一个类型断言为任何另一个类型呢?
interface Cat {
run(): void;
}
interface Fish {
swim(): void;
}
function testCat(cat: Cat) {
return (cat as any as Fish);
}
// 直接使用cat as Fish肯定会报错,因为Cat和Fish互都不兼容。
// 但是使用双重断言可以打破「要使得 A 能够被断言为 B,只需要 A 兼容 B 或 B 兼容 A 即可」的限制,将任意类型断言为任意另一个类型。
若你使用了这种双重断言,那么十有八九是非常错误的,它很可能会导致运行时错误。
除非迫不得已,千万别用双重断言。
类型断言 vs 类型转换
类型断言只会影响ts编译时的类型,断言语句在编译结果中会被删除:
function toBoolean(something: any): boolean {
return something as boolean;
// 编译后其实会变成 return something;
}
toBoolean(1);
// 返回值为 1
所以类型断言并不会影响到变量的类型,如果要进行类型转换,需要直接调用类型转换的方法:
function toBoolean(something: any): boolean {
return Boolean(something);
}
toBoolean(1);
// 返回值为 true
类型断言 vs 类型声明
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();
// 类型声明
const tom: Cat = getCacheData('tom');
tom.run();
// 两个结果是几乎一样的,tom在接下来代码中都变成了Cat类型
区别:
interface Animal {
name: string;
}
interface Cat {
name: string;
run(): void;
}
const animal: Animal = {
name: 'tom'
};
let tom = animal as Cat;
// 由于Animal兼容Cat,所以animal断言为Cat赋值给tom
let tom: Cat = animal;
// 这样将会报错,不允许将 animal 赋值为 Cat 类型的 tom
它们的核心区别就在于:
animal
断言为Cat
,只需要满足Animal
兼容Cat
或Cat
兼容Animal
即可animal
赋值给tom
,需要满足Cat
兼容Animal
才行
所以类型声明是比类型断言更加严格的。
所以最好优先使用类型声明,这也比类型断言的 as
语法更加优雅。
类型断言 vs 泛型
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();
// 泛型
const tom = getCacheData('tom');
tom.run();
通过给 getCacheData
函数添加了一个泛型 <T>
,我们可以更加规范的实现对 getCacheData
返回值的约束,这也同时去除掉了代码中的 any
,是最优的一个解决方案。
9.内置对象
js中有很多内置对象,他们可以直接在ts中当作定义好的类型。
ECMAScript 的内置对象
Boolean
、Error
、Date
、RegExp
等。
let b: Boolean = new Boolean(1);
let e: Error = new Error('Error occurred');
let d: Date = new Date();
let r: RegExp = /[a-z]/;
更多的内置对象,可以查看 MDN 的文档。
DOM 和 BOM 的内置对象
Document
、HTMLElement
、Event
、NodeList
等。
let body: HTMLElement = document.body;
let allDiv: NodeList = document.querySelectorAll('div');
document.addEventListener('click', function(e: MouseEvent) {
// Do something
});
用 TypeScript 写 Node.js
Node.js 不是内置对象的一部分,如果想用 TypeScript 写 Node.js,则需要引入第三方声明文件:
npm install @types/node --save-dev