TypeScript渐进式入门

本部分主要是一些基础概念,掌握后可以从JavaScript语法习惯转变为TypeScript。

基础类型

表示形式:

1
const foo:string = "test"
  • 字符串:string
  • 数字:number
  • 布尔值:boolean
  • 空值:void (主要用于表示函数无返回值或返回值为null时的定义)
  • 任意值:any (意味着不限制类型,可以为任意值)
  • 其他:undefinednull

类型推断

TS提供两种类型推论机制(代码中未定义类型时,编译时自动增加类型)

1
2
3
4
let bar     //  等同于 let bar:any
bar = "ok"

const t1 = "str" // 等价于const t1:string = "str"

需要注意的是,在使用类型推断后,不能在随意修改为其他类型,例如

1
2
let bar = "string" // 此时相当于被ts推断为了 let bar:string = "string"
bar = 1 // error 类型冲突

数组也是如此

1
2
3
4
5
6
let bar = ['a','b']
bar[1] = 'c' // OK
bar[0] = 1 // ERROR

bar = ['b','a'] // OK
bar = [2, 1] // ERROR

对于对象

1
2
3
4
let bar = { a: '123', b: 234, c: true } //自动被推断生成一个接口
bar.a = 'test' // OK
bar.b = 'test' //ERROR 类型冲突
bar.d = 123 // ERROR 不存在属性“d”

联合类型

1
let myFavoriteNumber: string | number;

联合类型使用 | 分隔每个类型。

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

1
2
3
function getLength(something: string | number): number {
return something.length; // 编译器发生报错
}
1
2
3
4
5
let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
console.log(myFavoriteNumber.length); // 5
myFavoriteNumber = 7;
console.log(myFavoriteNumber.length); // 编译时报错

交叉类型

extend是一种非常常见的模式,可以从多个对象合并为一个新的对象。而在TS中,交叉类型可以让你安全的使用此模式 ,通过& 对类型进行连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function extend<T extends object, U extends object,B>(first: T, second: U, dsf:B): T & U & B{
let res = {} as T & U & B // 将res别名设置为T、U、B泛型的交叉类型,为了能够正常将res return出去
for (const key in first) {
(res as T)[key] = first[key]
}
for (const key in second) {
if(!Object.prototype.hasOwnProperty.call(res,key)){
(res as U)[key] = second[key]
}
}
for (const key in dsf) {
if(!Object.prototype.hasOwnProperty.call(res,key)){
(res as B)[key] = dsf[key]
}
}
return res
}
let bp = extend({a:'123'},{a:'222',b:234},{b: 888, c:true}) //{ a: '123', b: 234, c: true }

接口

接口用于规范对象的属性和方法,是对象的类型。

确定属性

1
2
3
4
5
6
7
8
9
interface Person{
name:string;
age:number;
}

let Tom:Person{
name: "Tom",
age:19,
}

接口定义关键字interface;每个字段定义完毕之后用分号分割

接口的定义与Java一致,实例赋值的时候不能比接口定义的少或者多,需要与接口的形状保持一致。

可选属性

使用?修饰的属性是可选的,不要求实例必须实现该属性

1
2
3
4
5
6
7
8
interface Person {
name: string;
age?: number;
}

let tom: Person = {
name: 'Tom'
};

这时仍然不允许添加未定义的属性

任意属性

在有些书上也会被称之为“索引签名”。 有时候我们希望一个接口允许有任意的属性,可以使用如下方式:

1
2
3
4
5
6
7
8
9
10
interface Person {
name: string;
age?: number;
[propName: string]: any;
}

let tom: Person = {
name: 'Tom',
gender: 'male'
};

使用[propName: string]定义了任意属性可以取any值

任意属性的stringKeyType,而any代表ValueType

需要注意的是,任意属性被定义后,其他与任意属性相同KeyType的各种属性在赋值时会先去校验一次任意属性的ValueType

以下代码将发生变异错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Person {
name: string;
age?: number;
[propName: string]: string; // string 无法同时满足name和age
}

