Fork me on GitHub

TypeScript基础

TypeScript 基础
参考:https://ts.xcatliu.com/

原始数据类型

布尔值、数值、字符串、null、undefined、以及 ES6 中的新类型 Symbol

布尔值

1
let isDone: boolean = false;

注意,使用构造函数Boolean创造的对象不是布尔值

1
let createdByNewBoolean: boolean = new Boolean(1);

编译报错
boolean
new Boolean()返回的是一个Boolean对象
直接调用Boolean()也可以返回一个boolean类型,将参数转化为 Boolean 值。

数值

number

1
2
3
4
5
6
7
8
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;

编译结果:
二进制和八进制会编译成十进制

1
2
3
4
5
6
7
8
var decLiteral = 6;
var hexLiteral = 0xf00d;
//ES6中的二进制表示法
var binaryLiteral = 10;
//ES6中的八进制表示法
var octalLiteral = 484;
var notANumber = NaN;
var infinityNumber = Infinity;

字符串

string

1
2
3
4
5
let myName: string = "Tom";
let myAge: number = 25;
//模板字符串
let sentence: string = `Hello, my name is ${myName}.I'll be ${myAge +
1} years old next month.`;

编译结果:

1
2
3
4
5
var myName = "Tom";
var myAge = 25;
//模板字符串
var sentence = "Hello, my name is " + myName + ".I'll be " + (myAge +
1) + " years old next month.";

元组 Tuple

元组类型是用固定数量的元素组成的数组,元素的类型是已知的,但不一定是相同的

1
2
3
let x: [string, number];
x = ["hello", 5]; //ok
x = [5, "hello"]; //error

基础1
通过下标可以访问元素,访问不存在的下标会报错。

1
2
console.log(x[0]);
console.log(x[2]);

枚举 Enum

枚举成员会被赋值为从 0 开始递增的数字,同时也会对枚举值到枚举名进行反向映射。
值是只读的,无法修改

1
2
3
4
5
6
7
8
9
10
11
enum Color {
Red,
Green,
Blue
}
console.log(Color["Red"] === 0); //true
console.log(Color["Green"] === 1); //true
console.log(Color["Blue"] === 2); //true
console.log(Color[0] === "Red"); //true
console.log(Color[1] === "Green"); //true
console.log(Color[2] === "Blue"); //true

可以设置其中一个成员的值,后面的值随之递增

1
2
3
4
5
6
7
8
9
10
//枚举
enum Color {
Red,
Green = 3,
Blue
}
console.log(Color["Red"]); //0
console.log(Color["Green"]); //3
console.log(Color["Blue"]); //4
console.log(Color['4'] === "Blue") //true

也可以同时设置所有的值

1
2
3
4
5
6
7
8
9
enum Color {
Red = 1,
Green = 3,
Blue = 5
}

console.log(Color["Red"]); //1
console.log(Color["Green"]); //3
console.log(Color["Blue"]);//5

如果一个枚举的值为 string,后面的值都要设置

1
2


空值

JavaScript 没有空值(Void)的概念,在 TypeScript 中,可以使用 void 表示没有任何返回值的函数;

1
2
3
function alterName(): void {
alert("My name is Tom");
}

void 类 U 型的变量只能赋值 undefined 和 null

1
let unusable:void = undefined;

Null 和 Undefined

使用nullundefined来定义原始数据类型:

1
2
let u: undefined = undefined;
let n: null = null

void的区别,undefinednull是所有类型的子类型。也就是说undefined类型的变量,可以赋值给number类型的变量.

当指定了–strictNullChecks 标记,null 和 undefined 只能赋值给 void 和他们各自。

1
2
3
let num: number = undefined;
let u:undefined;
let num:number = u;

任意值

what

Any 用来表示允许赋值为任意类型。

1
2
let myFavoriteNumber: string = "seven";
myFavoriteNumber = 7;

报错:hello.ts:36:1 - error TS2322: Type ‘7’ is not assignable to type ‘string’.

如果类型为 any,就被允许赋值为任意值

1
2
let myFavoriteNumber1: any = "seven";
myFavoriteNumber1 = 7;

任意值的属性和方法

在任意值上访问任何属性都是允许的

1
2
3
let anyThing: any = "hello";
console.log(anyThing.myName);
//undefined

也允许调用任何方法

1
2
3
4
5
let anyThing: any = "hello";
anyThing.toFixed();
let prettySure: Object = 4;
prettySure.toFixed();
//prettySure.toFixed()编译会报错anyThing.toFixed()不会

声明一个变量为任意值后,对它的任何操作,返回的内容的类型都是任意值。

未声明类型的变量

如果在声明的时候未指定其类型,就会被识别为任意值类型。

1
2
3
4
let something;
something = "seven";
something = 7;
something.setName("Tom");

Nerver

what

表示那些用不存在的值的类型

  1. 总是会抛出异常或根本不会有返回值的函数表达式或箭头函数表达式的返回值类型。
  2. 变量被永不为真的类型所保护时,也可以是 never 类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 有无法到达的终点
function error(message: string): never {
throw new Error(message);
}

function infiniteLoop(): never {
while(true){

}
}
//推断返回值是never
function fail() {
error("fail");
}
  • never 是任何类型的子类型,并且可以赋值给任何类型
  • 除了 never 类型本身以外没有类型可以赋值给 never 类型
  • 在一个没有返回值标注的函数表达式或箭头函数中, 如果函数没有 return 语句, 或者仅有表达式类型为 never 的 return 语句, 并且函数的终止点无法被执行到 (按照控制流分析), 则推导出的函数返回值类型是 never.
  • 在一个明确指定了 never 返回值类型的函数中, 所有 return 语句 (如果有) 表达式的值必须为 never 类型, 且函数不应能执行到终止点.

Object

非原始类型,除了number,string,boolean,symbol,null,undefined之外的类型。
使用Object类型就可以更好的表示像Object.create这样的 API。

