# 再战 TypeScript
本文是我的一个个人复习 typescript 的记录,因此有一些基础会省略,小白建议先了解一下 typescript 的基础再来看这篇文章~
# 基础类型篇
# Object 对象类型
# Readonly
Readonly
个人觉得,可以类比const
,即基本类型只读,对于引用类型,可以改变其属性的值而对其地址只读。除此之外,TypeScript 在检查两个类型是否兼容的时候并不会考虑readonly
,readonly
的属性可以通过别名修改
interface Person {
name: string;
age: number;
}
interface ReadonlyPerson {
readonly name: string;
readonly age: number;
}
let writablePerson: Person = {
name: "Person McPersonface",
age: 42,
};
// works
let readonlyPerson: ReadonlyPerson = writablePerson;
console.log(readonlyPerson.age); // prints '42'
writablePerson.age++;
console.log(readonlyPerson.age); // prints '43'
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 索引签名
interface NumberDictionary {
[index: string]: number;
length: number; // ok
name: string;
// Property 'name' of type 'string' is not assignable to 'string' index type 'number'.
}
2
3
4
5
6
7
# * 接口继承和交叉类型
这两种对象拓展的方式最大的不同在于冲突如何处理!
interface Colorful {
color: string;
}
interface ColorfulSub extends Colorful {
color: number;
}
// Interface 'ColorfulSub' incorrectly extends interface 'Colorful'.
// Types of property 'color' are incompatible.
// Type 'number' is not assignable to type 'string'.
interface Colorful {
color: string;
}
type ColorfulSub = Colorful & {
color: number;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
接口继承如果重写属性类型会报错,但交叉类型不会,最终color
类型为 never (string 和 number 的交集)
# ReadonlyArray 和 元组
- 语法:
ReadonlyArray<Type>
或者readonly T[]
需要注意的是,Array
和ReadonlyArray
并不能双向赋值
- 元组(Tuple Types)是另一种数组类型,明确知道数组长度而且每个位置元素都被确定下来了。比如
type Tuple = [string, number]
当元组遇上剩余元素语法的时候,来看一下下面的例子:
type StringNumberBooleans = [string, number, ...boolean[]];
type StringBooleansNumber = [string, ...boolean[], number];
type BooleansStringNumber = [...boolean[], string, number];
function readButtonInput(...args: [string, number, ...boolean[]]) {
const [name, version, ...input] = args;
// ...
}
2
3
4
5
6
7
8
readonly
元组类型
type pair = readonly [string, number];
// 等同于 ['1',2] as const
2
# Function 函数类型
# 函数类型表达式 Function Type Expression
type foo = (a: string) => void;
# 调用签名 Call Signatures
JavaScript 的函数除了被调用,自己也是可以有属性值的。如果想要描述一个带属性的函数,我们就可以在对象类型中写一个调用签名。(这里很重要,这是一个对象类型,因此不用=>
)
type DescribableFunc = {
des: string;
(num: number): number;
};
function doSomething(fn: DescribableFunc) {
console.log(fn.description + " returned " + fn(6));
}
2
3
4
5
6
7
# 构造签名 Construct Signatures
JavaScript 函数可以使用new
来调用,此时函数为构造函数。那么我们可以使用构造签名来描述它:
type SomeConstructor = {
new (s: string): SomeObject;
};
2
3
# 函数泛型、约束
有时候我们想要关联某些值,如下面的例子,要求返回参数中length
更长的那一个:
function longest<T extends { length: number }>(a: T, b: T) {
return a.length > b.length ? a : b;
}
2
3
注意看一下下面一个使用泛型约束常出现的错误:
function minimumLength<Type extends { length: number }>(
obj: Type,
minimum: number
): Type {
if (obj.length >= minimum) {
return obj;
} else {
return { length: minimum };
// Type '{ length: number; }' is not assignable to type 'Type'.
// '{ length: number; }' is assignable to the constraint of type 'Type', but 'Type' could be instantiated with a different subtype of constraint '{ length: number; }'.
}
}
2
3
4
5
6
7
8
9
10
11
12
你可能会不知道为什么会报错,返回的对象也有length
属性呀。但是注意,Type 不只有length
属性,我们很容易可以举一个反例:
// 'arr' gets value { length: 6 }
const arr = minimumLength([1, 2, 3], 6);
// and crashes here because arrays have
// a 'slice' method, but not the returned object!
console.log(arr.slice(0));
2
3
4
5
# 一个良好的泛型函数
- 类型参数下移
function firstElement1<Type>(arr: Type[]) {
return arr[0];
}
function firstElement2<Type extends any[]>(arr: Type) {
// arr[0]有可能是对象中的key为0的属性值,无法推断
return arr[0];
}
// a: number (good)
const a = firstElement1([1, 2, 3]);
// b: any (bad)
const b = firstElement2([1, 2, 3]);
2
3
4
5
6
7
8
9
10
11
12
13
我们来看上面两个函数,它们长的非常像,但第一个函数更好。因为第一个函数 typescript 可以推断出来返回类型是number
,而第二个推断的是any
。
这里的下移指的是如果超类中某个函数只与一个或少数几个子类有关,最好将其从超类中拿走,放到具体的子类中去。即父类只需要保留最少的公共部分即可。
- 使用更少的类型参数
这是另一对长得很像的函数:
function filter1<Type>(arr: Type[], func: (arg: Type) => boolean): Type[] {
return arr.filter(func);
}
function filter2<Type, Func extends (arg: Type) => boolean>(
arr: Type[],
func: Func
): Type[] {
return arr.filter(func);
}
2
3
4
5
6
7
8
9
10
我们并不需要去毫无理由地去指定多一个类型参数Func
,因为Func
只与 Type 关联。
- 类型参数应该出现两次
有时候我们可能会忘记一个函数其实并不需要泛型。但我们只需要记住,泛型是用来关联多个值的类型,如果它只在函数签名中出现了一次,那它就没有产生任何关联。
# 函数重载
举个例子,我想要写一个返回Date
类型的函数,它可以只接收一个时间戳或者一个年、月、日格式的参数:
function makeDate(timestamp: number): Date;
function makeDate(y: number, m: number, d: number): Date;
function makeDate(yOrTimestamp: number, m?: number, d?: number): Date {
if (m !== undefined && d !== undefined) {
return new Date(yOrTimestamp, m, d);
} else {
return new Date(yOrTimestamp);
}
}
const d1 = makeDate(12345678);
const d2 = makeDate(5, 5, 5);
const d3 = makeDate(1, 3);
// error : No overload expects 2 arguments, but overloads do exist that expect either 1 or 3 arguments.
2
3
4
5
6
7
8
9
10
11
12
13
这里我们写了两个函数重载,被称为重载签名,最后是一个实现签名。但这个签名不能直接被调用。尽管我们在函数声明中,在一个必须参数后,声明了两个可选参数,它依然不能被传入两个参数进行调用,即可选符失效
再举个例子:
function fn(x: string): void;
function fn() {
// ...
}
// Expected to be able to call with zero arguments
fn();
Expected 1 arguments, but got 0.
2
3
4
5
6
7
实现签名对外界来说是不可见的,因此一定要多个重载签名在实现签名之上。
# 一个良好的函数重载
需要注意的是,TypeScript 只能一次使用一个函数重载来处理函数调用:
function len(s: string): number;
function len(arr: any[]): number;
function len(x: any) {
return x.length;
}
len(""); // OK
len([0]); // OK
len(Math.random() > 0.5 ? "hello" : [0]);
No overload matches this call.
Overload 1 of 2, '(s: string): number', gave the following error.
Argument of type 'number[] | "hello"' is not assignable to parameter of type 'string'.
Type 'number[]' is not assignable to type 'string'.
Overload 2 of 2, '(arr: any[]): number', gave the following error.
Argument of type 'number[] | "hello"' is not assignable to parameter of type 'any[]'.
Type 'string' is not assignable to type 'any[]'.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
尽可能使用联合类型去代替函数重载
# 函数的可赋值性
当函数的返回类型是void
的时候,下面这些情况是不会强制函数一定不能返回内容的:
type voidFunc = () => void;
const f1: voidFunc = () => {
return true;
};
const f2: voidFunc = () => true;
const f3: voidFunc = function () {
return true;
};
const src = [1, 2, 3];
const dst = [0];
src.forEach((el) => dst.push(el));
// push返回一个数字,但forEach方法期待一个返回void的函数,但这段代码不会报错
2
3
4
5
6
7
8
9
10
11
12
13
14
15
但如果是函数字面量返回void
,则一定不能返回内容
# Enum 枚举类型
可以理解为一个双向的字典。
# 数字枚举
enum Status {
MASSAGE, // 对应 0
TEACHER, // 对应 1
BAG, // 对应 2
}
2
3
4
5
enum Statu {
loading, // 0
fulfilled = 100, // 100
rejected, // 101
}
2
3
4
5
# 字符串枚举以及异构枚举
字符串枚举最大的特点就是,一定要有初始值 & 字符串枚举无反向映射
异构枚举顾名思义,就是不同类型,即数字、字符串混合在一起的枚举。
不建议运用异构枚举,因为这对你想要做的目标并不清晰。
# 计算&常量成员枚举
enum FileAccess {
// constant members
None, // 0
Read = 1 << 1, // 2
Write = 1 << 2, // 4
ReadWrite = Read | Write, // 6
// computed member
G = "123".length, // 3
}
2
3
4
5
6
7
8
9
# 联合枚举和枚举成员类型
当枚举成员都是字面量类型的时候,枚举就成了类型!
enum ShapeKind {
Circle,
Square,
}
interface Circle {
kind: ShapeKind.Circle;
radius: number;
}
interface Square {
kind: ShapeKind.Square;
sideLength: number;
}
let c: Circle = {
kind: ShapeKind.Square,
// Type 'ShapeKind.Square' is not assignable to type 'ShapeKind.Circle'.
radius: 100,
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# const 枚举
有的时候为了减少访问枚举值的代价,就可以使用 const 枚举。const 枚举成员不能是计算类型。
const enum Direction {
Up,
Down,
Left,
Right,
}
2
3
4
5
6
Enum 在编译之后是一个丰富的对象,但是 const Enum 编译之后是没有东西的,看下面这个例子:
const enum Status {
success = 200,
error = 500,
}
const res = Status.successs;
// 编译后
("use strict");
const res = 200; /* success */
2
3
4
5
6
7
8
# 环境枚举
enum a {
A = 1,
B,
C = 2,
}
/* 1: "A"
2: "C"
A: 1
B: 2
C: 2 */
2
3
4
5
6
7
8
9
10
# Enum vs Object
const enum EDirection {
Up,
Down,
Left,
Right,
}
const ODirection = {
Up: 0,
Down: 1,
Left: 2,
Right: 3,
} as const;
EDirection.Up;
// ^? 0
ODirection.Up;
// ^? 0
// Using the enum as a parameter
function walk(dir: EDirection) { }
// It requires an extra line to pull out the values
type Direction = typeof ODirection[keyof typeof ODirection];
function run(dir: Direction) { }
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
# 字面量推断
当你初始化变量为一个对象的时候,TypeScript 会假设该对象属性的值未来会被修改。因此 typescript 只会推断类型。看下面的代码:
declare function handleRequest(url: string, method: "GET" | "POST"): void;
const req = { url: "https://example.com", method: "GET" };
handleRequest(req.url, req.method);
// Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.
2
3
4
5
6
解决方式也很容易想到,使用类型断言 as
或者用const,使用as const
可以确保所有属性都被赋予了字面量类型,而不是简单的字符串类型:
const req = { url: "https://example.com", method: "GET" } as const;
# 关于接口与类的约束关系
类中的方法是定义的非常细节,但接口中的方法只定义类型
# 接口 interface 和 type 的区别
interface
只能定义对象类型、而type
可定义任何类型interface
可以声明同样两个接口合并,而type
不行,type
是唯一的type
有类型推导
# 类与接口的约束
implements 关键字,表示约束的意思,即 Teacher 类必须满足 Person 接口中定义的属性
class Teacher implements Person {}
# 类之间的继承
class EnglishTeacher extends Teacher {}
super
关键字表示继承父类的各种方法
# 类的访问类型----3 个关键字
public
允许在类的内部和外部被调用. 默认类型
private
只允许再类的内部被调用,外部不允许调用
protected
允许在类内及继承的子类中使用
# 泛型的使用
# 基础用法
function identity<T>(arg: T): T {
return arg;
}
let output = identity < string > "myString"; // type of output will be 'string'
// 类型推断
let output = identity("myString"); // type of output will be 'string'
2
3
4
5
6
数组中泛型的使用
如果传递过来的值要求是数字,如何用泛型进行定义那两种方法:
- 第一种是直接使用[]。
number[]
- 第二种是使用 Array<泛型>。形式不一样,其他的都一样。
Array<number>
Promise<void>
多个泛型的定义
function join<T, P>(first: T, second: P) {
return `${first}${second}`;
}
join < number, string > (1, "2");
join(1, "2"); // 类型推断
2
3
4
5
类中的泛型
看下面的案例:
interface Person {
name: string
}
// 这里不能用implements 因为是接口间继承
class Persons<T extends Person>{
constructor(private persons: T[]) {
this.persons = persons;
}
getPersonName(index: number):string {
return this.persons[index].name
}
}
let arr = new Persons([
{name:'xiaohong'},
{name:'xiaolan'}
])
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 泛型约束结合类型参数
比如,我们希望写一个获取对象属性值的方法,我们需要确保不会去获取不存在的属性。
function getProperty<T, Key extends keyof obj>(obj: T, key: Key) {
return obj[key];
}
2
3
# 泛型结合类
class BeeKeeper {
hasMask: boolean = true;
}
class ZooKeeper {
nametag: string = "Mikle";
}
class Animal {
numLegs: number = 4;
}
class Bee extends Animal {
keeper: BeeKeeper = new BeeKeeper();
}
class Lion extends Animal {
keeper: ZooKeeper = new ZooKeeper();
}
function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
createInstance(Lion).keeper.nametag;
createInstance(Bee).keeper.hasMask;
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 Waiter {
isTeaching: boolean;
say: () => {};
}
interface Teacher {
isTeaching: boolean;
skill: () => {};
}
function judgeWho(person: Waiter | Teacher) {
person.say();
}
2
3
4
5
6
7
8
9
10
11
12
13
上述例子,由于不知道 person 的类型是什么,无法判断 person 是否有 say 函数,因此 ts 会报错。
我们可以利用类型断言 ( as ) 来保护类型。例子如下:
function judgeWho(person: Waiter | Teacher) {
if (person.isTeaching) {
(person as Teacher).skill();
}else{
(person as Waiter).say();
}
}
2
3
4
5
6
7
8
或者利用 in 语法
function judgeWho(person: Waiter | Teacher) {
if ("skill" in person) {
person.skill();
} else {
person.say();
}
}
2
3
4
5
6
7
利用 typeof 来保护类型
function add(first: string | number, second: string | number) {
if (typeof first === "string" || typeof second === "string") {
return `${first}${second}`;
}
return first + second;
}
2
3
4
5
6
利用 instanceof 来保护类型 (只能作用 类)
class NumberObj {
count: number;
}
function addObj(first: object | NumberObj, second: object | NumberObj) {
if (first instanceof NumberObj && second instanceof NumberObj) {
return first.count + second.count;
}
return 0;
}
2
3
4
5
6
7
8
9
# 可辩别联合
试想我们要一个处理Shape:(Circle、Squares)
的函数,Shape
定义如下:
interface Shape {
kind: "circle" | "square";
radius?: number;
sideLength?: number;
}
2
3
4
5
然后我们需要去获取其面积:
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
// Object is possibly 'undefined'.
}
else{
...
}
}
2
3
4
5
6
7
8
9
因为我们一开始定义radius
属性的时候设置了可选属性,但是我们需要在这里认为其一定存在,前后语义矛盾了!这里我们可以使用!
非空断言符号来解决。
但这不是一个很好的方法,主要的问题在于Shape
并不能根据kind
的值来判断radius
是否存在。因此我们只能将Circle
和Square
分开定义
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
2
3
4
5
6
7
8
9
10
11
此时Shape
联合类型中每个类型都有kind
属性,即可辩别联合,然后就可以根据其来对具体成员类型进行收窄
# 穷尽检查 (Exhaustiveness checking)
never
类型可以赋值给任何类型,但没有类型可以赋值给 never,因此当 typescript 将所有类型可能都判断完毕后,就可以使用 never 来做一个穷尽检查:
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}
2
3
4
5
6
7
8
9
10
11