let tom: Person = {
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 'Person'.
// Index signatures are incompatible.
// Type 'string | number' is not assignable to type 'string'.
// Type 'number' is not assignable to type 'string'.

上例中将任意属性改为联合类型即可

1
[propName: string]: string | number;

或者把任意属性的类型改成非string类型(当然这也就意味着新增的任意属性值只能为number了)

1
[propName: number]: string;
使用一组有限的字符串字面量作为属性

一个索引签名可以通过映射类型来使索引字符串为联合类型中的一员,如下所示:

1
2
3
4
5
6
7
8
9
type Index = 'a' | 'b' | 'c';
type FromIndex = { [k in Index]?: number };

const good: FromIndex = { b: 1, c: 2 };

// Error:
// `{ b: 1, c: 2, d: 3 }` 不能分配给 'FromIndex'
// 对象字面量只能指定已知类型,'d' 不存在 'FromIndex' 类型上
const bad: FromIndex = { b: 1, c: 2, d: 3 };

只读属性

如果需要某个属性在对象赋值后就不能被改变可以在接口中使用readonly关键字来定义只读属性

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

let tom: Person = {
id: 89757,
name: 'Tom',
gender: 'male'
};

tom.id = 9527;

// index.ts(14,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.

只读属性也是在定义时必必须要填的,只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候

数组类型

类型+方括号表示法

数组内容需要符合数组类型定义

1
let fibonacci: number[] = [1, 1, 2, 3, 5]; //只能存在number类型的数组元素

数组泛型

也可以使用范型的方式来定义数组类型Array<elemType>

1
let fibonacci: Array<number> = [1, 1, 2, 3, 5];

用接口表示数组

利用任意属性来表示数组

1
2
3
4
interface NumberArray {
[index: number]: number;
}
let fibonacci: NumberArray = [1, 1, 2, 3, 5];

通常不用这种方法来表示数组,但是这个方式可以有效的用来表示类数组

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

事实上常用的类数组都有自己的接口定义,如 IArguments, NodeList, HTMLCollection [参考内置对象章节]等:

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

函数类型

函数声明

function

1
2
3
function sum(a:number,b:number):number{
return a+b
}

函数表达式

1
2
3
4
5
6
7
8
9
10
11
//类型推断简写方式
let sum = function (a:number,b:number):number{
return a+b
}

//或者

let sum:(x:number,y:number)=>number = function (a:number,b:number):number{
return a+b
}
console.log(sum(3,5))

箭头函数

1
2
3
let sum = (a:number,b:number):number=>{
return a+b
}

回调函数

1
2
3
4
5
6
7
function map(array:any[],callback:(el?:any,index?:number,arr?:any[])=>any):any{
let result:any[] = []
for(let i:number = 0;i<array.length;i++){
result.push(callback(array[i],i,[...array]))
}
return result
}

仿写一个TS版的Array.prototype.map方法。其中map的第二个形参就是回调函数定义的方法。

利用接口定义函数的形状

1
2
3
4
5
6
7
8
interface sumFunc{
(a:number,b:number):number
}

let sum:sumFunc
sum = function(a:number,b:number){
return a+b
}

采用函数表达式|接口定义函数的方式时,对等号左侧进行类型限制,可以保证以后对函数名赋值时保证参数个数、参数类型、返回值类型不变。

可选参数

正常情况下,输入多余的(或者少于要求的)参数,是不允许的。但是可以用 ? 表示可选的参数

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 tom = buildName('Tom');

需要注意的是,可选参数必须接在必需参数后面。换句话说,可选参数后面不允许再出现必需参数了

函数重载

1
2
3
4
5
6
7
8
9
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('');
}
}

类型别名

可以为类型定义别名,定义后可以直接使用别名来进行变量声明

1
2
3
4
5
type Name = string
type NameResolve = () => Name
function getName(n:NameResolve):Name{
return n()
}

字面量类型

字面量类型用来约束取值只能是某几个字符串中的一个。

1
2
3
4
5
6
type ab = 1 | '1' | true

let k:ab = 1

let b:ab = 2
//error TS2322: Type '2' is not assignable to type 'ab'.

元组

数组是合并相同类型的对象,而元组可以合并不同类型的对象

定义一对值分别为 stringnumber 的元组:

1
let tom: [string, number] = ['Tom', 25];