1
2
3
4
5
6
7
8
declare function create(o: object | null): void;
create({ prop: 0 });
create(null);
create(undefined);

create(42)//error
create("string"); //error
create(false); //error

类型检查机制

类型推论

如果没有明确的指定类型,TypeScript 会依照类型推论(Type Inference)的规则推断出一个类型。
推论发生在初始化变量和成员

1
2
3
4
let myFavoriteNumber = "seven";
myFavoriteNumber = 7;
let h=[1]
let foo=(x=1)=>x+1

推论
等价于

1
2
3
4
let myFavoriteNumber:string = "seven";
myFavoriteNumber:number = 7;
let h:number[]=[1]
let foo=(x:number=1):number=>x+1

如果定义的时候没有赋值,不管之后有没有赋值,都会被推断为any类型,而类型完全不被类型检查

1
2
3
let myFavoriteNumber1;
myFavoriteNumber1 = "seven";
myFavoriteNumber1 = 7;//不报错

最佳通用类型

计算通用类型算法会考虑所有的候选类型,并给出一个兼容所有候选类型的类型。

1
let x =[0,1,null]

考虑所有元素的类型,推论 x 的类型为(number|null)[]
如果没有找到最佳通用类型,类型判断结果为联合数组类型
注:需要在 tsconfig.js 配置strictNullChecks": false,将null,undefined视为其他类型的子类型,x 被推断为number[]

1
let zoo =[new Rhino(), new Elephant(), new Snake()]

没法直接推断 zoo 的类型为 Animal[]类型,只能联合数组类型[Rhino|Elephant|Snake]

上下文归类

1
2
3
4
window.onmousedown = function(mouseEvent) {
console.log(mouseEvent.button); //ok
console.log(mouseEvent.kangaroo); //error
};

TypeScript 类型检查器会使用 Window.onmousedown 函数的类型来推断右边函数表达式的类型。 所以它能够推断出 mouseEvent 参数的类型中包含了 button 属性而不包含 kangaroo 属性。
可能是我编写测试代码环境有问题,编译的时候没报错
上下文类型也会作为最佳通用类型的候选类型

1
2
3
function createZoo(): Animal[]{
return [new Rhino(), new Elephant(), new Snake()]
}

这个例子里,最佳通用类型有 4 个候选者:Animal,Rhino,Elephant 和 Snake。

类型断言

Type Assertion
用来手动指定一个值的类型

语法

1
2
3
<类型>值
//或
值 as 类型

在 tsx 语法(React 的 jsx 语法的 ts 版中必须用后一种

例子:将一个联合类型的变量指定为一个更加具体的类型

1
2
3
4
5
6
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'.

有时候我们确实需要在还不确定类型的时候访问其中一个类型的属性或方法。

1
2
3
4
5
6
7
8
9
10
11
function getLength(something: string | number): number {
if (something.length) {
return something.length;
} else {
return something.toString().length;
}
}
//error TS2339: Property 'length' does not exist on type 'string | number'.
//Property 'length' does not exist on type 'number'.
//Property 'length' does not exist on type 'string | number'.
//Property 'length' does not exist on type 'number'

此时可以使用类型断言,将 something 断言成 string:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function getLength(something: string | number): number {
if ((something as string).length) {
return (something as string).length;
} else {
return something.toString().length;
}
}
//或
function getLength(something: string | number): number {
if ((<string>something).length) {
return (<string>something).length;
} else {
return something.toString().length;
}
}

类型断言不是类型转换,断言成一个联合类型中不存在的类型是不允许的

1
2
3
4
5
function toBoolean(something: string | number): boolean {
return <boolean>something;
}
//error TS2352: Conversion of type 'string | number' to type 'boolean' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
//Type 'number' is not comparable to type 'boolean'.

类型兼容性

定义

类型 Y 可以被赋值给类型 X,则称类型 X 兼容类型 Y
结构之间兼容:成员少的兼容成员多的
函数之间兼容:参数多的兼容参数少的

1
2
3
let s: string = 'a'
s=null
//strictFunctionTypes=false,string可以兼容null

接口兼容

1
2
3
4
5
6
7
8
9
10
11
12
13
interface X{
a: number;
b :number
}
interface Y{
a: number;
b: number;
c: number;
}
let x: X = { a: 1, b: 2 }
let y: Y = { a: 1, b: 2, c: 3 }
x = y
y = x //Type 'X' is not assignable to type 'Y'.Property 'c' is missing in type 'X'.

所以 X 兼容 Y,Y 不兼容 X

函数兼容性

1)参数个数

1
2
3
4
5
6
7
8
type Handler = (a: number, b: number) => void
function hof(handler: Handler) {
return handler
}
let handler1 = (a: number) => { }
hof(handler1)
let handler2 = (a: number, b: number, c: number) => { }
//hof(handler2) //handler2有三个参数hof无法处理

可选参数

1
2
3
4
5
6
7
8
9
10

let a = (p1: number, p2: number) => { }
let b = (p1?: number, p2?: number) => { }
let c = (...args: number[]) => { }
a = b
a = c
//b = c//strictFunctionTypes=false时兼容
//b = a//strictFunctionTypes=false时兼容
c = a
c = b

2)参数类型

1
2
let handler3 = (a: string) => { }
//hof(handler3)//无法传入

对象参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Point3D{
x: number;
y: number;
z: number;
}
interface Point2D{
x: number;
y: number;
}

let p3d = (point: Point3D) => { }
let p2d = (point: Point2D) => { }
p3d = p2d
//p2d = p3d 不兼容,参数多的兼容参数少的,strictFunctionTypes=false,p2兼容p3d

3)返回值类型

1
2
3
let f = () => ({ name: 'Alice' } );
let g = () => ({ name: 'Alice', location: 'Beijing' });
f = g //返回值少的兼容返回值多的

