typescript笔记

Typescript的类型

截至目前版本(TypeScript 2.6), Typescript类型有以下几种:

类型描述
boolean布尔值
number数字
string字符串
array数组
Tuple元组类型允许表示一个已知元素数量和类型的数组
enum枚举,给数字集合更好的命名,默认从0开始
any任意类型
void可以看成any的对立面,这个常在没有返回的函数那里看到,它表示没有任何类型
Null 和 UndefinedTypeScript里,undefined和null两者各自有自己的类型分别叫做undefined和null
Never表示的是那些永不存在的值的类型,never类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型

目前来说文档算是对类型讲的比较清楚

声明变量

let和const

区别在于let声明以后可以更改,而const不能。根据最小权限原则,除非你计划更改,那么就用const。

声明函数

简单说,就是对函数的输入和输出进行一个约束。

1
2
3
function add(num1: number, num2: number): number {
return num1 + num2;
}

接口

接口我个人认为是typescript用的最多的东西之一,但是可能也是常规使用ts最容易遇到问题的地方之一,正常来讲我感觉说它最难应该也没啥问题?不过接口是在类型系统的基础上建立的,它的作用是对数据的结构体进行校验,而ts里面常规的类型系统,一般的类型校验都是对值或者函数的校验。

接口这里个人感觉是一个熟能生巧的东西。

简单的例子

1
2
3
4
5
6
interface Person{
name: 'zhangsan'
}
function getName(person: Person){
console.log(`my name is ${persion.name}`)
}

可选属性 && 只读属性

上面的实际上只是最简单的接口的例子,实际上应用的时候不会这样简单。
常见的场景就是有些属性有些对象有有些却没有有,这时候如果全部如上面那么写就会抛错。可选属性是写ts时候面临的第一个困惑我认为。
套用一下官方的例子:

1
2
3
4
interface SquareConfig {
color?: string;
width?: number;
}

这个时候,color和width就都不是必须的了。可以根据情况来选择传不传。
另外就是只读属性

1
2
3
4
5
6
interface Point {
readonly x: number;
readonly y: number;
}
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!

额外的属性检查

接口默认是一种严格的校验,当一个数据结构符合一个接口时候(而不做特殊处理),他的意思是严格的符合,不可以带『私货』。
所以说如下接口使用时候,可以给createSquare传{color:”xxxx”},可以传{width: 10},也可以两个都传,但是不可以再加一个height;

1
2
3
4
5
6
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
}

同理的,如下代码也是会抛错的

1
2
var a: SquareConfig = {color: "red", width: 10};
a.height = 10; // error

此时可以加上引索签名来解决这个问题

1
2
3
4
5
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}

此时可以给它传递任何类型的属性了,就像常规的的js对象一样。

函数接口(可调用签名)

上面的接口相关都是用来描述一个对象,一个数据结构是怎样的,但是没有说,一个函数该如何用接口描述;

1
2
3
4
5
6
7
8
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(src, sub) {
let result = src.search(sub);
return result > -1;
}

函数接口这里定义还是比较简单,核心还是 (source: string, subString: string): boolean;, 冒号前面那块是参数,冒号后面则是返回值的类型。
这里有几个要点可以关注一下

  • 函数在声明时候设好了接口以后, 参数名不需要与接口里定义的名字相匹配,它是挨个匹配类型,而不是根据参数名
  • 定义了接口以后,函数定义时候, src, sub 参数这里可以不设置类型,而是通过ts自动推导出来。

可索引的类型(索引签名)

除了描述对象、函数,接口也可以描述可引索的类型。这是一个名为索引签名的东西实现的。

1
2
3
4
5
6
7
8
interface StringArray {
[index: number]: string;
}

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];

如上,这种签名如果只是为了处理数组,显然这是这没啥用的,因为不如 let myArray: string[] = [“Bob”, “Fred”];好用也不如其整洁。我个人认为索引签名其实还是如上面提到的额外的属性检查这块的作用更大一些。