当赋值或访问一个已知索引的元素时,会得到正确的类型:

1
2
3
4
5
6
let tom: [string, number];
tom[0] = 'Tom';
tom[1] = 25;

tom[0].slice(1);
tom[1].toFixed(2);

也可以只赋值其中一项:

1
2
let tom: [string, number];
tom[0] = 'Tom';

但是当直接对元组类型的变量进行初始化或者赋值的时候,需要提供所有元组类型中指定的项。

1
2
3
4
5
6
let tom: [string, number];
tom = ['Tom', 25];
let tom: [string, number];
tom = ['Tom'];

// Property '1' is missing in type '[string]' but required in type '[string, number]'.

越界的元素

当添加越界的元素时,它的类型会被限制为元组中每个类型的联合类型:

1
2
3
4
5
6
let tom: [string, number];
tom = ['Tom', 25];
tom.push('male');
tom.push(true);

// Argument of type 'true' is not assignable to parameter of type 'string | number'.

注意ts本质上无法进行运行时检测,所以push被优化为了联合类型检测。但是,如果是直接字面量对数组进行赋值,编译是无法通过的。

1
2
3
let tom:[string,number]
tom[2] = 123
//. Tuple type '[string, number]' of length '2' has no element at index '2'.

枚举

普通枚举

枚举(Enum)类型用于取值被限定在一定范围内的场景,比如一周只能有七天,颜色限定为红绿蓝等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};
console.log(Days);
/*
{
'0': 'Sun',
'1': 'Mon',
'2': 'Tue',
'3': 'Wed',
'4': 'Thu',
'5': 'Fri',
'6': 'Sat',
Sun: 0,
Mon: 1,
Tue: 2,
Wed: 3,
Thu: 4,
Fri: 5,
Sat: 6
}
*/

事实上,上面的例子会被编译为:

1
2
3
4
5
6
7
8
9
10
var Days;
(function (Days) {
Days[Days["Sun"] = 0] = "Sun";
Days[Days["Mon"] = 1] = "Mon";
Days[Days["Tue"] = 2] = "Tue";
Days[Days["Wed"] = 3] = "Wed";
Days[Days["Thu"] = 4] = "Thu";
Days[Days["Fri"] = 5] = "Fri";
Days[Days["Sat"] = 6] = "Sat";
})(Days || (Days = {}));

手动赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
enum Days {Sun = 7, Mon, Tue, Wed, Thu, Fri, Sat}
console.log(Days)
/*
{
'7': 'Sun',
'8': 'Mon',
'9': 'Tue',
'10': 'Wed',
'11': 'Thu',
'12': 'Fri',
'13': 'Sat',
Sun: 7,
Mon: 8,
Tue: 9,
Wed: 10,
Thu: 11,
Fri: 12,
Sat: 13
}
*/

自动赋值规则:未指定值的key将会以前一个key的值加1作为默认值,如果前面没有key则从0开始

常数枚举

常数枚举是使用 const enum 定义的枚举类型:

1
2
3
4
5
6
7
8
const enum Directions {
Up,
Down,
Left,
Right
}

let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];

常数枚举与普通枚举的区别是,它会在编译阶段被删除,并且不能包含计算成员。

上例的编译结果是:

1
var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];

外部枚举

外部枚举(Ambient Enums)是使用 declare enum 定义的枚举类型:

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

let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];

之前提到过,declare 定义的类型只会用于编译时的检查,编译结果中会被删除。

上例的编译结果是:

1
var directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];

外部枚举与声明语句一样,常出现在声明文件中。

同时使用 declareconst 也是可以的:

1
2
3
4
5
6
7
8
declare const enum Directions {
Up,
Down,
Left,
Right
}

let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];

编译结果:

1
var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];

断言

TS中的断言可以用于将当前值变成一个其他形状的兼容工具

语法

1
as 类型 // 推荐

或者

1
<类型>值

声明文件

利用declare关键字声明的为全局模块可访问的实例

内置对象

TS提供了对内置对象的 类型声明定义。内置对象是指根据标准在全局作用域(Global)上存在的对象。这里的标准是指 ECMAScript 和其他环境(比如 DOM)的标准。

ES内置对象