重载

1
2
3
4
5
6
function overload(a: number, b: number): number;
function overload(a: string, b: string): string;
function overload(a: any, b: any): any { }
//function overload(a: any, b: any, c:any): any { }
//function overload(a: any, b: any, c:any) { }
//都不兼容

枚举类型兼容性

1
2
3
4
5
6
7
8
enum Fruit{ Apple, Banana }
enum Color{Red,Yellow}
//枚举类型与数字类型兼容
let fruit: Fruit.Apple = 3;
let no: number = Fruit.Apple

//枚举类型之间不兼容
//let color:Color.Red=Fruit.Apple

类兼容性

构造函数和静态属性不参与兼容比较

1
2
3
4
5
6
7
8
9
10
11
12
13
class A{
constructor(p: number, q: number) { }
id:number=1
}
class B{
static s = 1;
constructor(p: number) { }
id:number=2
}
let aa = new A(1, 2);
let bb = new B(1)
aa = bb
bb = aa

如果含有私有属性,不兼容其他类。兼容其子类

1
2
3
4
5
6
7
8
9
class C extends A{
constructor(p: number, q: number, name: string) {
super(1, 2),
name = name
}
}
let cc = new C(1, 2,'333')
aa = cc
cc = aa

泛型兼容性

1
2
3
4
5
6
7
interface Empty<T>{
value:T
}
//let obj1: Empty<number> = {}
//let obj2: Empty<string> = {}
// obj1=obj2
//不兼容

泛型函数,参数多的兼容参数少的

1
2
3
4
5
6
7
8
9
10
11
12
13
let log1 = <T>(x: T): T => {
console.log('x')
return x
}
let log2 = <T>(y: T): T => {
console.log('y')
let a: T;

return y
}

log1 = log2
log2 = log1

类型保护

TS 能够在特定的区块中保证变量属于某种确定的类型
可以在此区块中放心的引用此类型的属性或者调用此类型的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
enum Type{ Strong, Week }
class Java{
helloJava() {
console.log('hello Java')
}
}

class JavaScript{
helloJavaScript() {
console.log('Hello JavaScript')
}
}

function getLanguage(type: Type) {
let lang = type === Type.Strong ? new Java() : new JavaScript()
if ((lang as Java).helloJava) {
(lang as Java).helloJava()
} else {
(lang as JavaScript).helloJavaScript()
}
}

instanceof

1
2
3
4
5
6
7
8
9
10
function getLanguage(type: Type) {
let lang = type === Type.Strong ? new Java() : new JavaScript()

if (lang instanceof Java) {
lang.helloJava()
} else {
lang.helloJavaScript()
}
return lang
}

in

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
enum Type{ Strong, Week }
class Java{
helloJava() {
console.log('hello Java')
}
java:any
}

class JavaScript{
helloJavaScript() {
console.log('Hello JavaScript')
}
javaScript:any
}