Class约束

类的约束有两种情况:

  • 静态部分
  • 实例部分

静态部分

1
2
3
4
5
6
7
8
interface ClockInterface {
currentTime: Date;
}

class Clock implements ClockInterface {
currentTime: Date;
constructor(h: number, m: number) { }
}

实例部分

实例部分和静态部分接口是不通用的,实例这块最大的问题是构造器签名这块。
那么什么是一个『构造器签名』呢?如下代码,这就是一个很常规的构造器签名。

1
2
3
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}

说它和静态类型不能公用接口的意思是,构造器签名在类中,是不可implementable的。如下代码,就会抛出错误:

1
2
3
4
5
6
7
8
interface ClockConstructor {
new (hour: number, minute: number);
}

class Clock implements ClockConstructor {
currentTime: Date;
constructor(h: number, m: number) { }
}

所以,正常情况需要两个签名才能完整定义实例部分,整个环节简单的描述就是定义静态接口和构造器接口,然后新增一个类工厂用其传入构造器并返回实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
tick();
}
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
return new ctor(hour, minute);
}
class DigitalClock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("beep beep");
}
}
let digital = createClock(DigitalClock, 12, 17);

这里官方的例子还是看得比较明晰,静态类型、构造器签名、还有工厂类,各司其职。不过可能还是有些人不喜欢这种写法,毕竟添加一个工厂类,也是麻烦的一种。

此时,如果是在写npm包可以使用 d.ts 文件声明来处理这种情况。

1
2
3
4
declare var DigitalClock {
new(h: number, m: number): DigitalClock;
tick: ();
}

不过思考之后觉得其实不使用构造器签名,其实问题也不大,因为构造器返回的还是构造好的实例,这个实例会依从静态类型那块的签名,传入的参数这里的类型校验是可以直接定义好的。

继承接口

1
2
3
4
5
6
7
8
9
10
interface Shape {
color: string;
}
interface PenStroke {
penWidth: number;
}
interface Square extends Shape, PenStroke {
sideLength: number;
}
let square = <Square>{};

函数

个人认为Typescript里面函数算是比较简单的一块。这里面需要注意的有:

  • 可选参数 & 默认值
  • 动态参数个数(剩余参数)
  • 重载

可选参数 & 默认值

1
2
function buildName(firstName: string, lastName?: string) {}
function buildName(firstName: string, lastName = "Smith") {}

此时第一行的参数lastName是可选的,而第二行lastName设置了初始值”Smith”,同时它也是可选值。

动态参数个数(剩余参数)

1
2
3
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}

重载

个人认为函数的重载实在很常见,尤其是非常流星的 lodash和 jQuery里面的api里面就重载得非常厉害。
举个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
function LngLat2Address(x: string): string;
function LngLat2Address(x: {lng: number, lat: number}): string;
function LngLat2Address(x): string {
if (typeof x == "object") {
return ""
} else{
return ""
}
}
let address1 = LngLat2Address("113,23");
let address2 = LngLat2Address({lng: 111, lat: 23});

如上面这段代码,它同时接受字符串和对象形式的经纬度,并转换为地址(过程代码略),返回类型固定为string(实际上也可以返回不同的类型)。

泛型

泛型可以说是玩好TypeScript一个必须上的台阶。初步使用Ts时候不熟泛型也没啥问题,但是到了后面泛型其实也非常重要,因为它可以大大减少你重复自己的代码,减少写出重复的代码导致不好维护的问题。
简单的例子,比如常见的开发时候很多时候会请求一个列表展示,这里最简要的举个例子:

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
interface User{
name: string;
age: number
}
function getUserList(cb: (Users: User[]) => void 0) {
$.ajax({
url: 'abc.com/api/users',
method: 'get',
success: ({data}: any)=> {
cb(data)
}
})
}
interface Role{
name: string;
level: number
}
function getRoleList(cb: (Roles: Role[]) => void 0) {
$.ajax({
url: 'abc.com/api/roles',
method: 'get',
success: ({data}: any)=> {
cb(data)
}
})
}

