基础
# 基础
# 原始数据类型
let isEnabled:boolean = true;
let user:string = 'PPG007';
let age:number = 23;
function voidFunc(message:string):void {
console.log(message);
}
let u:undefined = undefined;
let n:null = null;
2
3
4
5
6
7
8
Tips
在非严格模式下,可以将 undefined 赋值给其他类型变量,需要在项目中有一个 tsconfig.json
配置文件,使用下面的配置关闭严格模式:
{
"compilerOptions": {
"strict": false
}
}
2
3
4
5
# 任意值
如果是一个普通类型,在赋值过程中改变类型是不被允许的,但是对于 any 类型是可以的。
let x:any = 123;
x = '123';
2
变量如果在声明的时候没有指定类型那么就会被识别为任意类型。
# 类型推论
如果没有明确指定类型,那么会依照类型推论的规则推断出是什么类型:
let x = 1;
x = "7"; // error
2
Tips
如果声明时不赋值那么将被推断为 any 类型。
let x;
x = 1;
x = "7";
2
3
# 联合类型
联合类型表示取值可以是多种类型中的一种。
let x:string|number;
x = "123";
x = 123;
2
3
当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型的共有属性或方法。
// error
function demo(x:string|number):void{
console.log(x.length);
}
// ok
function demo(x:string|number):void{
console.log(x.toString());
}
2
3
4
5
6
7
8
联合类型的变量在被赋值的时候,会根据类型推论的规则推断出一个类型。
let x:string|number;
x = "123";
console.log(x.length);
x = 123;
// error, 此时推断 x 为 number
console.log(x.length);
2
3
4
5
6
# 接口
TypeScript 使用接口来定义对象的类型。TypeScript 除了用于对类的一部分进行抽象,还可以对对象的形状进行描述。
interface Person{
name: string;
age: number;
}
// 定义的变量必须和接口里的一样
let me: Person = {
name: 'PPG007',
age: 23,
};
2
3
4
5
6
7
8
9
10
如果希望某些字段是可选的,那么可以使用可选属性:
interface Person{
name: string;
age: number;
city?: string;
}
let me: Person = {
name: 'PPG007',
age: 23,
};
2
3
4
5
6
7
8
9
10
如果希望某些字段只能在创建的时候赋值,那么可以使用只读属性:
interface Person{
readonly name: string;
age: number;
city?: string;
}
let me: Person = {
name: 'PPG007',
age: 23,
};
// error
me.name = "123";
2
3
4
5
6
7
8
9
10
11
12
如果希望一个接口允许任意的属性:
interface Person{
[propName: string]: string|number
}
let me: Person = {
name: 'PPG007',
age: 23,
};
2
3
4
5
6
7
8
一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型。
# 数组的类型
基础表示法:
let arr: number[];
arr = [1, 2, 3];
// error
arr.push('123');
2
3
4
数组泛型:
let arr: Array<string>;
arr.push('123');
// error
arr.push(123);
2
3
4
用接口表示数组:
interface StringArray{
[index: number]: string;
}
let arr: StringArray = ['1', '2'];
2
3
4
5
any 数组:
let list: any[] = [1, '2', false, {user: '123'}];
# 函数的类型
# 函数声明
function sum(a: number, b:number) :number {
return a+b;
}
sum(1, 2);
2
3
4
5
Note
多余的或者少于的参数输入是不被允许的。
# 函数表达式
let sum: (a: number, b: number) => number = function (a, b){
return a+b;
}
console.log(sum(2, 4));
2
3
4
Tips
在 TypeScript 的类型定义中,=>
表示函数的定义,左边是参数类型,右边是返回值类型。
# 用接口定义函数
interface Sum {
sumNumber: (a: number, b: number) => number;
sumString: (a: string, b: string) => string;
}
let sumImpl: Sum = {
sumNumber(a, b) {
return a+b;
},
sumString(a, b) {
return `${a}${b}`;
}
}
console.log(sumImpl.sumNumber(1, 2));
console.log(sumImpl.sumString('1', '2'));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 可选参数
function sub(a: number, b: number, abs?:boolean) :number {
if (!abs) {
return a - b;
}
return Math.abs(a-b);
}
console.log(sub(1, 2));
console.log(sub(1, 2, true));
2
3
4
5
6
7
8
Note
可选参数必须在必需参数后面,可选参数后不能再有必需参数。
# 参数默认值
function sub(a: number, b: number = 100, abs?:boolean) :number {
if (!abs) {
return a - b;
}
return Math.abs(a-b);
}
console.log(sub(1));
console.log(sub(1, 10, true));
2
3
4
5
6
7
8
# 剩余参数
function sum(...nums :number[]):number {
let total: number = 0;
nums.forEach(num => {
total += num;
});
return total;
}
console.log(sum(1, 2, 3, 4));
console.log(sum());
2
3
4
5
6
7
8
9
Note
剩余参数只能是最后一个参数。
# 重载
function sum(a: number, b: number): number;
function sum(a: string, b: string): string;
function sum(a: number|string, b: number|string) :number|string {
if (typeof a == 'number') {
return Number(a) + Number(b);
}
return a.concat(String(b));
}
console.log(sum(1, 2));
console.log(sum('1', '2'));
2
3
4
5
6
7
8
9
10
# 类型断言
类型断言可以用来手动指定一个值的类型。
语法:${value} as ${type}
或者 <${type}>${value}
,建议使用前一种。
将联合类型断言为其中一个类型:
function sum(a: number, b: number): number;
function sum(a: string, b: string): string;
function sum(a: number | string, b: number | string): number | string {
if (typeof a == 'number') {
return <number>a + <number>b;
}
return a.concat(String(b));
}
2
3
4
5
6
7
8
将一个父类断言为更加具体的子类:
class HttpError extends Error {
code: number = 200;
}
function isHttpError(err: Error): boolean {
if (typeof (err as HttpError).code == 'number') {
return true;
}
return false;
}
console.log(isHttpError(new(HttpError)));
2
3
4
5
6
7
8
9
10
11
将任何一个类型断言为 any:
当我们确定一个属性或者方法存在时,可以将对象断言为 any 类型,在 any 变量上,访问任何属性都是允许的。
(window as any).foo = 1;
将 any 断言为一个具体的类型:
let cache: any[] = ['123'];
console.log((cache[0] as string).length);
2
类型断言的限制:
- 联合类型可以被断言为其中一个类型。
- 父类可以被断言为子类。
- 任何类型都可以被断言为 any。
- 要使得
A
能够被断言为B
,只需要A
兼容B
或者B
兼容A
即可。
Tips
兼容可以理解为结构相同。
# 双重断言
any 可以断言为任意类型,任意类型可以断言为 any,所以任意类型可以断言为 any 后在断言为其他任意类型:
interface Cat {
run(): void;
}
interface Fish {
swim(): void;
}
function testCat(cat: Cat) {
return (cat as any as Fish).swim();
}
2
3
4
5
6
7
8
9
10
尽管两个类型并不兼容,但是还是能断言成功,不建议使用。
# 断言与类型转换
类型断言只会影响 TypeScript 编译时的类型,类型断言语句在变异结果中会被删除。
如果真的希望类型转换应该调用类型转换的方法。
# 断言与类型声明
类型声明比断言更加严格,如果向一个 A
类型的变量赋值一个 B
类型的变量,则 A
必须兼容 B
。
interface Animal {
name: string;
}
interface Cat {
name: string;
run(): void;
}
const animal: Animal = {
name: 'tom'
};
// ok
let tom1: Cat = animal as Cat;
// error
let tom2: Cat = animal;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 声明文件
当使用第三方库时,需要引用它的声明文件以获得对应的代码补全、接口提示等功能。
# 示例
如果希望使用 jQuery,一般通过 script 引入后就可以使用全局变量 $
或者是 jQuery
了,但是在 TypeScript 中,编译器不知道 $
和 jQuery
是什么,这时我们需要 declare var
来定义它的类型。
declare var $: (selector: string) => any;
declare var jQuery: (selector: string) => any;
$('#foo');
jQuery('body');
2
3
4
declare var
并不会真的定义一个变量,只是定义了全局变量 $
和 jQuery
的类型,仅仅用作编译时的检查。
# 声明文件
通常把声明语句放到一个单独的文件中,这个文件就是声明文件,声明文件必须以 .d.ts
做后缀。
使用 @types
统一管理第三方库的声明文件,以 jQuery 为例:
# npm
npm install @types/jquery --save-dev
# yarn
yarn add @types/jquery --dev
2
3
4
# 书写声明文件
当一个第三方库没有提供声明文件时,就需要自行编写声明文件。
# 全局变量
通过 <script>
标签引入第三方库,注入全局变量。
全局变量主要包含以下语法:
declare var
声明全局变量。declare function
声明全局方法。declare class
声明全局类。declare enum
声明全局枚举类型。declare namespce
声明(含有子属性的)全局对象。interface
和type
声明全局类型。
declare var
:
declare var accountId: string;
declare const println: (selector: string) => void;
2
当使用 const
定义时,表示此全局变量是一个常量,不允许再去修改它的值了。
Note
声明语句里只能定义类型,不能在声明语句中定义具体的实现。
declare function
:
declare function
用来定义全局函数的类型。jQuery 其实就是一个函数,所以也可以用 function 来定义:
declare function jQuery(selector: string): any;
// 支持函数重载
declare function jQuery(domReadyCallback: () => any): any;
2
3
declare class
:
当全局变量是一个类的时候,可以用 declare class
来定义它的类型。
// user.d.ts
declare class User {
constructor(name: string);
name: string;
getName(): string;
}
// user.ts
class User {
constructor(name: string){
this.name = name;
}
name: string;
getName(): string {
return this.name;
}
}
// main.ts
const user: User = new User('PPG007');
console.log(user.getName());
// tsc user.ts main.ts --outFile=main.js && node main.js
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
declare enum
:
// httpCode.d.ts
declare enum HttpCode {
OK,
BadRequest,
InternalServerError
}
// main.ts
const code: HttpCode = HttpCode.BadRequest;
2
3
4
5
6
7
8
declare namespace
:
namespace 是早期为了解决模块化而创造的关键字,在早期没有 ES6 的时候,ts 使用 module
关键字表示内部模块,后来由于 ES6 也使用 module
关键字,ts 将 module 改为 namespace。
namespace 被淘汰了,但是在声明文件中,declare namespace 还是比较常用的,它用来表示全局变量是一个对象,包含很多子属性。
比如 jQuery 是一个全局变量,它是一个对象,提供了一个 jQuery.ajax 方法可以调用,那么我们就应该使用 declare namespace jQuery 来声明这个拥有多个子属性的全局变量。
// axios.d.ts
declare namespace Axios {
const version: number;
enum HttpCode {
OK,
}
class Response {
code: HttpCode;
Data: any;
}
function get(url: string): Response;
}
// main.ts
const resp = Axios.get('url');
resp.code;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Tips
namespace 中可以直接使用 function 声明函数而不是 declare function
,类似的也可以使用 enum、class、const 等。
如果对象拥有深层的层级,则需要用嵌套的 namespace 来声明深层的属性的类型。
declare namespace Axios {
const version: number;
enum HttpCode {
OK,
}
class Response {
code: HttpCode;
Data: any;
}
function get(url: string): Response;
namespace interceptor {
function run():void;
}
}
// main.ts
Axios.interceptor.run();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
如果只有内层 namespace,可以使用如下声明:
declare namespace Axios.interceptor {
function run():void;
}
2
3
在声明文件中可以直接使用 interface
和 type
来声明一个全局接口或类型:
declare namespace Axios {
const version: number;
enum HttpCode {
OK,
}
class Response {
code: HttpCode;
Data: any;
}
function get(url: string, setting: AxiosSetting): Response;
}
interface AxiosSetting {
headers?: Object,
}
type AxiosSettingType = {
headers?: Object,
}
// main.ts
let setting: AxiosSetting = {
headers: {
'x-user-name': 'PPG007',
},
};
Axios.get('url', setting);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
为了防止命名重复,可以将 interface
或 type
放到 namespace
中:
declare namespace Axios {
const version: number;
enum HttpCode {
OK,
}
class Response {
code: HttpCode;
Data: any;
}
interface AxiosSetting {
headers?: Object,
}
function get(url: string, setting: AxiosSetting): Response;
}
// main.ts
let setting: Axios.AxiosSetting = {
headers: {
'x-user-name': 'PPG007',
},
};
Axios.get('url', setting);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
对于 jQuery 这种既是一个函数可以被直接调用,又是一个对象,拥有子属性,那么可以组合多个声明语句,它们会不冲突地合并起来:
declare function jQuery(selector: string): any;
declare namespace jQuery {
function ajax(url: string, settings?: any): void;
}
// main.ts
jQuery('#id');
jQuery.ajax('url');
2
3
4
5
6
7
# npm 包
通过 import foo from 'foo'
导入,符合 ES6 模块规范。
一般来说,npm 包的声明文件可能存在于两个地方:
- 与该 npm 包绑定在一起,
package.json
中有 types 字段或者有一个index.d.ts
声明文件。 - 发布到
@types
里,尝试安装即可:npm install @types/XXX --save-dev
。
如果上述方式都无法找到声明文件,那么需要自行编写。
首先新建一个本地 npm 包,使用 npm init
或 yarn init
初始化,设置 package.json 中 private 为 true,编写 src/index.js(因为声明文件只有在引用 JavaScript 库时才有意义):
export class StrUtil {
static lower = (source) => {
return source.toLowerCase();
}
static upper = (source) => {
return source.toUpperCase();
}
static reverse = (source) => {
return source.split('').reverse().join('');
}
static concat = (...source) => {
return ''.concat(...source);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
然后执行:npm install -D @babel/core @babel/preset-env @babel/cli
安装 babel,因为我们需要将 ES6 代码编译成 CommonJS,然后编写 .babelrc
:
{
"presets": ["@babel/preset-env"]
}
2
3
修改 package.json 中 main 字段,改为 dist/index.js(即编译后输出到来的地方),同时添加 scripts:
"scripts": {
"build": "babel src -d dist"
}
2
3
最后执行编译:npm run build
。
然后我们在 TypeScript 项目中执行 yarn add ../strutil
引入本地的 JavaScript 包,新建 types/strutil
目录,在其中编写声明文件 index.d.ts,npm 包的声明文件有多种写法:
在此之前,需要先配置一下 TypeScript 编译选项:
{
"compilerOptions": {
"strict": false,
"module": "CommonJS",
"baseUrl": "./",
"paths": {
"*": ["types/*"]
}
}
}
2
3
4
5
6
7
8
9
10
export 导出:
export class StrUtil {
static lower(source: string): string;
static upper(source: string): string;
static reverse(source: string): string;
static concat(...source: string[]): string;
}
2
3
4
5
6
7
8
9
Tips
npm 包的声明文件与全局变量的声明文件有很大区别。在 npm 包的声明文件中,使用 declare 不再会声明一个全局变量,而只会在当前文件中声明一个局部变量。只有在声明文件中使用 export 导出,然后在使用方 import 导入后,才会应用到这些类型声明。
在 main.ts 中引用:
import { StrUtil } from 'strutil';
console.log(StrUtil.reverse('PPG007'));
2
3
declare
与 export
混用:
declare class StrUtil {
static lower(source: string): string;
static upper(source: string): string;
static reverse(source: string): string;
static concat(...source: string[]): string;
}
export { StrUtil };
2
3
4
5
6
7
8
9
10
11
export namespace
:
首先修改一下 StrUtil 包中的 index.js,增加一个内部对象:
static inner = {
author: 'PPG007'
}
2
3
然后重新编译、引入后,编写声明文件:
export namespace StrUtil {
function lower(source: string): string;
function upper(source: string): string;
function reverse(source: string): string;
function concat(...source: string[]): string;
namespace inner {
const author: string;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
在 main.ts 引入:
import { StrUtil } from 'strutil';
console.log(StrUtil.inner.author);
2
3
export default
:
// 在 StrUtil index.js 里增加一个枚举:
export default Object.freeze({
UPPER: 'UPPER',
LOWER: 'LOWER',
});
2
3
4
5
然后修改声明文件:
declare enum Case {
UPPER,
LOWER,
}
export default Case;
2
3
4
5
6
在 main.ts 中引用:
import Case from 'strutil';
console.log(Case.LOWER);
2
3
Note
只有 function
、class
、interface
可以直接默认导出,其他变量需要先定义出来再默认导出。
export =
:
修改一下 StrUtil,改为 CommonJS:
class StrUtil {
static lower = (source) => {
return source.toLowerCase();
}
static upper = (source) => {
return source.toUpperCase();
}
static reverse = (source) => {
return source.split('').reverse().join('');
}
static concat = (...source) => {
return ''.concat(...source);
}
}
module.exports = StrUtil;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
声明文件:
declare class StrUtil {
static lower(source: string): string;
static upper(source: string): string;
static reverse(source: string): string;
static concat(...source: string[]): string;
}
export = StrUtil;
2
3
4
5
6
7
8
9
10
main.ts 引用:
import StrUtil = require("strutil");
console.log(StrUtil.lower('QWE'));
2
# UMD 库
即可以通过 <script>
标签引入又可以通过 import
导入。相比于 npm 包的声明文件,我们需要额外声明一个全局变量,export as namespace
。
UMD 是一种模块规范,使库可以同时支持多种模块加载方案,比较流行的有
- CommonJS:用于 Node.js,通过 require() 导入。
- AMD:用于浏览器,通过 define() 定义和 require() 导入。
- 全局变量:将库暴露为一个全局变量,用于直接在浏览器中使用。
首先修改之前的 strutil:
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory);
} else if (typeof exports === 'object') {
module.exports = factory();
} else {
root.StrUtil = factory();
}
}(this, function () {
function StrUtil() { }
StrUtil.lower = function (source) {
return source.toLowerCase();
}
StrUtil.upper = function (source) {
return source.toUpperCase();
}
// 其他方法...
return StrUtil;
}));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
然后书写声明文件:
export as namespace su;
export = su;
declare function su(): string;
declare namespace su {
function lower(source: string): string;
function upper(source: string): string;
}
2
3
4
5
6
7
8
9
然后就可以在 main.ts 中引用了:
import * as su from 'strutil';
console.log(su.upper('ppg007'))
2
3
# 直接扩展全局变量
通过 <script>
引入后,改变一个全局变量的结构。
修改 strutil:
String.prototype.reverse = function() {
return this.split('').reverse().join('');
}
2
3
编写声明文件:
export as namespace su;
export class StrUtil {
static lower(source: string): string;
static upper(source: string): string;
static reverse(source: string): string;
static concat(...source: string[]): string;
}
declare global {
interface String {
reverse(): string;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
在 main.ts 中引用:
import * as su from 'strutil';
console.log(su.StrUtil.upper('ppg').reverse());
2
3
# 在 npm 包或 UMD 库中扩展全局变量
引用 npm 包或者 UMD 库后,改变一个全局变量的结构。
对于一个 npm 包或者 UMD 库的声明文件,只有 export 导出的类型声明才能被导入。所以对于 npm 包或 UMD 库,如果导入此库之后会扩展全局变量,则需要使用另一种语法在声明文件中扩展全局变量的类型,那就是 declare global。
修改 strutil:
export class StrUtil {
static lower = (source) => {
return source.toLowerCase();
}
static upper = (source) => {
return source.toUpperCase();
}
static reverse = (source) => {
return source.split('').reverse().join('');
}
static concat = (...source) => {
return ''.concat(...source);
}
}
globalThis.author = 'PPG007';
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
编写声明文件:
export as namespace su;
export class StrUtil {
static lower(source: string): string;
static upper(source: string): string;
static reverse(source: string): string;
static concat(...source: string[]): string;
}
declare global {
const author: string
}
2
3
4
5
6
7
8
9
10
11
12
13
14
在 main.ts 中引用:
import * as su from 'strutil';
console.log(su.StrUtil.upper('ppg'));
console.log(author);
2
3
4
# 模块插件
通过 <script>
或 import
导入后,改变另一个模块的结构。
修改 strutil:
import moment from 'moment';
moment.yesterday = function() {
return moment().subtract(1, 'day');
}
export default moment;
2
3
4
5
6
编写声明文件:
import moment = require("moment");
declare module 'moment'{
function yesterday(): moment.Moment;
}
export default moment;
2
3
4
5
6
7
main.ts:
import moment from 'strutil';
console.log(moment.yesterday().toISOString())
2
3
# 声明文件中的依赖
除了使用 import 导入另一个声明文件中的类型之外,还可以使用三斜线指令。
与 import 的区别是,当且仅当在以下几个场景下,我们才需要使用三斜线指令替代 import:
- 当我们在书写一个全局变量的声明文件时。
- 当我们需要依赖一个全局变量的声明文件时。
书写一个全局变量的声明文件:
在全局变量的声明文件中,是不允许出现 import, export 关键字的。一旦出现了,那么他就会被视为一个 npm 包或 UMD 库,就不再是全局变量的声明文件了。故当我们在书写一个全局变量的声明文件时,如果需要引用另一个库的类型,那么就必须用三斜线指令了。
修改 strutil:
export function foo(arg) {
console.log(arg);
}
2
3
声明文件:
/// <reference types="moment"/>
export function foo(p: moment.Moment): void;
2
3
main.ts:
import moment = require("moment");
import { foo } from "strutil";
foo(moment());
2
3
Note
注意,三斜线指令必须放在文件的最顶端,三斜线指令的前面只允许出现单行或多行注释。
依赖一个全局变量的声明文件:
在另一个场景下,当我们需要依赖一个全局变量的声明文件时,由于全局变量不支持通过 import 导入,当然也就必须使用三斜线指令来引入了。
首先安装 nodejs 类型声明:
yarn add @types/node --dev
// types/node-plugin/index.d.ts
/// <reference types="node" />
export function foo(p: NodeJS.Process): string;
2
3
4
5
main.ts:
import { foo } from "strutil";
foo(process);
2
# 自动生成声明文件
如果库本身就就是 TypeScript 变下的,那么可以自动生成声明文件。
修改 strutil 为一个纯 TypeScript 项目:
src/bar/index.ts:
export function bar(): string{
return 'bar';
}
2
3
src/index.ts:
export * from './bar';
export function foo(): string{
return 'foo';
}
2
3
4
5
tsconfig.json:
{
"compilerOptions": {
"strict": false,
"outDir": "lib",
"declaration": true,
"module": "CommonJS"
}
}
2
3
4
5
6
7
8
修改 package.json main 字段,指向 lib/index.js。
默认情况下,对于一个库会去先找最外层的 index.d.ts,没找到的话再去找 main 文件的目录中的 index.d.ts,如果再没有找到就会视为一个没有声明文件的库。
如果声明文件名字不是 index.d.ts,需要修改 package.json,例如修改 lib/index.d.ts 为 lib/foo.d.ts:
{
"name": "strutil",
"version": "1.0.0",
"main": "lib/index.js",
"license": "MIT",
"private": true,
"types": "lib/foo.d.ts",
}
2
3
4
5
6
7
8
# 内置对象
TypeScript 核心库的定义文件中定义了所有浏览器环境需要用到的类型,并且是预置在 TypeScript 中的。在使用一些常用的方法的时候,TypeScript 实际上已经帮你做了很多类型判断的工作。
Node.js 不是内置对象的一部分,如果想用 TypeScript 写 Node.js,则需要引入第三方声明文件。
npm install @types/node --save-dev