function getLanguage(type: Type) {
let lang = type === Type.Strong ? new Java() : new JavaScript()
if ('java' in lang) {
lang.helloJava()
} else {
lang.helloJavaScript()
}
return lang

typeof

1
2
3
4
5
6
7
function fun(x:string|number){
if (typeof x === 'string') {
x.length
} else {
x.toFixed(2)
}
}

类型保护函数

1
2
3
function isJava(lang:Java|JavaScript):lang is Java{
return (lang as Java).helloJava!==undefined
}

联合类型

Union Types 表示取值可以为多种类型中的一种

1
2
3
4
let a: string | number;
a = "seven";//true
a = 7;//true
a = true; //error

分隔符:|

访问联合类型的属性或方法

TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,只能访问此联合类型的所有类型里的共有属性或方法

1
2
3
4
5
function getLength(st: string | number): number {
return st.length;
}
//error TS2339: Property 'length' does not exist on type 'string | number'.
//Property 'length' does not exist on type 'number'.

length 不是 string 和 number 的共有属性所以会报错。

1
2
3
function getLength(st: string | number): string {
return st.toString();
}

toString 是 String 和 Number 的共有属性。

联合类型的变量在被赋值的时候,会根据类型推论的规则推出一个类型:

1
2
3
4
5
6
let b: string | number;
b = "seven";
console.log(b.length); //5
b = 7;
console.log(b.length);//error
// error TS2339: Property 'length' does not exist on type 'number'.

第二行b被推断成 string,访问它的length属性不会报错。第四行b被推断为number,访问length报错。

对象的类型–接口

面向对象语言中,接口(Interfaces)是一个很重要的概念,是对行为的抽象,具体如何行动需要由类去实现。
TypeScript 中的接口可用于对类的一部分行为进行抽象,也常用于对对象的形状进行描述。

1
2
3
4
5
6
7
8
interface Person {
name: string;
age: number;
}
let tom: Person = {
name: "Tom",
age: 25
};

接口一般首字母大写。有的编程语言中会建议接口的名称加上 I 前缀。
定义的变量比接口少了一些属性是不允许的。

1
2
3
4
let tom: Person = {
name: "Tom"
};
// error TS2741: Property 'age' is missing in type '{ name: string; }' but required in type 'Person'.

多了一些属性也是不允许的

1
2
3
4
5
6
7
let tom: Person = {
name: "Tom",
age: 25,
weight: 10
};
//error TS2322: Type '{ name: string; age: number; weight: number; }' is not assignable to type 'Person'.
// Object literal may only specify known properties, and 'weight' does not exist in type 'Person'.

赋值的时候,变量的形状必须和接口的形状保持一致。

可选属性

不需要完全匹配

1
2
3
4
5
6
7
8
9
10
11
interface Person {
name: string;
age?: number;
}
let tom: Person = {
name: "Tom"
};
let jerry: Person = {
name: "Jerry",
age: 25
};

但是非可选属性必须存在,也不允许添加未定义属性。

任意属性

希望接口允许有任意属性。

1
2
3
4
5
6
7
8
9
10
interface Person {
name: string;
age?: number;
[propName: string]: any;
}
let jerry: Person = {
name: "Jerry",
age: 25,
weight: 50
};

注意,一旦定义了任意属性,那么确定属性和可选属性的类型必须是它的类型子集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Person {
name: string;
age?: number;
[propName: string]: string;
}
let jerry: Person = {
name: "Jerry",
age: 25,
weight: 50
};
//Property 'age' of type 'number' is not assignable to string index type 'string'.
//Type '{ name: string; age: number; gender: string; }' is not assignable to type 'Person'.
//Property 'age' is incompatible with index signature.
//Type 'number' is not assignable to type 'string'.

任意属性的值允许是 string,但是可选属性 age 的值却是 number,number 不是 string 的子属性,所以报错

只读属性

对象的属性只能在对象创建的时候被赋值,可以使用 readonly 定义只读属性

1
2
3
4
5
6
7
8
let tom: Person = {
id: 123,
name: "Tom",
age: 25,
gender: "male"
};
tom.id=345;
// error TS2540: Cannot assign to 'id' because it is a read-only property.

注意:只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Person {
readonly id: number;
name: string;
age?: number;
[propName: string]: any;
}

let tom: Person = {
name: "Tom",
age: 25,
gender: "male"
};
tom.id = 345;
//error TS2741: Property 'id' is missing in type '{ name: string; age: number; gender: string; }' but required in type 'Person'.
//error TS2540: Cannot assign to 'id' because it is a read-only property.

数组 Array

  1. 元素类型后面加上[]
1
let list: number[] = [1, 2, 3];

编译为

1
var list = [1, 2, 3];
  1. 使用数组泛型,Array<元素类型>
1
let list:Array<number> = [1,2,3]
  1. 用接口表示数组
1
2
3
4
interface NumberArray {
[index: number]: number;
}
let fibonacci: NumberArray = [1, 2, 3, 4, 5];

NumberArray表示:只要索引的类型是数字,值的类型必须为数字
虽然接口可以用来描述数组,但是这种结果太复杂,一般不使用。

  1. 类数组
1
2
3
4
function sum() {
let args: number[] = arguments;
}
//error TS2740: Type 'IArguments' is missing the following properties from type 'number[]': pop, push, concat, join, and 15 more.

arguments 实际是一个类数组,不能用普通的数组的方式来描述。而用接口:

1
2
3
4
5
6
7
function sum() {
let args: {
[index: number]: number;
length: number;
callee: Function;
} = arguments;
}

约定索引的类型和值的类型必须是数字外,也约束了 length 和 callee 两个属性。
事实上常用的类数组都有自己的接口定义,如IArguments,NodeList,HTMLCollection

1
2
3
function sum() {
let args: IArguments = arguments;
}

其中 IArguments 是 TypeScript 中定义好的类型,它实际上就是:

1
2
3
4
5
interface IArguments{
[index: number]: any;
length: number;
callee: Function;
}
  1. any 在数组中的应用
    一个比较常见的做法是,用 any 表示数组中允许出现任意类型
1
let list: any[] = ["string", 24, { websites: "http://www.ss.com" }];

函数的类型

函数声明

1
2
3
4
5
6
7
8
//函数声明
function sum(x,y){
return x+y
}
//函数表达式
let muSum = function(x,y){
return x+y
}

TypeScript 中对函数的输入输出进行约束,其中函数声明的类型定义较简单

1
2
3
function sum(x: number, y: number): number {
return x + y;
}

注意,输入多余或少于要求的参数,是不被允许的

1
2
3
sum(1)
//error TS2375: Duplicate number index signature.
//error TS2554: Expected 2 arguments, but got 1.

函数表达式

1
2
3
let mySum = function(x: number, y: number): number {
return x + y;
};

上述定义,可以编译通过,但是只对等号右边的匿名函数进行类型定义,而等号左边的mySum,是通过赋值操作进行类型推论而推断出来的。
如果需要我们手动给 mySum 添加类型,则应该是这样:

1
2
3
let mySum:(x:number,y:number) =>number = function(x: number, y: number): number {
return x + y;
};

不要混淆 TypScript 中=>和 ES6 中的=>
在 TypeScript 的类型定义中,=>用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。

用接口定义函数的类型

1
2
3
4
5
6
7
interface SearchFun {
(source: string, subString: string): boolean;
}
let mySearch: SearchFun;
mySearch = function(source: string, subString: string) {
return source.search(subString) !== 1;
};

可选参数

?定义可选参数

1
2
3
4
5
6
7
8
9
function buildName(firstName: string, lastName?: string) {
if (lastName) {
return firstName + "" + lastName;
} else {
return firstName;
}
}
let tomcat = buildName("Tom", "Cat");
let tom1 = buildName("Tom");

注意,可选参数必须在必需产生后面,可选参数后面不能出现必需参数

1
2
3
4
5
6
7
8
9
10
function buildName(lastName?: string, firstName: string) {
if (lastName) {
return firstName + "" + lastName;
} else {
return firstName;
}
}
let tomcat = buildName("Tom", "Cat");
let tom1 = buildName(undefined, "Tom");
// error TS1016: A required parameter cannot follow an optional parameter.

参数默认值

TypeScript 会将添加了默认值的参数识别为可选参数

1
2
3
4
5
6
7
8
9
function buildName(firstName: string, lastName: string = "Cat") {
if (lastName) {
return firstName + "" + lastName;
} else {
return firstName;
}
}
let tomcat = buildName("Tom", "Cat");
let tom1 = buildName("Tom");

此时不受可选参数必须在参数后面的限制了

1
2
3
4
5
6
7
8
9
function buildName(lastName: string = "Cat", firstName: string) {
if (lastName) {
return firstName + "" + lastName;
} else {
return firstName;
}
}
let tomcat = buildName("Tom", "Cat");
let tom1 = buildName(undefined, "Tom");

剩余参数

...rest获取剩余参数

1
2
3
4
5
6
7
function push(array: any[], ...items: any[]) {
items.forEach(item => {
array.push(item);
});
}
let c = [];
push(c, 1, 2, 3);

注意,rest 参数只能是最后一个参数

重载

允许一个函数接受不同数量或类型的参数时,作出不同的处理。
实现一个函数reverse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function reverse(x: number | string): number | string {
if (typeof x === "number") {
return Number(
x
.toString()
.split("")
.reverse()
.join("")
);
} else if (typeof x === "string") {
return x
.split("")
.reverse()
.join("");
}
}

可以使用重载定义多个 reverse 的函数类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string {
if (typeof x === "number") {
return Number(
x
.toString()
.split("")
.reverse()
.join("")
);
} else if (typeof x === "string") {
return x
.split("")
.reverse()
.join("");
}
}

TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。

声明文件

使用第三方库的时候,需要引用其声明文件,才能获得对应的代码补全、接口提示等功能。

声明语句

  • 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扩展模块
  • ///<reference/>三斜线指令

例如,ts 无法识别用 script 标签引入的 Jquery,需要声明

1
2
declare var jQuery:(selector:string) => any
jQuery('#foo')

编译为

1
jQuery('#foo')

将声明语句放到一个单独文件(jQuery.d.ts)中

1
2
//src/jQuery.d.ts
declare var jQuery:(selector:string) => any
1
2
// src/index.ts
jQuery('#foo');

注意,声明文件必须以.d.ts 结尾
一般 ts 会解析项目中所有的*.ts文件,当然包括.d.ts结尾的文件,所以将jQuery.d.ts放到项目中,其他所有*.ts文件都可以获得jQuery的类型定义
项目目录

1
2
3
4
5
/path/to/project
├── src
| ├── index.ts
| └── jQuery.d.ts
└── tsconfig.json

假如仍然无法解析,可以检查一下tsconfig.json中的files,includeexclude配置,确保其包含了jQuery.d.ts文件。

第三方声明文件

jQuery 声明文件可以直接下载,jQuery in DefinitelyTyped)使用。
更推荐使用@types统一管理第三方库的声明文件。