提供了例如BooleanErrorDateRegExp等内置对象的声明

1
2
3
4
let b:Boolean = new Boolean(1);
let e: Error = new Error('Error occurred');
let d: Date = new Date();
let r: RegExp = /[a-z]/;

DOM和BOM内置对象

DOM 和 BOM 提供的内置对象有:

DocumentHTMLElementEventNodeList 等。

TypeScript 中会经常用到这些类型:

1
2
3
4
5
let body: HTMLElement = document.body;
let allDiv: NodeList = document.querySelectorAll('div');
document.addEventListener('click', function(e: MouseEvent) {
// Do something
});

访问修饰符

修饰在属性和方法上,用于设置该属性和方法的访问权限

  • public: 在任何地方被访问到。

  • protected: 只能被类自身和子类访问,应用于构造函数上则意味着只能被子类继承

  • private: 只能被类自身访问,应用于构造函数上则意味着该类不能被继承

参数属性

修饰符和readonly还可以使用在构造函数参数中,等同于类中定义该属性同时给该属性赋值,使代码更简洁。

1
2
3
4
5
6
7
8
9
10
11
12
class Animal {
public constructor(public name:string){}
}

//等同于

class Animal {
public name:string
public constructor(name:string){
this.name = name
}
}

readonly 关键字

在属性前添加readonly修饰意味着该属性为只读属性,如果修改该属性将会发生报错

抽象类

abstract 用于定义抽象类和其中的抽象方法。

抽象类不能被直接实例化,需要由子类继承实现 ,并且子类必须实现抽象类中的所有抽象方法。这一点几乎与JAVA对抽象类的设计思想一模一样。

1
2
3
4
5
6
7
8
9
10
11
12
abstract class Animal{
public constructor(protected name:string){}
public abstract eat():void
}
class Cat extends Animal {
eat(){
console.log(this.name + ' was eat!')
}
}

let kitty = new Cat('kitty')
kitty.eat()

类的类型

给类加上 TypeScript 的类型很简单,与接口类似:

1
2
3
4
5
6
7
8
9
10
11
12
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
sayHi(): string {
return `My name is ${this.name}`;
}
}

let a: Animal = new Animal('Jack');
console.log(a.sayHi()); // My name is Jack

流动的类型

复制类型和值

如果想要复制一个类,错误的办法是:

1
2
3
4
5
class Foo {}

const Bar = Foo;

let bar: Bar; // Error: 不能找到名称 'Bar'

正确的方式是利用import关键字进行引用,这里利用Hack方法演示下

1
2
3
4
5
6
namespace importing {
export class Foo {}
}

import Bar = importing.Foo;
let bar: Bar; // ok
捕获变量的类型

在变量类型声明中利用typeof来利用其他变量的类型,这允许你告诉编译器,一个变量的类型与其他类型相同。

1
2
3
4
5
const foo = 123;
const bar: typeof foo; // 捕获了foo的类型。因此bar当前为number类型

bar = 345; // ok
bar = "123"; // error
捕获类成员的类型

和捕获变量类型一致,需要将类注解到一个声明变量上,通过这个声明变量就可以捕获成员类型咯

1
2
3
4
5
6
7
class Foo{
foo: string;
}
declare let _foo: Foo;

const bar_1: typeof _foo.foo = 123; // Error
const bar_2: typeof _foo.foo = '123'; // Ok
捕获键的名称

keyof 操作符能让你捕获一个类型的键。例如,你可以使用它来捕获变量的键名称,在通过使用 typeof 来获取类型之后:

1
2
3
4
5
6
7
8
9
10
11
const colors = {
red: 'red',
blue: 'blue'
};

type Colors = keyof typeof colors;

let color: Colors; // color 的类型是 'red' | 'blue'
color = 'red'; // ok
color = 'blue'; // ok
color = 'anythingElse'; // Error

这允许你很容易地拥有像字符串枚举+常量这样的类型,如上例所示。

类与接口

接口(Interfaces)可以用于对「对象的形状(Shape)」进行描述。而事实上接口还有另外的用途

在TS中类与接口其实是比较容易混淆的盖点,接口是描述对象的形状,而类是描述对象的实际行为。在TS中,类和接口在一起还可以碰撞出更加灿烂的火光。

