一、环境搭建与编译执行
TypeScript 编写的程序并不能直接通过浏览器运行,我们需要先通过 TypeScript 编译器把 TypeScript 代码编译成 JavaScript 代码。
环境搭建
安装 node.js
TypeScript 的编译器是基于 Node.js 的,所以我们需要先安装 Node.js:node.js官网
安装完成以后,可以通过 终端
或者 cmd
等命令行工具来调用 node:
# 查看当前 node 版本
node -v
复制代码
安装 TypeScript 编译器
通过 NPM
包管理工具安装 TypeScript 编译器:
npm i -g typescript
// 或
yarn add -g typescript
复制代码
安装完成以后,我们可以通过命令 tsc
来调用编译器:
# 查看当前 tsc 编译器版本
tsc -v
复制代码
win10 运行 tsc
报错
以管理员身份运行 PowerShell
, 设置 set-ExecutionPolicy RemoteSigned
编译代码
默认情况下, TypeScript 的文件的后缀为 .ts
// ./src/test.ts
let str: string = 'una';
export {};
复制代码
TypeScript 与 ECMAScript 2015 一样,任何包含顶级
import
或者export
的文件都被当成一个模块。相反地,如果一个文件不带有顶级的import
或者export
声明,那么它的内容被视为全局可见的。
使用我们安装的 TypeScript 编译器 tsc
对 .ts
文件进行编译:tsc ./src/test.ts
。默认情况下会在当前文件所在目录下生成同名的 js 文件。
tsc ./src/test.ts
// 编译文件放在 src 文件夹
复制代码
常用编译选项
--outDir
:指定编译文件输出目录
tsc --outDir ./dist ./src/test.ts
// 编译文件放在dist文件夹下
复制代码
--target
:指定编译的代码版本目标,默认为 ES3
tsc --outDir ./dist --target ES6 ./src/test.ts
复制代码
--watch
:在监听模式下运行,当文件发生改变的时候自动编译
tsc --outDir ./dist --target ES6 --watch ./src/test.ts
复制代码
通过上面几个例子,我们基本可以了解 tsc
的使用了,但是大家应该也发现了,如果每次编译都输入这么一大堆的选项其实是很繁琐的,好在 TypeScript 编译为我们提供了一个更加强大且方便的方式,编译配置文件:tsconfig.json
,我们可以把上面的编译选项保存到这个配置文件中。
编译配置文件
我们可以把编译的一些选项保存在一个指定的 json 文件中,默认情况下 tsc
命令运行的时候会自动
去加载运行命令所在的目录下的 tsconfig.json
文件,配置文件格式如下:
// ./tsconfig.json
{
// 编译选项
"compilerOptions": {
// js 输出目录
"outDir": "./dist",
// 编译输出目标 ES 版本,默认ES3
"target": "ES2015",
// 监听改动
"watch": true,
},
// 需要编译的模块
// ** : 所有目录(包括子目录)
// * : 所有文件,也可以指定类型 *.ts
"include": ["./src/**/*"] // 相对路径
}
复制代码
有了单独的配置文件,我们就可以直接运行:tsc
。
指定加载的配置文件
// ./configs/tsconfig.json
{
// 编译选项
"compilerOptions": {
// js 输出目录
"outDir": "../dist",
// 编译输出目标 ES 版本,默认ES3
"target": "ES2015",
// 监听改动
"watch": true,
},
// 需要编译的模块
// ** : 所有目录(包括子目录)
// * : 所有文件,也可以指定类型 *.ts
"include": ["../src/**/*"] // 相对路径
}
复制代码
删除 ./tsconfig.json 文件,重新创建新的配置文件,注意相对路径发生了改变。
使用 --project
或 -p
指定配置文件目录,会默认加载该目录下的 tsconfig.json
文件:
tsc -p ./configs
复制代码
也可以指定某个具体的配置文件,把配置文件的文件名改为ts.json
:
tsc -p ./configs/ts.json
复制代码
二、类型系统初始
类型系统组成
类型系统包含两个重要组成部分:
- 类型标注(定义、注解) – typing
- 类型检测(检查) – type-checking
类型标注
类型标注就是在代码中给数据(变量、函数(参数、返回值))添加类型说明,当一个变量或者函数(参数)等被标注后不能存储与标注类型不符合的类型。
有了标注, TypeScript 编译器就能按照标注对这些数据进行类型合法检测,各种编辑器、IDE 等就能进行智能提示。
类型检测
顾名思义,就是对数据的类型进行检测。注意这里,重点是类型两字。
类型系统检测的是类型,不是具体值(虽然,某些时候也可以检测值),比如某个参数的取值范围(1-100之间),我们不能依靠类型系统来完成这个检测,它应该是我们的业务层具体逻辑,类型系统检测的是它的值类型是否为数字!
类型标注
在 TypeScript 中,类型标注的基本语法格式为:数据载体:类型
。
基础类型(string,number,boolean)
基础类型包含:string
,number
,boolean
。
-
布尔值 boolean
let isDone: boolean = false; 复制代码
-
数字 number
let decLiteral: number = 6; 复制代码
和 JavaScript 一样,TypeScript 里的所有数字都是浮点数。除了支持十进制和十六进制字面量,TypeScript 还支持 ECMAScript 2015 中引入的二进制和八进制字面量。
let hexLiteral: number = 0xf00d; let binaryLiteral: number = 0b1010; let octalLiteral: number = 0o744; 复制代码
-
字符串 string
let name: string = "bob";
模版字符串:被反引号包围(`),以${ expr }形式嵌入表达。可以定义多行文本和内嵌表达式。
let name: string = `una`; let age: number = 23; let sentence: string = `Hello, my name is ${ name }. I'll be ${ age + 1 } years old next month.`; 复制代码
类型推断:变量初始化时如果没有标识,也会自动识别类型。
空(null)和未定义(undefined)类型
TypeScript 里,undefined 和 null 两者各自有自己的类型分别叫做 undefined 和 null 。这两种类型有且只有一个值,在标注一个变量为 Null 和 Undefined 类型,那就表示该变量不能修改了。
let a: null;
a = null; // ok
a = 1; // error Null类型一旦标准不能修改
复制代码
默认情况下,null 和 undefined 是所有类型的子类型。 就是说你可以把 null 和 undefined 赋值给其它类型的变量。
let a: number;
a = null; // ok
复制代码
如果一个变量声明了,但是未赋值,那么该变量的值为 undefined
,但是如果它同时也没有标注类型的话,默认类型为 any
。
因为 null
和 undefined
都是其它类型的子类型,所以默认情况下会有一些隐藏的问题:
let a:number;
a = null; // 提示:不能将类型'null'分配给类型'number'
a.toFixed(1); // ok(不报错,但实际运行是有问题的)
复制代码
然而,当你指定了 --strictNullChecks
标记,null
和 undefined
只能赋值给 void
和它们自己。 这能避免很多常见的问题。
// ./tsconfig.json
{
"compilerOptions": {
"outDir": "./dist",
"target": "es5",
"watch": true,
"strictNullChecks": true // 严格的空值检查
},
"include": ["./src/**/*"]
}
复制代码
let a: number;
a = null;
a.toFixed(1); // error
复制代码
注意:TypeScript 鼓励尽可能地使用
--strictNullChecks
。
数组类型
TypeScript 中数组存储的类型必须一致,所以在标注数组类型的时候,同时要标注数组中存储的数据类型:
-
泛型标注:
Array<元素类型>
// <number> 表示数组中存储的数据类型 let arr1: Array<number> = []; arr1.push(100); // ok arr1.push('你好啊');// error 字符串不能赋给类型'number'的参数 复制代码
-
简单标注:在元素类型后面接上 [],表示由此类型元素组成的一个数组
let arr2: string[] = []; arr2.push('una'); // ok arr2.push(1); // error 复制代码
元组类型
- 元组表示一个
已知元素数量和类型的数组
,各元素的类型不必相同。 初始化
数据的个数 以及对应位置标注类型
必须一致- 当访问一个
越界
(超过之前定义的数量) 的元素,会使用联合类型
替代,但必须是元组标注中的类型之一。
// 1. 定义一对值分别为 string 和 number 类型的元组
let x: [string, number]; // 声明一个元组类型
x = ['hello', 10]; // OK
x = [10, 'hello']; // Error 初始化 数据的个数 以及 对应位置标注类型 必须一致
// 2. 当访问一个已知索引的元素,会得到正确的类型
console.log(x[0].substr(1)); // OK
console.log(x[1].substr(1)); // Error, 'number' does not have 'substr'
// 3. 当访问一个越界的元素,会使用联合类型替代:
x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型
x[4] = true; // Error, 布尔不是(string | number)类型
复制代码
枚举类型(enum)
枚举的作用是 组织收集一组关联数据的方式,通过枚举我们可以给一组有关联意义的数据赋予一些友好的名字:
enum HTTP_CODE {
ERROR,
OK = 200,
NOT_FOUND = 404,
METHOD_NOT_ALLOWED
};
HTTP_CODE.ERROR;// 0 第一个枚举值默认为:0
HTTP_CODE.OK;// 200
HTTP_CODE.METHOD_NOT_ALLOWED; // 405 非第一个枚举值为上一个数字枚举值 + 1
HTTP_CODE.OK = 1; // error 枚举值为只读(常量),初始化后不可修改
复制代码
注意事项:
-
key
不能是数字; -
value
可以是数字,称为 数字类型枚举,也可以是字符串,称为 字符串类型枚举,但不能是其它值,默认为数字:0; -
枚举值可以省略,如果省略,则:
- 第一个枚举值默认为:0;
- 非第一个枚举值为上一个数字枚举值 + 1;
-
枚举值为只读(常量),初始化后不可修改;
-
如果前一个枚举值类型为字符串,则后续枚举项必须手动赋值;
enum URLS {
USER_REGISETER = '/user/register',
USER_LOGIN = '/user/login',
// 如果前一个枚举值类型为字符串,则后续枚举项必须手动赋值
INDEX = 0
}
复制代码
枚举名称可以是大写,也可以是小写,推荐使用全大写(通常使用全大写的命名方式来标注值为常量)。
无值类型(void)
表示没有任何数据的类型,通常用于标注无返回值函数的返回值类型,函数默认标注类型为: void
function fn(): void {
// 没有 return 或者 return undefined
}
复制代码
声明一个 void
类型的变量没有什么大用,因为你只能为它赋予 undefined
和 null
:
let unusable: void = undefined;
复制代码
在
strictNullChecks
为 false 的情况下, undefined 和 null 都可以赋值给 void ,但是当strictNullChecks
为true
的情况下,只有undefined
才可以赋值给void
。
Never 类型
never 类型表示的是那些永不存在的值的类型:
- 抛出异常
- 无返回值的函数
- 箭头函数
- 永不为真的变量
never 类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是 never 的子类型,即使 any 也不可以赋值给 never。
下面是一些返回 never 类型的函数:
// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
throw new Error(message);
}
// 推断的返回值类型为 never
function fail() {
return error("Something failed");
}
// 返回 never 的函数必须存在无法达到的终点
function infiniteLoop(): never {
while (true) {
}
}
复制代码
任意类型(any)
有的时候,我们并不确定这个值到底是什么类型 或者 不需要对该值进行类型检测,就可以标注为 any
类型:
let a: any;
复制代码
- 一个变量 声明未赋值 且 未标注类型 的情况下,默认为
any
类型; - 任何类型值都可以赋值给
any
类型; any
类型也可以赋值给任意类型;any
类型有任意属性和方法;
标注为
any
类型,也意味着放弃对该值的类型检测,同时放弃 IDE 的智能提示。
let a; // 一个变量 声明未赋值 且 未标注类型,默认为 any 类型。当 noImplicitAny 为 true 时,报错。
let c: any = 'una'; // any 类型是任何类型的子类型
let d: number = 1;
d.toFixed(1)
d = c; // 把 c 赋值给 d,d 变成 any 类型
d.toFixed(1); // d 变成字符串,不具备 toFixed 方法,虽然 IDE 没报错,但是一运行就会报错
复制代码
当指定
noImplicitAny
配置为true
,当函数参数出现隐含的any
类型时报错。
未知类型(unknow)
unknow
,3.0 版本中新增,属于安全版的 any
,但是与 any
不同的是:
unknow
仅能赋值给unknow
、any
;unknow
没有任何属性和方法;
let c: unknow = 'una';
let d: number = 1;
d.toFixed(1)
d = c; // error unknow 仅能赋值给 unknow、any
d.toFixed(1); // error unknow 没有任何属性和方法
复制代码
对象类型
内置对象类型
在 JavaScript 中,有许多的内置对象,比如:Object
、Array
、Date
……,我们可以通过对象的 构造函数
或者 类
来进行标注。
object
表示非原始类型,也就是除number
,string
,boolean
,symbol
,null
或undefined
之外的类型。
let a: object = {};
let arr: Array<number> = [1,2,3];
let d1: Date = new Date();
复制代码
自定义对象类型
许多时候,我们可能需要自定义结构的对象。这个时候,我们可以通过以下方式来自定义对象:
- 字面量标注
- 接口
- 定义 类 或者 构造函数
字面量标注
let a: {username: string; age: number} = { username: 'una', age: 18 };
a.username; // ok
a.age; // ok
a.gender;// error
复制代码
- 优点: 方便、直接;
- 缺点: 不利于复用和维护;
接口
interface Person { username: string; age: number; };
let a: Person = { username: 'una', age: 18 };
a.username; // ok
a.age; // ok
a.gender;// error
复制代码
- 优点: 复用性高;
- 缺点: 接口只能作为类型标注使用,不能作为具体值,它只是一种抽象的结构定义,并不是实体,没有具体功能实现;
类 与 构造函数
class Person {
constructor(public username: string, public age: number) {
}
}
a.username; // ok
a.age; // ok
a.gender;// error
复制代码
- 优点: 功能相对强大,定义实体的同时也定义了对应的类型;
- 缺点: 复杂,比如只想约束某个函数接收的参数结构,没有必要去定一个类,使用接口会更加简单。
interface AjaxOptions {
url: string;
method: string;
}
function ajax(options: AjaxOptions) { }
ajax({url: '', method: 'get' });
复制代码
包装对象
这里说的包装对象其实就是 JavaScript 中的 String
、Number
、Boolean
,我们知道 string
类型 和 String
类型并不一样,在 TypeScript 中也是一样:
let a: string; // 简单类型字符串
a = '1';
a = new String('1'); // error String有的,string不一定有(对象有的,基础类型不一定有)
let b: String; // 字符串对象
b = new String('2');
b = '2'; // ok 和上面正好相反
复制代码
对象类型是可扩展的,但是简单类型不可扩展,
string
->String
会丢失数据。
函数类型
在 JavaScript 函数是非常重要的,在 TypeScript 也是如此。同样的,函数也有自己的类型标注格式:函数名称( 参数1: 类型, 参数2: 类型... ): 返回值类型;
function add(x: number, y: number): number {
return x + y;
}
复制代码
类型断言:不再检查
有时候你会遇到这样的情况,你会比 TypeScript 更了解某个值的详细信息。
通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。
类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。它没有运行时的影响,只是在编译阶段起作用。TypeScript会假设你已经进行了必须的检查。
类型断言有两种形式:
尖括号
语法:
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length; // 尖括号语法:<string>someValue
复制代码
as
语法:
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length; // as语法:someValue as string
复制代码
两种形式是等价的。 至于使用哪个大多数情况下是凭个人喜好;然而,当你在 TypeScript 里使用 JSX 时,只有 as 语法断言是被允许的。
三、接口详解
接口定义
接口:对复杂的对象类型进行标注的一种方式。
接口中多个属性之间可以使用 逗号 或者 分号 进行分隔。
接口的基础语法定义结构特别简单:
interface Point { x: number; y: number; }
复制代码
上面的代码定义了一个类型,该类型包含两个属性,一个 number
类型的 x
和一个 number
类型的
y
。我们可以通过这个接口来给一个数据进行类型标注:
let p1: Point = { x: 100, y: 100 };
p1.x = 200; // ok
p1.z = 100; // error 类型“Point”上不存在属性“z”
复制代码
注意:接口是一种 类型 ,不能作为 值 使用。
interface Point { x: number; y: number; }
let p1 = Point; // error 接口是一种类型,不能作为值使用
复制代码
接口属性
可选属性
接口也可以定义可选的属性,通过 ?
来进行标注
interface Point {
x: number;
y: number;
color?: string; // 可选属性,等同于 string | undefined
}
let p1: Point = { x: 100, y: 100 }; // color 属性是可选的,没有也不报错
复制代码
优点:
- 对可能存在的属性进行预定义
- 可以捕获引用了不存在的属性时的错误
let p2: Point = { x: 100, y: 100, color: 'red' };
let p3: Point = { x: 100, y: 100, col: 'red' }; // error 类型“Point”上不存在属性“col”
复制代码
只读属性
我们还可以通过 readonly
来标注属性为只读,只读属性除了初始化以外,是不能被再次赋值的。
interface Point {
readonly x: number; // 只读属性
readonly y: number;
}
let p1: Point = { x: 100, y: 100 };
p1.x = 200; // error 只读属性除了初始化以外,是不能被再次赋值的
复制代码
ReadonlyArray<T>
类型
TypeScript 还具有 ReadonlyArray<T>
类型,它与 Array<T>
相似,只是把所有可变方法去掉了,确保数组创建后再也不能被修改:
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error 类型“readonly number[]”中的索引签名仅允许读取
ro.push(5); // error 类型“readonly number[]”上不存在属性“push”
ro.length = 100; // error 无法分配到 "length" ,因为它是只读属性
a = ro; // error 类型 "readonly number[]" 为 "readonly",不能分配给可变类型 "number[]"
复制代码
上面代码的最后一行,可以看到就算把整个ReadonlyArray赋值到一个普通数组也不可以。但是你可以用类型断言重写:
a = ro as number[]; // ok
复制代码
readonly
vsconst
最简单判断该用
readonly
还是const
的方法是看要把它做为变量使用还是做为一个属性:变量const
,属性readonly
。
任意属性(额外属性)
有的时候,我们希望给一个接口添加任意属性,可以通过索引类型来实现。
TypeScript 支持两种索引签名:字符串索引和数字索引。
数字索引类型:
interface Point {
x: number;
y: number;
[prop: number]: number; // 任意属性,数字索引
}
复制代码
字符串索引类型:
interface Point {
x: number;
y: number;
[prop: string]: number; // 任意属性,字符串索引
}
复制代码
注意:索引签名参数类型必须为 string
或 number
之一,但两者可同时出现。
interface Point {
[prop1: string]: string;
[prop2: number]: string;
}
复制代码
数字索引是字符串索引的子类型。
注意:当同时存在数字类型索引和字符串类型索引时,数字索引的值类型 必须是 字符串索引的 值类型 或 子类型。
这是因为当使用
number
来索引时,JavaScript 会将它转换成string
然后再去索引对象。 也就是说用 100(number)去索引等同于使用”100″(string)去索引,因此两者需要保持一致。
interface Point1 {
[prop1: string]: string;
[prop2: number]: number; // error 当 同时存在数字类型索引和字符串类型索引 时,数字类型的值类型必须是字符串类型的值类型或子类型
}
interface Point2 {
[prop1: string]: string;
[prop2: number]: string; // ok
}
interface Point3 {
[prop1: string]: Object;
[prop2: number]: Date; // ok
}
复制代码
具体使用可看以下案例:
interface Point {
readonly x: number;
readonly y: number;
color?: string; // 可选属性 string | undefined
// [key: string]: number; // 这么设置的话 `color?: string` 会报错。
// color 属性明显也符合 [key: string] 的设置,但是两者类型不同,一个是 string, 一个是 number
[key: string]: number | string |undefined; // 兼容设置,扩展属性为3种类型即可
}
let p1: Point = {
x: 100,
y: 100
}
// p1.x = 200; // error 只读属性,仅能在初始化时赋值
p1.z = 100; // 扩展可选属性 [key: string]
p1[0] = 100; // 虽然0是number类型与可选属性设置不同,但是数字索引是字符串索引的子类型,所以不报错。但是反过来不行
// 等同于
// p1['0'] = 100;
复制代码
最后,你可以将索引签名设置为只读,这样就防止了给索引赋值:
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error 只读属性,仅能在初始化时赋值
复制代码
接口合并
多个同名的接口合并成一个接口:
interface Box {
height: number;
width: number;
}
interface Box {
scale: number;
}
let box: Box = {height: 5, width: 6, scale: 10}
复制代码
- 如果合并的接口存在同名的非函数成员,则必须保证他们类型一致,否则编译报错;
- 接口中的同名函数则是采用重载(后续会讲);
使用接口描述函数
我们还可以使用接口来描述一个函数
interface IFunc {
(a: string): string; // 单独描述函数,没有 key
}
let fn: IFunc = function(a) {}
复制代码
注意,如果使用接口来单独描述一个函数,是没 key 的。
interface IEventFunc {
(e: MouseEvent): void
}
function on(el: HTMLElement, evname: string, callback: IEventFunc) {
}
let div = document.querySelector('div');
if (div) {
on(div, 'click', function(e) {
e.clientX // HTMLElement 不存在 clientX 属性,MouseEvent 才存在 clientX 属性
});
}
复制代码
使用接口描述类
与 C# 或 Java 里接口的基本作用一样,TypeScript 也能够用它来明确的强制一个类去符合某种契约,使用 implements
:
interface ClockInterface {
currentTime: Date;
}
class Clock implements ClockInterface {
currentTime: Date;
constructor(h: number, m: number) { }
}
复制代码
接口描述了类的公共部分,而不是公共和私有两部分。 它不会帮你检查类是否具有某些私有成员。
具体讲解在 ts-笔记 面向对象编程 模块。
四、高级类型
联合类型
联合类型也可以称为多选类型,当我们希望标注一个变量为多个类型之一时可以选择联合类型标注(或)。
function css(ele: Element, attr: string, value: string | number) {
// ...
}
let box = document.querySelector('.box');
// document.querySelector 方法返回值就是一个联合类型
if (box) { // ts 会提示有 null 的可能性,加上判断更严谨
css(box, 'width', '100px');
css(box, 'opacity', 1);
css(box, 'opacity', [1,2]); // 错误
}
复制代码
交叉类型
交叉类型也可以称为合并类型,可以把多种类型合并到一起成为一种新的类型,对一个对象进行扩展(与):
interface o1 {
x: number,
y: string
};
interface o2 {
z: number
};
let o: o1 & o2 = Object.assign({}, {x: 1, y: '2'}, {z: 100});
console.log(o);
复制代码
小技巧:
TypeScript 在编译过程中只会转换语法(比如扩展运算符,箭头函数等语法进行转换,对于API 是不会进行转换的(也没必要转换,而是引入一些扩展库进行处理的),如果我们的代码中使用了 target 中没有的 API ,则需要手动进行引入,默认情况下 TypeScript 会根据target 载入核心的类型库
**target 为 es5 时: [“dom”, “es5”, “scripthost”] **
target 为 es6 时: [“dom”, “es6”, “dom.iterable”, “scripthost”]
如果代码中使用了这些默认载入库以外的代码,则可以通过 lib 选项来进行设置
字面量类型
有的时候,我们希望标注的不是某个类型,而是一个固定值,就可以使用字面量类型,配合联合类型会更有用
function setPosition(ele: Element, direction: 'left' | 'top' | 'right' | 'bottom') {
// ...
}
box && setDirection(box, 'bottom'); // ok
box && setDirection(box, 'hehe'); // error
复制代码
类型别名
有的时候类型标注比较复杂,这个时候我们可以类型标注起一个相对简单的名字,我们称之为 类型别名。
type dir = 'left' | 'top' | 'right' | 'bottom';
function setPosition(ele: Element, direction: dir) {
// ...
}
复制代码
使用类型别名定义函数类型
这里需要注意一下,如果使用 type 来定义函数类型,和接口有点不太相同
// 使用类型别名定义函数类型
type callback = (a: string) => string;
let fn: callback = function(a) {};
// 或者直接
let fn: (a: string) => string = function(a) {}
复制代码
interface 与 type 的区别
interface
- 只能描述
object
/class
/function
的类型 - 同名
interface
自动合并,利于扩展
- 只能描述
type
- 能描述所有数据
- 不能重名
类型推导
每次都显式标注类型会比较麻烦,TypeScript 提供了一种更加方便的特性:类型推导。TypeScript 编译器会根据当前上下文自动的推导出对应的类型标注,这个过程发生在:
- 初始化变量;
- 设置函数默认参数值;
- 返回函数值;
let x = 1; // 自动推断 x 为 number
x = 'a'; // 不能将类型“"a"”分配给类型“number”
// 函数参数类型、函数返回值会根据对应的默认值和返回值进行自动推断
// 相当于 function fn(a?: number): number
function fn(a = 1) { return a * a }
复制代码
类型断言
有的时候,我们可能标注一个更加精确的类型(缩小类型标注范围),比如:
let img = document.querySelector('#img');
复制代码
我们可以看到 img 的类型为 Element
,而 Element
类型其实只是元素类型的通用类型,如果我们去访问 src
这个属性是有问题的,我们需要把它的类型标注得更为精确:HTMLImageElement
类型,这个时候,我们就可以使用类型断言,它类似于一种 类型转换
:
let img = <HTMLImageElement>document.querySelector('#img');
// 或者
let img = document.querySelector('#img') as HTMLImageElement;
复制代码
注意:断言只是一种预判,并不会对数据本身产生实际的作用,即:类似转换,但并非真的转换了。
五、函数详解
一个函数的标注包含:
- 参数
- 返回值(如果函数没有返回任何值,你也必须指定返回值类型为
void
而不能留空)
对于返回值,我们在函数和返回值类型之前使用 =>
符号,使之清晰明了。
// 1. 直接标注
function fn1(a: string): string {
return '';
}
// 2. type 类型别名 标注函数
let fn2: (a: string) => string = function(a: string) {
return '';
}
// 在赋值语句的一边指定了类型但是另一边没有类型的话,TypeScript编译器会自动识别出类型
let fn3: (a: string) => string = function(a) {
return '';
}
// 3. type 类型别名 标注函数,拆分类型变量
type callback = (a: string) => string;
// 类型别名 使用的参数名为 a,函数使用的参数名为 b
// 只要参数类型是匹配的,那么就认为它是有效的函数类型,而不在乎参数名是否正确
let fn4: callback = function(b) {
return ''
}
// 4. interface 接口标注函数
interface ICallBack {
(a: string): string; // 使用接口单独描述函数,是没 key 的
}
let fn5: ICallBack = function(c) {
return '';
}
复制代码
函数参数
可选参数
通过参数名后面添加 ?
来标注该参数是可选的。可选参数必须跟在必须参数后面。
let div = document.querySelector('div');
function css(el: HTMLElement, attr: string, val?: any) {}
// 错误写法:
// function css(el?: HTMLElement, attr: string, val: any) {}
div && css( div, 'width', '100px' ); // 设置
div && css( div, 'width' ); // 获取
复制代码
默认参数
我们还可以给参数设置默认值:
- 有默认值的参数也是可选的
- 设置了默认值的参数可以根据值自动推导类型
带默认值的参数不需要放在必须参数的后面。 如果带默认值的参数出现在必须参数前面,用户必须明确的传入 undefined
值来获得默认值。
function sort1(items: Array<number>, order = 'desc') {}
sort1([1,2,3]);
// 也可以通过联合类型来限制取值,并设置默认值
// 'desc'|'asc' 联合类型,限制取值
// = 'desc' 设置默认值
function sort2(items: Array<number>, order: 'desc'|'asc' = 'desc') {}
sort2([1,2,3]); // ok
sort2([1,2,3], 'asc'); // ok
sort2([1,2,3], 'abc'); // error
function sort3(order: 'desc'|'asc' = 'desc', items: Array<number>) {}
sort3([1,2,3]); // error
sort3(undefined, [1,2,3]); // ok
sort3('asc', [1,2,3]); // ok
复制代码
剩余参数
剩余参数是一个数组,所以标注的时候一定要注意
interface IObj {
[key:string]: any;
}
function merge(target: IObj, ...others: Array<IObj>) {
return others.reduce( (prev, currnet) => {
prev = Object.assign(prev, currnet); // 合并参数
return prev;
}, target );
}
// target => {x: 1}, others => {y: 2}, {z: 3}
let newObj = merge({x: 1}, {y: 2}, {z: 3});
复制代码
函数中的this
普通函数
对于普通函数而言,this
是会随着调用环境的变化而变化的,所以默认情况下,普通函数中的 this
被标注为 any
,但我们可以在函数的第一个参数位(它不占据实际参数位置) 上显式的标注 this
的类型。
interface T {
a: number;
fn: (x: number) => void;
}
let obj1: T = {
a: 1,
fn(x: number) {
// 普调函数中的 this 为 any 类型
console.log(this);
// this.d // any 无检测,this.d 也不会报错
(<T>this).d // 类型断言
}
}
let obj2: T = {
a: 1,
fn(this: T, x: number) {
// 通过第一个参数位标注 this 的类型,它对实际参数不会有影响
console.log(this); // 有类型提示
}
}
obj2.fn(1);
复制代码
箭头函数
箭头函数的 this
不能像普通函数那样进行标注,它的 this
标注类型取决于它所在的作用域 this
的标注类型。
interface T {
a: number;
fn: (x: number) => void;
}
let obj2: T = {
a: 2,
fn(this: T) { // 箭头函数的this是固定的,取决于它所在作用域 this 的标注类型
return () => {
// T
console.log(this);
}
}
}
复制代码
函数重载
有的时候,同一个函数会接收不同类型的参数返回不同类型的返回值,我们可以使用函数重载来实现。
function showOrHide(ele: HTMLElement, attr: string, value: 'block' | 'none' | number) {}
let div = document.querySelector('div');
if (div) {
showOrHide( div, 'display', 'none' );
showOrHide( div, 'opacity', 1 );
// error,虽然通过了标注,但这里明显是有问题的
// 虽然通过联合类型能够处理同时接收不同类型的参数,但是多个参数之间是一种组合的模式,我们需要的应该是一种对应的关系
showOrHide( div, 'display', 1 );
}
复制代码
函数重载:
- 函数重载,是合并而不是覆盖
- 通过函数重载可以设置不同的参数的对应关系
- 重载函数类型只需要定义结构,不需要实体,类似接口
- 查找重载列表,尝试使用第一个重载定义,如果匹配的话就使用该定义。
// 通过函数重载可以设置不同的参数对应关系
// 重载函数类型只需要定义结构,不需要实体,类似接口
function showOrHide(ele: HTMLElement, attr: 'display', value: 'block' | 'none');
function showOrHide(ele: HTMLElement, attr: 'opacity', value: number);
// 注意,function showOrHide(ele: HTMLElement, attr: string, value: any) 并不是重载列表的一部分,而是具体的函数实现。
// 这里只有两个重载,以其它参数调用 showOrHide 会产生错误。
function showOrHide(ele: HTMLElement, attr: string, value: any) {
ele.style[attr] = value;
}
let div = document.querySelector('div');
if (div) {
showOrHide( div, 'display', 'none' );
showOrHide( div, 'opacity', 1 );
showOrHide( div, 'display', 1 ); // error
}
复制代码
interface PlainObject {
[key: string]: string | number;
}
function css(ele: HTMLElement, attr: PlainObject); // 对象
function css(ele: HTMLElement, attr: string, value: string | number); // 字符串
function css(ele: HTMLElement, attr: any, value?: any) {
if (typeof attr === 'string' && value) {
ele.style[attr] = value;
}
if (typeof attr === 'object') {
for (let key in attr) {
ele.style[attr] = attr[key];
}
}
}
let div = document.querySelector('div');
if (div) {
css(div, 'width', '100px');
css(div, { width: '100px'});
css(div, 'width'); // value? 如果不使用重载,这里就会有问题了
}
复制代码
为了让编译器能够选择正确的检查类型,它与JavaScript里的处理流程相似。 它查找重载列表,尝试使用第一个重载定义,如果匹配的话就使用这个。 因此,在定义重载的时候,一定要把最精确的定义放在最前面。