1
npm install @types/jquery --save-dev

可以使用TypeSearch搜索你要安装的库

书写声明文件

第三方库没有提供声明文件时,需要自己书写声明文件。
不同的场景下,声明文件的内容和使用方式不同

  • 全局变量:通过<script>标签引入第三方库。注入全局变量;
  • npm 包:通过import foo from 'foo导入,符合 ES6 规范;
  • UDM 库:既可以通过<script>标签引入,又可以通过import引入;
  • 直接扩展全局变量:通过<script>标签引入后,改变一个全局变量结构;
  • 在 npm 包或 UMD 库中扩展全局变量:引用npm包UMD库后,改变一个全局变量结构;
  • 模块插件:通过<script>引入后,改变另一个模块的结构。

全局变量

之前举的例子就是通过<script> 标签引入 jQuery,注入全局变量 \$ 和 jQuery。
如果是以npm install @types/xxx --save-dev安装的第三方库,就不需要配置。如果是将声明文件直接存放在当前项目中,则建议和其他源码一起放在src目录下(或对应源码目录下)

1
2
3
4
5
/path/to/project
├── src
| ├── index.ts
| └── jQuery.d.ts
└── tsconfig.json

全局变量的声明文件语法:

  • declare var 声明全局变量
  • declare function 声明全局方法
  • declare class 声明全局类
  • declare enum 声明全局枚举类型
  • declare namespace 声明(含子属性的)全局对象
  • interface 和 type 声明全局类型

declare var

还有declare letdeclare const,与直接使用let,const没区别:
// src/jQuery.d.ts

1
declare let jQuery: (selector: string) => any;

// src/index.ts

1
2
3
4
5
jQuery('#foo');
// 使用 declare let 定义的 jQuery 类型,允许修改这个全局变量
jQuery = function(selector) {
return document.querySelector(selector);
};

let 声明的变量可以更改,const 更改的不能修改
// src/jQuery.d.ts

1
declare const jQuery: (selector: string) => any;

// src/index.ts

1
2
3
4
jQuery = function(selector) {
return document.querySelector(selector);
};
//error TS2588: Cannot assign to 'jQuery' because it is a constant.

注意:声明语句中只能定义类型,切勿在声明语句中定义具体的实现

1
2
3
4
declare const jQuery = function(selector) {
return document.querySelector(selector);
};
//error TS1183: An implementation cannot be declared in ambient contexts.

declare function

用来定义全局函数的类型,jQuery 其实就是一个函数,也可以使用function来定义

1
declare function jQuery(selector:string):any

在函数类型的声明语句中,函数重载也支持:
//src/jQuery.d.ts

1
2
declare function jQuery(selector:string):any
declare function jQuery(domReadyCallback:()=>any):any

//src/index.ts

1
2
3
4
jQuery('#foo');
jQuery(function(){
alert('Dom Ready!')
})

declare class

当全局变量是一个类时,使用declare class定义它的类型

1
2
3
4
5
6
//src/Animal.d.ts
declare class Animal{
name:string;
constructor(name:string);
sayHi():string;
}

同样的,declare class 语句也只能用来定义类型,不能用来定义具体的实现,比如定义 sayHi 方法的具体实现则会报错:

1
2
3
4
5
6
7
8
9
10
// src/Animal.d.ts

declare class Animal {
name: string;
constructor(name: string);
sayHi() {
return `My name is ${this.name}`;
};
// ERROR: An implementation cannot be declared in ambient contexts.
}

declare enum

使用declare enum定义的枚举类型也称作外部枚举 (Ambient Enums)