类继承接口

按照Java中的开发思想,一个类只能继承于一个父类。有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口(interfaces),用 implements 关键字来实现。这个特性大大提高了面向对象的灵活性。

在TypeScript中,类实现接口需要把接口中所有的方法都实现,否则会发生报错!

举例来说,门是一个类,防盗门是门的子类。如果防盗门有一个报警器的功能,我们可以简单的给防盗门添加一个报警方法。这时候如果有另一个类,车,也有报警器的功能,就可以考虑把报警器提取出来,作为一个接口,防盗门和车都去实现它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface Alarm {
alert(): void;
}

class Door {
}

class SecurityDoor extends Door implements Alarm {
alert() {
console.log('SecurityDoor alert');
}
}

class Car implements Alarm {
alert() {
console.log('Car alert');
}
}

一个类可以实现多个接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface Alarm {
alert(): void;
}

interface Light {
lightOn(): void;
lightOff(): void;
}

class Car implements Alarm, Light {
alert() {
console.log('Car alert');
}
lightOn() {
console.log('Car light on');
}
lightOff() {
console.log('Car light off');
}
}

上例中,Car 实现了 AlarmLight 接口,既能报警,也能开关车灯。

接口继承接口

接口与接口之间可以是继承关系:

1
2
3
4
5
6
7
8
interface Alarm {
alert(): void;
}

interface LightableAlarm extends Alarm {
lightOn(): void;
lightOff(): void;
}

这很好理解,LightableAlarm 继承了 Alarm,除了拥有 alert 方法之外,还拥有两个新方法 lightOnlightOff

接口继承类

本质上声明一个类时,还会同时声明一个包含其实例属性和实例方法的同名类型,而该类型本质上和接口一致。因此与其说是“接口继承类”,不如说是“接口继承接口”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}

interface Point3d extends Point {
z: number;
}

let point3d: Point3d = {x: 1, y: 2, z: 3}; //还记得指定“形状”的赋值方法吗

泛型

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

例如:

1
2
3
4
5
6
7
8
9
function createArray<T>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}

createArray<string>(3, 'x'); // ['x', 'x', 'x']

上例中,我们在函数名后添加了 <T>,其中 T 用来指代任意输入的类型,在后面的输入 value: T 和输出 Array<T> 中即可使用了。

接着在调用的时候,可以指定它具体的类型为 string。当然,也可以不手动指定,而让类型推论自动推算出来。

多个类型参数

定义泛型的时候,可以一次定义多个类型的参数:

1
2
3
4
5
function swap<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]];
}

swap([7, 'seven']); // ['seven', 7]

泛型约束

可以通过extend 关键字对泛型的形状进行约束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 例如
function logLength<T>(arg: T):T{
console.log(arg.length) //error: 类型“T”上不存在属性“length”。
return arg
}

// 可以改为

interface IShape{
length:number
}
function logLength<T extends IShape>(arg: T):T{
console.log(arg.length) //error: 类型“T”上不存在属性“length”。
return arg
}
logLength({length: 8}) // 8

// 非规定入参将发生编译错误
logLength(88) //error: 类型“88”的参数不能赋给类型“IShape”的参数

多个泛型类型之间也可以进行约束

1
2
3
4
5
6
7
8
9
10
11
function copyFields<T extends U , U>(target:T,source: U):T{
for(let key in source){
target[key] = (source as T)[key]
}
return target
}

let x = {a: 1,b:2,c:3}
let y = {b:8}

copyFields(x,y) // { a: 1, b: 8, c: 3 }

代码中,既然T收到了U的约束,那么T必须是U的超集。其本质上与受到接口的约束效果一致

泛型接口

之前接触过,可以使用接口的方式来定义一个函数学要符合的形状

1
2
3
4
5
6
7
interface SearchFunc {
(source: string, subString: string): boolean;
}

let mySearch:SearchFunc = function(source:string, subString:string):boolean{
return source.search(subString) !== -1
}

而现在,可以使用泛型来定义接口

1
2
3
4
5
6
7
8
9
10
11
12
13
interface ICreateArrayFun{
<T>(length:number,value: T):Array<T>
}

