共计 8439 个字符,预计需要花费 22 分钟才能阅读完成。
目录
-
-
- 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.toString();如果不是共有属性则不行,如 length.length,因为 number 类型并没有 length 属性。
但是联合类型的变量被赋值时,会根据类型推论的规则推断出一个类型。
5. 对象类型–接口(interfaces)
在 ts 中,我们使用接口来定义对象类型,接口可用于对类的一部分行为进行抽象,也常用于对象的形状进行描述。
interface IPerson {
name: string;
age: number;
}
let tom: IPerson = {
name: 'Tom',
age: 18
}
** 定义的变量不允许比接口少一些或者多一些属性,赋值是变量的形状必须与接口形状保持一致。
可选属性
当然,又是我们希望每个对象不是完全相同的,那么可以用可选属性(表示该属性可以不存在):
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'
};
上述中,任意属性的值允许是 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]
let arr: number[] = [1,2,3]arr.push('7')
数组泛型定义
可以使用数组泛型 Array
来表示数组:
let arr: Array = [1,2,54,73]
用接口定义()
interface IArr{
[index: number]: number
}
let arr: IArr = [1,2,5,8]
虽然接口可以用来描述数组,但是一般不这么做,因为比较麻烦。
有一种情况例外,就是用它来表示类数组。
类数组
function sum() {
let args: number[] = arguments;
}
上述代码中,arguments
实际上是一个类数组,不能用普通数组方式描述,而需要用接口
function sum() {
let args: {
[index: number]: number;
length: number;
callee: Function;
} = arguments;
}
除了约束当索引的类型是数字时,值的类型必须是数字外,还约束力他有一个 length
属性和 callee
两个属性。
事实上常用的类数组都有自己的接口定义,如 IArguments
, NodeList
, HTMLCollection
等:
function sum() {
let args: IArguments = arguments;
}
interface IArguments {
[index: number]: any;
length: number;
callee: Function;
}
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;
}
但是有时候,我们确实需要再不确定类型的时候就访问某个类型中特有的属性或者方法:
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;
}
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;
}
在这个例子中有一个更合适的方式来判断是不是 ApiError
,那就是使用 instanceof
:
function isApiError(error: Error) {
if (error instanceof ApiError) {
return true;
}
return false;
}
但是有的情况下 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 as any).foo = 1;
注意: 将一个变量断言为 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;
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);
}
若你使用了这种双重断言,那么十有八九是非常错误的,它很可能会导致运行时错误。
除非迫不得已,千万别用双重断言。
类型断言 vs 类型转换
类型断言只会影响 ts 编译时的类型,断言语句在编译结果中会被删除:
function toBoolean(something: any): boolean {
return something as boolean;
}
toBoolean(1);
所以类型断言并不会影响到变量的类型,如果要进行类型转换,需要直接调用类型转换的方法:
function toBoolean(something: any): boolean {
return Boolean(something);
}
toBoolean(1);
类型断言 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();
区别:
interface Animal {
name: string;
}
interface Cat {
name: string;
run(): void;
}
const animal: Animal = {
name: 'tom'
};
let tom = animal as Cat;
let tom: Cat = animal;
它们的核心区别就在于:
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
函数添加了一个泛型
,我们可以更加规范的实现对 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) {
});
用 TypeScript 写 Node.js
Node.js 不是内置对象的一部分,如果想用 TypeScript 写 Node.js,则需要引入第三方声明文件:
npm install @types/node --save-dev
原文地址: TypeScript 基础语法 /ts 入门