仅用于定义类型,而不是具体的值。

declare namespace

命名空间
早前 ts 提供了一种模块化方案,使用module关键字表示内部模块,后来为了兼容 ES6 就把module替换为namespcae,现在已经不建议使用 ts 的namespace
declare namespace 还是常用的,用来表示全局变量是一个对象,包含很多子属性。
例如 jQuery 对象中的 ajax 方法
src/jQuery.d.ts

1
2
3
declare namespace jQuery {
function ajax(url: string, settings?: any): void;
}

src/index.ts

1
jQuery.ajax('/api/get')

注意,在declare namespace 内部,直接使用 function ajax 声明函数,而不是使用 declare function ajax。类似里面还可以使用 const,class,enum
src/jQuery.d.ts

1
2
3
4
5
6
7
8
9
10
declare namespace jQuery {
function ajax(url: string, settings?: any): void;
const version: number;
class Event {
blur(eventType: EventType): void;
}
enum EventType {
CustomClick
}
}

src/hello.ts

1
2
const e = new jQuery.Event();
e.blur(jQuery.EventType.CustomClick);
嵌套的命名空间

如果对象拥有深层的层级,则需要用嵌套的namespace来声深层属性的类型
src/jQuery.d.ts

1
2
3
4
5
6
declare namespace jQuery {
function ajax(url: string, settings?: any): void;
namespace fn {
function extend(object: any): void;
}
}

src/index.ts

1
2
3
4
5
6
7
jQuery.fn.extend({
check: function() {
return this.each(() => {
this.checked = true;
});
}
});

假如jQuery下面仅有fn这一属性(没有ajax等其他属性或方法),则可以不需要嵌套namespace;
src/jQuery.d.ts

1
2
3
declare namespace jQuery.fn {
function extend(object:any):void;
}

src/index.ts

1
2
3
4
5
6
7
jQuery.fn.extend({
check: function() {
return this.each(() => {
this.checked = true;
});
}
})

interface 和 type

直接使用interfacetype来声明一个全局的接口或类型。
src/jQuery.d.ts

1
2
3
4
5
6
7
interface AjaxSettings {
method?: "GET" | "POST";
data?: any;
}
declare namespace jQuery {
function ajax(url: string, settings?: AjaxSettings);
}

其他文件也可以使用这个接口或类型

1
2
3
4
5
6
7
let settings: AjaxSettings = {
method: "POST",
data: {
name: "foo"
}
};
jQuery.ajax("/api/get", settings);
防止命名冲突

暴露在最外层的interfacetype会作为全局类型作用于整个项目,应该尽量减少全局变量或全局类型的数量,故最好将他们放到namespace

1
2
3
4
5
6
7
declare namespace jQuery {
interface AjaxSettings {
method?: "GET" | "POST";
data?: any;
}
function ajax(url: string, settings?: AjaxSettings);
}

注意,使用interface的时候,也应该加上jQuery前缀
src/jQuery.d.ts

1
2
3
4
5
6
7
declare namespace jQuery {
interface AjaxSettings {
method?: "GET" | "POST";
data?: any;
}
function ajax(url: string, settings?: AjaxSettings);
}

src/index.ts

1
2
3
4
5
6
7
let settings: jQuery.AjaxSettings = {
method: 'POST',
data: {
name: 'foo'
}
};
jQuery.ajax('/api/post_something', settings);
声明合并

假如 jQuery 既是一个函数,可以直接被调用jQuery("#foo"),又是一个对象拥有子属性jQuery.ajax(),那么我们可以组合多个声明语句,他们不冲突的合并起来。
src/jQuery.d.ts

1
2
3
4
5
6
7
8
declare function jQuery(selector: string): any;
declare namespace jQuery {
interface AjaxSettings {
method?: "GET" | "POST";
data?: any;
}
function ajax(url: string, settings?: AjaxSettings);
}

src/hello.js

1
2
3
4
5
6
7
8
jQuery("#foo");
let settings: jQuery.AjaxSettings = {
method: "POST",
data: {
name: "foo"
}
};
jQuery.ajax("/api/get", settings);

npm 包

ES6:import foo from 'foo'
为 npm 包创建声明文件之前,需要先看它的声明文件是否存在,一般来说,npm 包的声明文件可以存在于两个地方:

  1. 与该 npm 包绑定在一起。判断依据package.json中有types字段,或者有一个index.d.ts声明文件。这种模式不需要额外安装其他包,最为推荐,所以以后我们自己创建 npm 包的时候,最好将声明文件与 npm 包绑定在一起。
  2. 发布到@types里。我们只需要尝试安装一下对应的@types包就知道是否存在该声明文件,安装命令是npm install @types/foo --save-dev。这种模式一般是由于 npm 包的维护者没有提供声明文件,所以只能由其他人将声明文件发布到@types

假如以上两种方式都没有找到对应的声明文件,那么就需要自己写声明文件,由于是通过import语句导入模块,所以声明文件存放的位置也有所约束,一般有两种方案:

  1. 创建一个node_module/@types/foo/index.d.ts文件,存放foo模块的声明文件。这种方式不需要额外配置,但是node_modules目录不稳定,代码也没有被保存到仓库,无法回溯版本,有被删除的风险,故不太建议用这种方案,一般只做临时测试。
  2. 创建一个types目录,专门用来管理自己写的声明文件,将foo的声明文件放到types/foo/index.d.ts中,这种方式需要配置下tsconfig.json中的pathsbaseUrl字段
    目录结构:
1
2
3
4
5
6
7
/path/to/project
├── src
| └── index.ts
├── types
| └── foo
| └── index.d.ts
└── tsconfig.json

tsconfig.json 内容:

1
2
3
4
5
6
7
8
9
{
"compilerOptions": {
"module": "commonjs",
"baseUrl": "./",
"paths": {
"*": ["types/*"]
}
}
}