let createArray:ICreateArrayFun = function<T>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}

console.log(createArray(3,true)) // [true,true,true]

当然,泛型类型的定义也可以提到接口外面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface ICreateArrayFun<T>{ //将泛型提到此处
(length:number,value: T):Array<T>
}

// 需要在定义时就进行类型指定
let createArray:ICreateArrayFun<number> = function<T>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
createArray(3,123) // [123,123,123]
createArray(3,true) // error: 类型“true”的参数不能赋给类型“number”的参数。

泛型类

与泛型函数类似,泛型也可以用于类的类型定义中

泛型参数的默认类型

在 TypeScript 2.3 以后,我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用。(很少用到)

1
2
3
4
5
6
7
function createArray<T = string>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}

泛型配合Axios使用

通常将把服务端返回的响应体通过interface来进行声明 interface.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 请求接口数据
export interface ResponseData<T = any> {
/**
* 状态码
* @type { number }
*/
code: number;

/**
* 数据
* @type { T }
*/
result: T;

/**
* 消息
* @type { string }
*/
message: string;
}

将API单独抽离成模块时user.ts

1
2
3
4
5
6
7
import axios from './axios'; // 此处的axios应当是已经处理后(添加通用配置,拦截器等)的axios实例对象
import { ResponseData } from './interface.ts';
import { IRequestParams } from '@/types/global';

export function fetchUserAccount<T>(params?: IRequestParams) {
axios.post<ResponseData<T>>('/user/account', params);
}

使用该接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { fetchUserAccount } from './user';

interface IUser {
name: string;
age: number;
}

async function test() {
const user = await fetchUserAccount<IUser>();
// user 被推断出为
// {
// code: number,
// result: { name: string, age: number },
// message: string
// }
}

声明合并

和函数重载一样,接口和类也可以重复声明,TS编译阶段会将他们合并到一块

1
2
3
4
5
6
7
8
9
10
11
12
13
interface Person{
name:string
}
interface Person{
age:number
}

// 相当于

interface Person{
name: string;
age:number;
}

多个要合并的接口或者类之间可以有重复的属性,但是属性的类型必须是一致的。否则会发生报错

1
2
3
4
5
6
7
8
interface Person{
name:string
age:string
}
interface Person{
name:string
age:number // error: 后续属性声明必须属于同一类型。属性“age”的类型必须为“string”,但此处却为类型“number”
}

而方法合并和函数重载规则一致

1
2
3
4
5
6
7
8
interface Alarm {
price: number;
alert(s: string): string;
}
interface Alarm {
weight: number;
alert(s: string, n: number): string;
}

相当于:

1
2
3
4
5
6
interface Alarm {
price: number;
weight: number;
alert(s: string): string;
alert(s: string, n: number): string;
}

TypeScript项目

编译上下文

所谓编译上下文指的是TS编译的范围,通过配置文件可以对当前编译上下文中的TS文件提供编译选项,而这个配置文件事实上指的就是tsconfig.json

tsconfig.json

开始使用 tsconfig.json 是一件比较容易的事,你仅仅需要写下:

1
{}

例如,在项目的根目录下创建一个空 JSON 文件。通过这种方式,TypeScript 将 会把此目录和子目录下的所有 .ts 文件作为编译上下文的一部分,它还会包含一部分默认的编译选项。

编译选项