很常规的需求,请求用户和角色列表。这里除了User和Role内部数据不一样其他都是一样,那么有没有办法可以避免写出重复度如此高的代码呢?很显然,这是可以的,这里就是泛型的意义之所在(提高复用性,不要重复自己)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getList<T>(url: string, cb: (T[]) => void 0) {
$.ajax({
url,
method: 'get',
success: ({data}: any)=> {
cb(data)
}
})
}
// 此时就可以直接使用 getUserList和 getRolesList
getList<User>('abc.com/api/users', (User[]) => {
});
getList<Role>('abc.com/api/roles', (Role[]) => {
});

高级类型

文档上高级类型看起来有很多。。。不过我这边用到过的也不多。。。这里就按自己相对熟悉一些的记录一下。

联合类型

常见是一个函数同时可以接受 number和 string形式的参数,那么此时除了给它设为any之外,另一个方案就是设置为联合类型:

1
2
3
function toString(s: number | string): string {
return '' + s;
}

交叉类型

和联合类型很类似,不过交叉类型应该是对象形式的数据的联合。比如{a:1}, {b:2},的交叉,成为{a: 1, b: 2};
extend函数大家应该都用过,具体里面实现不表,它的定义应该是这样的,这就是一个非常典型的交叉类型。

1
function extend<T, U>(first: T, second: U): T & U {}

类型保护与区分类型

目前这个还没遇到过,不过觉得很有意思就记录一下,以便后面慢慢体会。
首先, 如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Bird {
fly();
layEggs();
}
interface Fish {
swim();
layEggs();
}
function getSmallPet(): Fish | Bird {
// ...
}
let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim(); // errors

所以上面这段代码就会在ts编译阶段就抛错,但是从业务逻辑环节来讲,这显然是很不合理的。但是换ts编译器的角度来说,这个到底抛错不抛错,也确实是个问题。——所以这里有了类型保护这个东西。
为了让上面代码运行,加入一个类型断言

1
2
3
4
5
6
let pet = getSmallPet();
if ((<Fish>pet).swim) {
(<Fish>pet).swim();
} else {
(<Bird>pet).fly();
}

如果这里反复判断的比较多,那么就需要用到自定义类型保护了,这样方便重用代码和保持代码清洁(毕竟这种格式我个人觉得也是很难看)。

1
2
3
4
5
6
7
8
9
10
let pet = getSmallPet();

function isFish(pet: Fish | Bird): pet is Fish {
return (<Fish>pet).swim !== undefined;
}
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}

这里有格式上的注意事项, pet is Fish是类型谓词,这里pet必须和签名里面参数的名称保持一致。

Index types && Mapped types

这两个大概是我个人认为比较具有黑魔法的地方,可能是在javascript里面数组和对象承担了太多责任,所以对它们的约束太灵活多变导致的。

在这两个类型之间,个人私底下还是觉得可能是 K extends keyof TK in keyof T 之间的区别

keyof

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

type P1 = keyof Person; // "name" | "age" | "location"

可以看到keyof返回了一个联合类型 这个联合类型的值是Person的key的组合

Index types && extends keyof

extends keyof而言,可能官方例子的getProperty是一个非常好的理解途径

1
2
3
4
function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
return o[name]; // o[name] is of type T[K]
}
let name: string = getProperty(person, 'name');

这里K extends keyof T断言了T内部的一个键名,只有T内部有的属性,才能检查通过

Mapped types && in keyof

in keyof呢,ts的es5标准库里面的Partial可能是一个非常好的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Partial<T> = {
[P in keyof T]?: T[P];
};

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

type PersonPartial = Partial<Person>
// 相当于:
// interface Person {
// name?: string;
// age?: number;
// }

这里索引签名in keyof的配合就将Person整体遍历了一次, 返回了一个继承它的、新的属性名和选的接口。