如此配置之后,通过 import 导入 foo 的时候,也会去 types 目录下寻找对应的模块的声明文件了。
注意,module配置可以有多种选项,不同的选项影响模块的导入导出模式,commonjs是最常用的选项
npm 包的声明文件主要有以下几种语法:

  • export 导出变量
  • export namespace 导出(含子属性的)对象
  • export default ES6 默认导出
  • export= commonjs 导出模块

export

npm 包的声明文件与全局变量的声明文件有很大的区别,使用declare不再会声明一个全局变量,而只会在当前文件中声明一个局部变量。只有在声明文件中使用export导出,然后在使用import导入,才会应用到这些类型声明。
export的语法与普通 ts 语法类似,区别在于声明文件中禁止定义具体的实现:
src/foo/index.d.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export const name: string;
export function getName(): string;
export class Animal {
constructor(name: string);
sayHi(): string;
}
export enum Directions {
Up,
Down,
Left,
Right
}
export interface Options {
data: any;
}

对应的导入和使用模块应该这样使用
src/index.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { name, getName, Animal, Directions, Options } from "foo";
console.log(name);
let myName = getName();
let cat = new Animal("Tom");
let directions = [
Directions.Up,
Directions.Down,
Directions.Left,
Directions.Right
];
let options: Options = {
data: {
name: "foo"
}
};

混用 declare 和 export

先使用declare声明多个变量,然后再使用export一次性导出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
declare const name: string;
declare function getName(): string;
declare class Animal {
constructor(name: string);
sayHi(): string;
}
declare enum Directions {
Up,
Down,
Left,
Right
}
interface Options {
data: any;
}
export { name, getName, Animal, Directions, Options };

注意,与全局变量声明文件类似,interface前不需要declare

export namespace

导出一个拥有子属性的对象
types/foo/index.d.ts

1
2
3
4
5
6
export namespace foo {
const name: string;
namespace bar {
function baz(): string;
}
}

src/index.ts

1
2
3
import {foo} from 'foo';
console.log(foo.name)
foo.bar.baz()

export default

导出默认值
types/foo/index.d.ts

1
export default function foo():string

src/index.ts

1
2
import foo from 'foo'
foo()

注意,只有function,class,interface可以直接默认导出,其他变量需要先定义再默认导出

1
2
3
4
5
6
7
export default Directions;
declare enum Directions {
Up,
Down,
Left,
Right
}

默认导出一般放在声明文件的最前面

export =

commonjs规范中,导出模块

1
2
3
4
//整体导出
module.exports = foo
//单个导出
exports.bar=bar

导入方式

  1. const ... = require
1
2
3
4
//整体导入
const foo = require('foo')
//单个导入
const bar = require('foo').bar
  1. ‘import … from ‘
    整体导入 import * as ... from
1
2
3
4
//整体导入
import * as foo from 'foo';
//单个导入
import {bar} from 'foo'
  1. import ... require,ts 官方推荐
1
2
3
4
//整体导入
import foo = require('foo') ;
//单个导入
import bar = foo.bar;

对于这种使用 commonjs 规范的库,假如为它写类型声明文件的话,就需要使用export =这种语法

1
2
3
4
5
export = foo;
declare function foo():string;
declare namespace foo{
const bar:number
}

上例使用export =之后,就不能使用单个导出export {bar}。通过声明合并,使用declare namespace foo来将bar合并到foo
export = 不仅可以用在声明文件中,也可以用在普通的 ts 文件中。实际上,import ... require 和 export = 都是 ts 为了兼容 AMD 规范和 commonjs 规范而创立的新语法,由于并不常用也不推荐使用。
不推荐使用export =,除非第三方使用 commonjs 规范。推荐使用 ES6 标准的export defaultexport

UMD 库

既可以通过<script>标签引入,又可以通过import导入的库,称为 UMD 库。相比于 npm 包的类型声明文件,我们需要额外声明一个全局变量,为了实现这种方式,ts 提供一个新语法export as namespace

export as namespace

一般使用export as namespace时,都先有了 npm 包的声明文件,再基于它添加一条export as namespace语句,即可将声明好的一个变量声明为全局变量。

1
2
3
4
5
6
export as namespace foo;
export = foo;
declare function foo(): string;
declare namespace foo {
const bar: number;
}

也可以与export default一起使用

1
2
3
4
5
6
export as namespace foo;
export default foo;
declare function foo(): string;
declare namespace foo {
const bar: number;
}

直接扩展全局变量

有的第三方库扩展了一个全局变量,可是全局变量的类型却没有相应的更新过来。就会导致 ts 编译出错
扩展String类型:

1
2
3
4
interface String{
prependHello().string;
}
'foo'.prependHello()

通过声明合并,使用interface String即可给String添加属性或方法
也可以使用declare namespace给已有的命名空间添加类型声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// types/jquery-plugin/index.d.ts

declare namespace JQuery {
interface CustomOptions {
bar: string;
}
}

interface JQueryStatic {
foo(options: JQuery.CustomOptions): string;
}
// src/index.ts

jQuery.foo({
bar: ''
});

在 npm 包或 UMD 库中扩展全局变量

如之前所说,对于一个 npm 包或者 UMD 库的声明文件,只有export 导出的类型声明才能被导入。所以对于 npm 包或 UMD 库,如果导入此库之后会扩展全局变量,则需要使用另一种语法在声明文件中扩展全局变量的类型,那就是 declare global

declare global

可以在 npm 包或者 UMD 库的声明文件中扩展全局变量的类型。
types/foo/index.d.ts

1
2
3
4
5
6
declare global {
interface String {
prependHello(): string;
}
}
export {};

src/index.ts

1
'bar'.prependHello();

注意,此声明文件不需要导出任何东西,但是仍需要导出一个空对象,用来告诉编译器这是一个模块的声明文件,而不是一个全局变量的声明文件