可以通过compilerOptions来定制当前编译上下文的编译选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
{
"compilerOptions": {

/* 基本选项 */
"target": "es5", // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
"module": "commonjs", // 指定使用模块: 'None', 'CommonJS', 'AMD', 'System', 'UMD', 'ES6', 'ES2015', 'ES2020' or 'ESNext'.
"lib": [], // 指定要包含在编译中的库文件
"allowJs": true, // 允许编译 javascript 文件
"checkJs": true, // 报告 javascript 文件中的错误
"jsx": "preserve", // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
"declaration": true, // 生成相应的 '.d.ts' 文件
"sourceMap": true, // 生成相应的 '.map' 文件
"outFile": "./", // 将输出文件合并为一个文件
"outDir": "./", // 指定输出目录
"rootDir": "./", // 用来控制输出目录结构 --outDir.
"removeComments": true, // 删除编译后的所有的注释
"noEmit": true, // 不生成输出文件
"importHelpers": true, // 从 tslib 导入辅助工具函数
"isolatedModules": true, // 将每个文件作为单独的模块 (与 'ts.transpileModule' 类似).

/* 严格的类型检查选项 */
"strict": true, // 启用所有严格类型检查选项
"noImplicitAny": true, // 在表达式和声明上有隐含的 any类型时报错
"strictNullChecks": true, // 启用严格的 null 检查
"noImplicitThis": true, // 当 this 表达式值为 any 类型的时候,生成一个错误
"alwaysStrict": true, // 以严格模式检查每个模块,并在每个文件里加入 'use strict'

/* 额外的检查 */
"noUnusedLocals": true, // 有未使用的变量时,抛出错误
"noUnusedParameters": true, // 有未使用的参数时,抛出错误
"noImplicitReturns": true, // 并不是所有函数里的代码都有返回值时,抛出错误
"noFallthroughCasesInSwitch": true, // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)

/* 模块解析选项 */
"moduleResolution": "node", // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
"baseUrl": "./", // 用于解析非相对模块名称的基目录
"paths": {}, // 模块名到基于 baseUrl 的路径映射的列表
"rootDirs": [], // 根文件夹列表,其组合内容表示项目运行时的结构内容
"typeRoots": [], // 包含类型声明的文件列表
"types": [], // 需要包含的类型声明文件名列表
"allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入。

/* Source Map Options */
"sourceRoot": "./", // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
"mapRoot": "./", // 指定调试器应该找到映射文件而不是生成文件的位置
"inlineSourceMap": true, // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
"inlineSources": true, // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性

/* 其他选项 */
"experimentalDecorators": true, // 启用装饰器
"emitDecoratorMetadata": true // 为装饰器提供元数据的支持
}
}

当配置完毕时,VScode将会提供对TS的即时编译,但是如果想从命令行运行TS编译器,可以通过以下方式:

  • 直接运行 tsc, 它会从当前目录或者父级目录寻找tsconfig.json
  • 运行tsc -p ./path-to-project-directory ,它会从指定路径进行编译

你也可以显式指定需要编译的文件:

1
2
3
4
5
{
"files": [
"./some/file.ts"
]
}

你还可以使用 includeexclude 选项来指定需要包含的文件和排除的文件:

1
2
3
4
5
6
7
8
9
{
"include": [
"./folder"
],
"exclude": [
"./folder/**/*.spec.ts",
"./folder/someSubFolder"
]
}

注意

使用 globs**/* (一个示例用法:some/folder/**/*)意味着匹配所有的文件夹和所有文件(扩展名为 .ts/.tsx,当开启了 allowJs: true 选项时,扩展名可以是 .js/.jsx)。

声明空间

  • 声明空间指的是TS在编译阶段会将类型和变量添加到编译声明空间中

  • 声明空间可以分为变量声明空间和类型声明空间

    • 类型声明空间

      通过interface定义的接口都会被ts编译器添加到类型声明空间

      1
      2
      3
      4
      export interface Person{ //将会提升到类型声明空间
      name:string
      age:number
      }
    • 变量声明空间

      类、变量、函数都会被ts编译器添加到变量声明空间中

      1
      2
      3
      4
      5
      6
      7
      class Person{ //提升到变量声明空间 && 但是在声明类时如果类型声明空间中没有该类型,则会自动创建一个同名类型,其内容为类型中已声明的属性。
      //如果类型声明空间中已存在该类型,则利用声明合并规则进行合并
      constructor(public name:string){}
      say(){
      console.log(this.name)
      }
      }
  • 全局声明空间的范围是当前编译上下文

  • 默认声明的将会成为全局的声明空间,加入export后成为局部模块声明空间

    `

文件模块

  • Typescript 模块解析就是指导 ts 编译器查找导入(import)内容的流程
  • TS模块解析供有两种策略:
    • Classic: 以前是TypeScript 默认的解析策略,目前仅用作向后兼容
    • Node: 与 NodeJS 模块机制一致的解析策略
Node模块解析策略

当导入路径不是相对路径时,模块解析将会模仿 Node 模块解析策略,下面我将给出一个简单例子:

  • 当你使用

    1
    import * as foo from 'foo'

    ,将会按如下顺序查找模块:ts

    • ./node_modules/foo
  • ../node_modules/foo

    • ../../node_modules/foo
    • 直到系统的根目录
  • 当你使用

    1
    import * as foo from 'something/foo'

    ,将会按照如下顺序查找内容

    • ./node_modules/something/foo
  • ../node_modules/something/foo

    • ../../node_modules/something/foo
    • 直到系统的根目录
  • 当你使用

    1
    import xx from './foo'

    将会按照如下顺序查找内容

image-20210106105126793

优先查找.ts后缀 如无则查找 .d.ts同名文件

Classic解析策略
  • 当你使用文件路径

    1
    import xx from './foo'

    将会按照如下顺序查找内容

    image-20210106110526489

  • 当你使用

    1
    import xx from 'foo'

    将会按照如下顺序查找内容

    image-20210106111747235

重写文件查找策略

在TypeScript中,可以通过声明全局模块的方法,来绕过文件查找策略

1
2
3
4
5
6
// global.d.ts
// 在当前编译上下文作用域内 声明全局模块
declare module 'foo' {
// some variable declarations
export var bar: number;
}

接着

1
2
3
4
// test.ts
import * as foo from 'foo';
// TypeScript 将假设(在没有做其他查找的情况下)
// foo 是 { bar: number }

动态导入表达式

动态导入表达式是 ECMAScript 的一个新功能,它允许你在程序的任意位置异步加载一个模块,TC39 JavaScript 委员会有一个提案,目前处于第四阶段,它被称为 import() proposal for JavaScript

此外,webpack bundler 有一个 Code Splitting 功能,它能允许你将代码拆分为许多块,这些块在将来可被异步下载。因此,你可以在程序中首先提供一个最小的程序启动包,并在将来异步加载其他模块。

这很自然就会让人想到(如果我们工作在 webpack dev 的工作流程中)TypeScript 2.4 dynamic import expressions 将会把你最终生成的 JavaScript 代码自动分割成很多块。但是这似乎并不容易实现,因为它依赖于我们正在使用的 tsconfig.json 配置文件。

webpack 实现代码分割的方式有两种:使用 import() (首选,ECMAScript 的提案)和 require.ensure() (最后考虑,webpack 具体实现)。因此,我们期望 TypeScript 的输出是保留 import() 语句,而不是将其转化为其他任何代码。

让我们来看一个例子,在这个例子中,我们演示了如何配置 webpack 和 TypeScript 2.4 +。

在下面的代码中,我希望懒加载 moment 库,同时我也希望使用代码分割的功能,这意味 moment 会被分割到一个单独的 JavaScript 文件,当它被使用时,会被异步加载。

1
2
3
4
5
6
7
8
9
10
11
import(/* webpackChunkName: "momentjs" */ 'moment')
.then(moment => {
// 懒加载的模块拥有所有的类型,并且能够按期工作
// 类型检查会工作,代码引用也会工作 :100:
const time = moment().format();
console.log('TypeScript >= 2.4.0 Dynamic Import Expression:');
console.log(time);
})
.catch(err => {
console.log('Failed to load moment', err);
});

这是 tsconfig.json 的配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"lib": [
"dom",
"es5",
"scripthost",
"es2015.promise"
],
"jsx": "react",
"declaration": false,
"sourceMap": true,
"outDir": "./dist/js",
"strict": true,
"moduleResolution": "node",
"typeRoots": [
"./node_modules/@types"
],
"types": [
"node",
"react",
"react-dom"
]
}
}

重要的提示

从JavaScript迁移

一般来说,将 JavaScript 代码迁移至 TypeScript 包括以下步骤:

  • 添加一个 tsconfig.json 文件;
  • 把文件扩展名从 .js 改成 .ts,开始使用 any 来减少错误;
  • 开始在 TypeScript 中写代码,尽可能的减少 any 的使用;
  • 回到旧代码,开始添加类型注解,并修复已识别的错误;
  • 为第三方 JavaScript 代码定义环境声明(*.d.ts)。

参考文章

0%