模块插件

通过import导入一个模块插件,可以改变另一个原有模块的结构。此时如果原有模块已经有了类型声明文件,而插件模块没有类型声明文件,导致类型不完整。

declare module

如果需要扩展原有模块的话,需要在类型声明文件中引用原有模块,再使用declare module扩展原有模块。
types/moment-plugin/index.d.ts

1
2
3
4
5
6

import * as moment from 'moment';

declare module 'moment' {
export function foo(): moment.CalendarKey;
}

src/index.ts

1
2
3
4
import * as moment from 'moment';
import 'moment-plugin';

moment.foo();

一次性声明多个模块类型
types/foo-bar.d.ts

1
2
3
4
5
6
7
8
declare module "foo" {
export interface Foo {
foo: string;
}
}
declare module "bar" {
export function bar(): string;
}

src/index.ts

1
2
3
4
import { Foo } from 'foo';
import * as bar from 'bar';
let f:Foo;
bar.bar()

声明文件中的依赖

一个声明文件依赖另一个声明文件中的类型,declare module例子中,导入moment,并使用了moment.CalendarKey类型:

1
2
3
4
5
6
7
// types/moment-plugin/index.d.ts

import * as moment from 'moment';

declare module 'moment' {
export function foo(): moment.CalendarKey;
}

除了可以在声明文件中通过import导入另一个声明文件,还可以使用三斜线指令导入

三斜线指令

现在推荐使用 ES6 来实现模块之间的依赖,不过声明模块之间的依赖关系还需要使用三斜线指令

  • 当我们在书写一个全局变量的声明文件时
    在全局变量的声明文件中,不允许出现importexport关键字,一旦出现,文件就会被视为一个 npm 包或 UMD 库,如果全局变量声明文件需要引用另一个库,就必须使用三斜线指令

    1
    2
    /// <reference types="jquery" />
    declare function foo(options: JQuery.AjaxSettings): string;

    语法 ///后面使用 xml 格式添加对jquery类型的依赖,这样就可以在声明文件中使用JQuery.AjaxSettings类型
    注意,三斜线指令必须放在文件的最顶端,三斜线指令的最前面值允许出现单行或多行注释

  • 当我们需要依赖一个全局变量的声明文件时
    types/node-plugin/index.d.ts

1
2
///<reference types='node'/>
export function foo(p: NodeJS.Process): string;

src/index.ts

1
2
import {foo } from 'node-plugin';
foo(global.process)

由于引入的 node 中的类型都是全局变量的类型,它们是没有办法通过 import 来导入的,所以这种场景下也只能通过三斜线指令来引入了。

拆分声明文件

当全局声明文件太大时,可以拆分为多个文件,然后在一个入口文件中将他们一一引入,提高代码的可维护性。
jquery的声明文件:

1
2
3
4
5
6
7
/// <reference types="sizzle" />
/// <reference path="JQueryStatic.d.ts" />
/// <reference path="JQuery.d.ts" />
/// <reference path="misc.d.ts" />
/// <reference path="legacy.d.ts" />

export = jQuery;

其中用到了typespath两种不同参数。

  • types:用于声明对另一个库的依赖,
  • path:用于声明对另一个文件的依赖

自动生成声明文件

如果库的源码本身就由 ts 写的,那么在使用 tsc 脚本将 ts 编译为 js 的时候,添加declaration选项。

1
2
3
4
5
6
7
{
"compilerOptions": {
"module": "commonjs",
"outDir": "lib",
"declaration": true,
}
}
  • outDir,将 ts 文件编译后的结果输出的到 lib 目录下
  • declaration,执行编译的时候,ts 文件不仅生成.js文件同时自动生成.d.ts声明文件,也会输出到lib目录下
    其他配置
  • declarationDir,设置生成.d.ts文件的目录
  • declarationMap,对每个.d.ts文件,生成对应的.d.ts.map文件
  • emitDeclarationOnly,仅生成’.d.ts’文件,不生成.js文件

内置对象

ECMAScript 的内置对象

根据 js 的内置对象定义 ts 变量

1
2
3
4
let aa: Boolean = new Boolean(1);
let bb: Error = new Error("Error occurred");
let cc: Date = new Date();
let dd: RegExp = /[a-z]/;

DOM 和 BOM 的内置对象

1
2
3
let body: HTMLElement = document.body;
let allDiv: NodeList = document.querySelectorAll("div");
document.addEventListener("click", function(e: MouseEvent) {});

TypeScript 核心库的定义文件

定义了所有了浏览器环境需要用到的类型,并且预置在 TypeScript 中

1
2
Math.pow(10, "2");
//error TS2345: Argument of type '"2"' is not assignable to parameter of type 'number'.

Math.pow 必须接受两个 number 类型的参数。Math.pow 的类型定义如下

1
2
3
4
5
6
7
8
interface Math {
/**
* Returns the value of a base expression taken to a specified power.
* @param x The base value of the expression.
* @param y The exponent value of the expression.
*/
pow(x: number, y: number): number;
}

DOM 的例子

1
2
3
4
5
document.addEventListener('click', function(e) {
console.log(e.targetCurrent);
});

// index.ts(2,17): error TS2339: Property 'targetCurrent' does not exist on type 'MouseEvent'.

addEventListener 方法是在 TypeScript 核心库中定义的:

1
2
3
interface Document extends Node, GlobalEventHandlers, NodeSelector, DocumentEvent {
addEventListener(type: string, listener: (ev: MouseEvent) => any, useCapture?: boolean): void;
}

注意:TypeScript 核心库的定义不包含 Node.js 部分

用 TypeScript 写 Node.js

node.js 不是内置对象的一部分,如果要用 TypeScript 写 Node.js,则需要引入第三方声明文件:

1
npm install @types/node --save-dev
-------------本文结束感谢阅读-------------