Typescript学习总结
/ 32 min read
Typescript 是 Javascript 的超集而非一门真正的语言。它最终的运行是转化为 JavaScript。没有可以真实运行 Typescript 的环境...
一、前言
Typescript 是 Javascript 的超集而非一门真正的语言。它最终的运行是转化为 JavaScript。没有可以真实运行 Typescript 的环境。没有 TS 的日子 JS 依旧可以发展的很好。前端领域缺少对 TS 的硬性要求,缺乏考察标准。原始类型、数组、接口、枚举、函数、字面量、断言和 global.d.ts 覆盖了大部分使用场景。再深入的使用反而增加了使用成本和阅读成本。即使有心深入去了解 TS 如何应用,也存在比较高的门槛。因为它建立在“约定”之上,不具备底层逻辑性。
Typescript 带给前端开发者安全感同时提高效率。适当的类型限制,在开发阶段、构建阶段提高代码健壮性;使用 TS 在项目中定义变量类型,配合 IDE 进行异常提示、类型推导,提高开发效率。
二、基础概念
1. 模块系统
Typescript 的模块系统分为全局模块和文件模块(外部模块),它推荐使用 ES6 的语法规则 来导入/导出模块。最终编译为 JavaScript 进行运行,支持编译为多种规范的模块类型。事实上 Typescript 的模块系统应该是基于类型声明来讨论。Typescript 的模块系统可以归纳为以下三部分(民间归纳):
-
基于 lib.d.ts/global.d.ts 的全局模块,同时支持 declare module xxx 的模块定义,作用于项目空间上下文;
-
基于代码文件的文件模块(外部模块),支持 ES 语法的导入导出,作用于文件本身或者被导入文件;
-
基于三方依赖的文件模块,特指 node_module 指定的类型定义文件,或者是代码文件同路径下的 xxx.d.ts 文件;
2. 使用接口
Typescript 使用结构化类型,结构化类型系统背后的思想是如果他们的成员类型是兼容的。接口旨在声明 JavaScript 中可能存在的任意结构。接口具备的一些特点:
- 和内联注解结构相同时,两者相同;
- 在同模块中名称相同的接口会合并;
- 支持继承(extends)和实现(implements);
接口的一些使用实例:
- 可调用接口
interface ReturnString {
(): string;
}
declare const returnString: ReturnString;
const bar = returnString();
- 可重载
interface Overloaded {
(foo: number): number;
(foo: string): string;
}
const overloaded: Overloaded = function(foo: any): any {
if (typeof foo === 'number') {
return foo * foo;
} else if (typeof foo === 'string') {
return `hello ${foo}`;
}
}
const baz = overloaded(1);
const bar = overloaded('bar');
- 可实例化
interface Crazy {
new (): string;
}
declare const CrazyMan: Crazy;
const Jack = new CrazyMan();
- 可实现(implements)
interface CrazyConstructor {
new (name: string): CrazyInstance;
}
interface CrazyInstance {
isMan(): Boolean;
}
const CrazyMan: CrazyConstructor = class implements CrazyInstance {
constructor(name) {
this.name = name;
}
isMan() {
return true;
}
name: string;
}
const Jack = new CrazyMan('Jack');
3. 索引签名
TypeScript 的索引签名必须是 string 或者 number。symbols 也是有效的。所有成员都必须符合字符串的索引签名。讲索引签名是因为经常会碰到 Window 下的索引签名定义。
- 所有成员都必须符合字符串的索引签名
interface Window {
BridgeCall: () => void;
[index: number]: string;
[index: string]: string | (() => void);
// [index: string]: any;
}
- 使用一组有限的字符串字面量
type Behavior = 'EAT' | 'SLEEP' | 'BARK'
type Gog = {
[index in Behavior]: () => void;
}
type ICRY = Gog['CRY'];
- 索引签名中排除某些属性
// 报错
type FieldState = {
value: string;
};
type FromState = {
isValid: boolean; // Error: 不符合索引签名
[filedName: string]: FieldState;
};
// 使用交叉类型 可以在索要签名中排除某些属性
type FieldState = {
value: string;
};
type FormState = { isValid: boolean } & { [fieldName: string]: FieldState };
4. ThisType
This 对 JavaScript 开发者来说并不陌生,编码过程中或多或少要使用到,所以对 This 的类型定义值得被拿出来记录。以下类型都是在配置 noImplicitThis: true 的情况
- 当 this 为方法入参时,定义 this 类型
const foo = {
x: 1,
m: function(this: {x: string}) {
// this: {x: string}
}
}
const foo = {
x: 1,
m: function(this) {
// this: { x: number, m: (this...) => void }
}
}
- 当类型有 ThisType 定义时,定义 this 类型
const foo: { x: number, m: () => void } & ThisType<{x: string}> = {
x: 1,
m: function() {
// this: {x: string}
}
};
- 当类型没有 ThisType 定义时,this 为对象字面的类型
type FooType = { x: number, m: () => void };
const foo: FooType = {
x: 1,
m: function() {
// this: FooType
}
};
- 重新定义 ThisType 类型的使用场景
当开启 noImplicitThis 的情况下,保持良好的开发习惯(不使用 this 作为入参),以上下文作为 this 类型比较准确;但是存在一些我们修改了 this 的情况就需要做特殊定义了。
// Compile with --noImplicitThis
type ObjectDescriptor<D, M> = {
data?: D;
methods?: M & ThisType<D & M>; // Type of 'this' in methods is D & M
};
function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M {
let data: object = desc.data || {};
let methods: object = desc.methods || {};
return { ...data, ...methods } as D & M;
}
let obj = makeObject({
data: { x: 0, y: 0 },
methods: {
moveBy(dx: number, dy: number) {
this.x += dx; // Strongly typed this
this.y += dy; // Strongly typed this
}
}
});
obj.x = 10;
obj.y = 20;
obj.moveBy(5, 5);
三、类型分发(条件类型)
类型分发依赖 extends 关键词,extends 即可在继承时使用,也可以在类型分发使用。在类型分发的使用中它的意思是左边可分配给右边。
- 类型分发示例
interface Animal {
live(): void;
}
interface Dog extends Animal {
woof(): void;
}
type Condition<T> = T extends Animal ? true : false;
const c: Condition<Dog> = true;
- extends 的一些论证
// eg.1 对象字面量
type Human = { name: string; }
type lookasHuman = { name: string; age: number; }
type Bool = lookasHuman extends Human ? true : false; // true
// eg.2 联合类型
type A1 = 'x' extends 'x' ? string : number // string
type A2 = 'x' | 'y' extends 'x' ? string : number // number
type A3 = 'y' | 'x' extends 'x' ? string : number // number
type A4 = 'y' extends 'x' | 'y' ? string : number // string
type A5 = 'y' | 'x' extends 'x' | 'y' ? string : number // string
// eg.3 分配条件类型
// 注意:当条件类型作用于泛型类型时,当给定联合类型时,它们就变成分布类型
// 举个例子:在条件类型 T extends U ? X : Y 中,当 T 是 A | B时,会拆分成
// A extends U ? X : Y | B extends U ? X : Y
type P<T> = T extends 'x' ? string : number
type B = P<'x' | 'y'> // string | number
// eg.4 防止条件判断中的分配
// 在条件判断类型的定义中,将泛型参数使用 [] 括起来,
// 即可阻断条件判断类型的分配,此时,传入参数T的类型将被当做一个整体,不再分配。
type P<T> = [T] extends ['x'] ? string : number;
type A1 = P<'x' | 'y'> // number
// never是所有类型的子类型
type A2 = P<never> // string
3. extends 的另一个运用 - 泛型约束
// eg.1
type Color = 'green' | 'blue';
type Condition<T extends Color> = { [k in Color]: T }[T]
const c: Condition<'green'> = 'green';
const c2: Condition<'red'> = 'green'; // Error
四、类型推断
类型推断使 Typescript 变得强大,可以在代码的逻辑里推断出具体类型来。配合 IDE 的实时推断、代码提示,开发体验得到增强。当然在整个推导过程中,若中间环节被错误定义,便会导致推导结果达不到预期。这里建议项目中开启 noImplicitAny 避免一些隐藏问题。
- 定义变量 & 赋值
在定义变量过程中,会以值得内容作为类型
const foo = 123; // number
const bar = 'hello'; // string
type Adder = (a: number, b: number) => number;
let biz: Adder = (a, b) => a + b;
function iTakeAnAdder(adder: Adder) {
return adder(1, 2);
}
iTakeAnAdder((a, b) => {
return a + b;
});
- 函数返回
在函数里返回类型可以被 return 语句推断,没有返回则为 void 类型
function add(a: number, b: number) {
return a + b;
}
const sum = add(1, 2); // number
- 结构化
这些简单的规则也适用于结构化的存在(对象字面量)
const foo = {
a: 123,
b: 456,
};
foo.a = 'hello'; // Error: 不能把 'string' 类型赋值给 'number' 类型
数组
const bar = [1, 2, 3];
bar[0] = 'hello'; // Error: 不能把 'string' 类型赋值给 'number' 类型
- 解构
对象字面量:
const foo = {
a: 123,
b: 456,
};
let { a } = foo;
a = 'hello'; // Error: 不能把 'string' 类型赋值给 'number' 类型
数组:
const bar = [1, 2];
let [a, b] = bar;
a = 'hello';
函数参数:
type Adder = (n: {a: number; b: number}) => number;
function iTakeAnAdder(adder: Adder) {
return adder({a: 1, b: 2});
}
iTakeAnAdder(({a, b}) => {
a = 'hello'; // Error: 不能把 'string' 类型赋值给 'number' 类型
return a + b;
});
- inter
- 概念:
Inter 的出现使类型推断能力提升,inter 可以理解为一个中间量,用于指代某个未知变量。或者可以看出是占位。结合 extends 使用可以用于推导未知类型。使用场景:
type Parameters<T> = T extends (...args: infer R) => any ? R : any;
type Adder = (a: number, b: number) => number;
type T0 = Parameters<Adder>; // [number, number]
-
内置类型:
- ReturnType
type ReturnType<T> = T extends (...args: any[]) => infer P ? P : any;
- ConstructorParameters
type ConstructorParameters<T extends new (...args: any[]) => any> = T extends new (...args: infer P) => any ? P : never; type InstanceType<T extends new (...args: any[]) => any> = T extends new (...args: any[]) => infer P ? P : any;
-
tuple 转 union
type TTuple = [string, number];
type TArray = Array<string | number>;
type ElementOf<T> = T extends Array<infer E> ? E : never;
type ToUnion = ElementOf<TTuple>;
- Union 转 intersection
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void)
? I
: never;
type T1 = { name: string }
type T2 = { age: number }
type Reg = UnionToIntersection<T1 | T2> // T1 & T2
- 类型保护是类型推断的结果
五、类型兼容
类型兼容性用于确定一个类型是否能赋值给其他类型。 如 string 类型与 number 类型不兼容:
let str: string = 'Hello';
let num: number = 123;
str = num; // Error: 'number' 不能赋值给 'string'
num = str; // Error: 'string' 不能赋值给 'number'
- 安全性
类型兼容不一定安全,比如,小狗属于动物。说明小狗和动物是兼容的,但不是所有的动物都会叫,这里就会导致一定安全风险。尽可能的缩小类型范围,减少安全性风险
let foo: any = 123;
foo = 'Hello';
foo.toPrecision(3);
- 结构化
Typescript 对象是一种结构类型,这意味着只要结构匹配,名称也就无关紧要了:
interface Point {
x: number;
y: number;
}
class Point2D {
// public 是 Point2D实例具有 x, y 属性
constructor(public x: number, public y: number) {}
}
let p: Point;
p = new Point2D(1, 2);
这允许你动态创建对象,并且它如果能被推断,该对象仍然具有安全性
interface Point2D {
x: number;
y: number;
}
interface Point3D {
x: number;
y: number;
z: number;
}
const point2D: Point2D = { x: 0, y: 0 };
const point3D: Point3D = { x: 0, y: 0, z: 0};
function iTakePoint2D(point: Ponit2D) {
// do something
}
iTakePoint2D(point2D);
iTakePoint2D(point3D);
iTakePoint2D({ x: 0 }); // Error: 没有 'y'
- 变体
对类型兼容性来说,变体是一个利于理解和重要的概念。 对一个简单类型 Base 和 Child 来说,如果 Child 是 Base 的子类,Child 的实例能被赋值给 Base 类型的变量。 在由 Base 和 Child 组合的复杂类型的类型兼容性中,它取决于相同场景下的 Base 与 Child 的变体:
- 协变(Covariant):只在同一个方向,示例:Base -> Child,SuperType
-> SuperType ; - 逆变(Contravariant):只在相反的方向,示例:Base -> Child,SuperType
-> SuperType ; - 双向协变(Bivariant):包括同一个方向和不同方向,示例:Base -> Child,SuperType
<-> SuperType ; - 不变(Invariant):如果类型不完全相同,则它们是不兼容的,示例:Base -> Child,SuperType
<!=> SuperType 。
- 函数
- 返回类型
协变:返回类型必须包含足够的数据。
- 参数数量
更少的参数数量是好的(如:函数能够选择性的忽略一些多余的参数),但是你得保证有足够的参数被使用了。换言之,函数参数随数量减少协变:
// eg.1
type T1 = (a: number, b: number) => number;
type T2 = (a: number) => number;
type Reg0 = T2 extends T1 ? true : false // true;
let func1: T1 = function (a, b) { return a + b };
let func2: T2 = function (a) { return Math.floor(a) };
func1 = func2;
func2 = func1; // Error: 'func1' 不能赋值给 'func2'
// eg.2
const iTakeSomethingAndPassItAnErr = (x: (err: Error, data: any)) => {
// do something
};
iTakeSomethingAndPassItAnErr(() => null);
iTakeSomethingAndPassItAnErr(err => null);
iTakeSomethingAndPassItAnErr((err, data) => null);
iTakeSomethingAndPassItAnErr((err, data, more) => null); // Error
// eg.3
type P1 = (n: { a: number, b: number }) => void;
type P2 = (n: { a: number }) => void;
type Result = P2 extends P1 ? true : false; // true
- 可选的和 rest 参数
可选的(预先确定的)和 Rest 参数(任何数量的参数)都是兼容的:
1. 在 strictNullChecks 为 false 时,可以理解为双向协变
2. 在 strictNullChecks 为 true 时,确定参数和 rest 参数为双向协变,可选参数为不变性;
// 可选的(上例子中的 bar)与不可选的(上例子中的 foo)仅在选项为 strictNullChecks 为 false 时兼容。
let foo = (x: number, y: number) => {};
let bar = (x?: number, y?: number) => {};
let bas = (...args: number[]) => {};
foo = bar = bas;
bas = bar = foo;
-
函数参数类型
- 在 strictFunctionTypes 为 false 的情况下是双向协变的,旨在支持常见的事件处理方案
- 在 strictFunctionTypes 为 true 的情况下是逆变的
// 事件等级
interface Event {
timestamp: number;
}
interface MouseEvent extends Event {
x: number;
y: number;
}
interface KeyEvent extends Event {
keyCode: number;
}
// 简单的事件监听
enum EventType {
Mouse,
Keyboard
}
function addEventListener(eventType: EventType, handler: (n: Event) => void) {
// ...
}
// 不安全,但是有用,常见。函数参数的比较是双向协变。
addEventListener(EventType.Mouse, (e: MouseEvent) => console.log(e.x + ',' + e.y));
// 在安全情景下的一种不好方案
addEventListener(EventType.Mouse, (e: Event) => console.log((<MouseEvent>e).x + ',' + (<MouseEvent>e).y));
addEventListener(EventType.Mouse, <(e: Event) => void>((e: MouseEvent) => console.log(e.x + ',' + e.y)));
// 仍然不允许明确的错误,对完全不兼容的类型会强制检查
addEventListener(EventType.Mouse, (e: number) => console.log(e));
- 枚举
- 枚举与数字类型相互兼容
- 来自于不同枚举的枚举变量,被认为是不兼容的
- 类
- 仅仅只有实例成员和方法会相比较,构造函数和静态成员不会被检查:
class Animal {
feet: number;
constructor(name: string, numFeet: number) {}
}
class Size {
feet: number;
constructor(meters: number) {}
}
let a: Animal = new Animal('', 1);
let s: Size = new Size(1);
a = s; // OK
s = a; // Ok
- 私有的和受保护的成员必须来自于相同的类:
class Animal {
protected feet: number;
}
class Cat extends Animal {}
let animal: Animal = new Animal();
let cat: Cat = new Cat();
animal = cat; // ok
cat = animal; // ok
class Dog extends Animal {
protected canWang: boolean;
}
let dog: Dog = new Dog();
animal = dog; // OK !!!
dog = animal; // Error
class Size {
protected feet: number;
}
let size: Size = new Size();
animal = size; // ERROR
size = animal; // ERROR
- 泛型
- Typescript 类型系统基于变量的结构,仅当类型参数在被一个成员使用时,才影响兼容性。如下例子中,T 对兼容性没有影响:
interface Empty<T> {}
let x: Empty<number>;
let y: Empty<string>;
x = y; // ok
- 当 T 被成员使用时,它将在实例化泛型后影响兼容性:
interface Empty<T> {
data: T;
}
let x: Empty<number>;
let y: Empty<string>;
x = y; // Error
- 如果尚未实例化泛型参数,则在检查兼容性之前将其替换为 any:
let identity = function<T>(x: T): T {
// ...
};
let reverse = function<U>(y: U): U {
// ...
};
identity = reverse; // ok
- 类中的泛型兼容性与前文所提及一致:
class List<T> {
add(val: T) {}
}
class Animal {
name: string;
}
class Cat extends Animal {
meow() {
// ...
}
}
const animals = new List<Animal>();
animals.add(new Animal()); //ok
animals.add(new Cat()); // ok
const cats = new List<Cat>();
cats.add(new Animal()); // Error
cats.add(new Cat()); // ok
六、类型保护
类型声明后,包含接口定义、赋值、参数等对变量起到类型约束作用。类型保护在这个基础进一步缩小范围,使类型定义在一个范围内更准确,更安全。
- Typeof
当使用 typeof 明确某个变量类型之后,在该分支下该变量的类型得到保护,类型明确。
function doSome(x: number | string) {
if (typeof x === 'string') {
console.log(x.subtr(1)); // Error
console.log(x.substr(1));
}
x.substr(1); // Error
}
- Instanceof
与 typeof 类似,当使用 instanceof 明确某个变量的原型对象时,在该分支下该变量的类型得到保护,类型明确。
class Foo {
foo = 123;
common = '123';
}
class Bar {
bar = 123;
common = '123';
}
function doStuff(arg: Foo | Bar) {
if (arg instanceof Foo) {
console.log(arg.foo); // ok
console.log(arg.bar); // Error
}
if (arg instanceof Bar) {
console.log(arg.foo); // Error
console.log(arg.bar); // ok
}
}
doStuff(new Foo());
doStuff(new Bar());
- In
In 操作符可以安全的检查一个对象上是否存在一个属性,它通常也被作为类型保护使用
interface A {
x: number;
}
interface B {
y: string;
}
function dStuff(q: A | B) {
if ('x' in q) {
// q: A
} else {
// q: B
}
}
- 字面量类型保护
可以通过明确对象字面量的属性类型来区分对象字面量,在该分支下对象字面量类型明确。
type Foo = {
kind: 'foo';
foo: number;
};
type Bar = {
kind: 'bar';
bar: number;
};
function doStuff(arg: Foo | Bar) {
if (arg.kind === 'foo') {
console.log(arg.foo); // ok
console.log(arg.bar); // Error
} else {
console.log(arg.foo); // Error
console.log(arg.bar); // ok
}
}
- 使用定义的类型保护
使用自定义的类型保护函数
interface Foo {
foo: number;
common: string;
}
interface Bar {
bar: number;
common: string;
}
function isFoo(arg: Foo | Bar): arg is Foo {
return (arg as Foo).foo !== undefined;
}
七、声明合并
Declaration Type | Namespace | Type | Value |
---|---|---|---|
Namespace | x | x | |
Class | x | x | |
Enum | x | x | |
Interface | x | ||
Type Alias | x | ||
Function | x | ||
Variable | x |
- 函数合并
同个命名空间内的出现相同的函数声明被看做是函数重载声明
function func(a: string): string;
function func(a: number): number;
function func(a: any): any {
console.log(a);
};
- 接口合并
同个命名空间内相同的接口声明会触发合并,接口里相同的方法属性声明会触发重载,非方法的属性声明重复弱类型不同会报错
interface A {
a: string;
}
interface A {
b: string;
}
let a1: A = { a: '1' }; // Error
- 类的合并
类不能与其它类或变量合并
- 命名空间合并
同名命名空间会合并其成员,模块导出的同名接口进行合并,构成单一命名空间内含合并后的接口;对于命名空间里值得合并,如果当前已存在给定名字的命名空间,那么后来的命名空间的导出成员会被加到已经存在的那个模块里。
namespace Animals {
export class Zebra {}
}
namespace Animals {
export interface Legged { num: number }
export class Dog{ }
}
// 等同于
namespace Animals {
export interface Legged { num: number; }
export class Zebra {}
export class Dog {}
}
非导出成员仅在其原有的(合并前的)命名空间内可见。这就是说合并之后,从其他命名空间合并进来的成员无法访问非导出成员。
namespace Animal {
let haveMuscles = true;
export function animalsHaveMuscles() {
return haveMuscles;
}
}
namespace Animal {
export function doAnimalsHaveMuscles() {
return haveMuscles; // Error
}
}
- 命名空间与类、函数、枚举类型合并
- 合并命名空间和类
合并结果是一个类并带有一个内部类。 你也可以使用命名空间为类增加一些静态属性
class Album {
label: Album.AlbumLabel;
}
namespace Album {
export class AlbumLabel { }
}
- 合并命名空间和函数
创建一个函数稍后扩展它增加一些属性也是很常见的。 TypeScript使用声明合并来达到这个目的并保证类型安全。
function buildLabel(name: string): string {
return buildLabel.prefix + name + buildLabel.suffix;
}
namespace buildLabel {
export let suffix = "";
export let prefix = "Hello, ";
}
console.log(buildLabel("Sam Smith"));
- 合并命名空间和枚举
命名空间可以用来扩展枚举类型
enum Color {
red = 1,
green = 2,
blue = 4
}
namespace Color {
export function mixColor(colorName: string) {
if (colorName == "yellow") {
return Color.red + Color.green;
}
else if (colorName == "white") {
return Color.red + Color.green + Color.blue;
}
else if (colorName == "magenta") {
return Color.red + Color.blue;
}
else if (colorName == "cyan") {
return Color.green + Color.blue;
}
}
}
八、实践总结
- Event Emitter
interface Listener<T> {
(event: T): any;
}
interface Disposable {
dispose(): any;
}
class TypedEvent<T> {
private listeners: Listener<T>[] = [];
private listenersOncer: Listener<T>[] = [];
public on = (event: Listener<T>): Disposable => {
this.listeners.push(event);
return {
dispose: () => this.off(event),
};
};
public once = (event: Listener<T>): void => {
this.listenersOncer.push(event);
};
public off = (event: Listener<T>): void => {
const callbackIndex = this.listeners.indexOf(event);
if (callbackIndex > -1) {
this.listeners.splice(callbackIndex, 1);
}
};
public emit = (event: T) => {
this.listeners.forEach(listener => listener(event));
this.listenersOncer.forEach(listener => listener(event));
this.listenersOncer = [];
};
public pipe = (te: TypedEvent<T>): Disposable => {
return this.on(e => te.emit(e));
};
}
const onFoo = new TypedEvent<Foo>();
const onBar = new TypedEvent<Bar>();
onFoo.emit(foo);
onBar.emit(bar);
onFoo.on(foo => console.log(foo));
onBar.on(bar => console.log(bar));
- 定义请求库
- Fetch 的类型定义
interface IUser {
nick: string;
age: number;
}
function getJson<T>(config: { url: string; headers?: { [key: string]: string } }) {
const fetchConfig = {
method: 'GET',
Accept: 'application/json',
'Content-Type': 'application/json',
...(config.headers || {})
};
return fetch(config.url, fetchConfig).then<T>(response => response.json());
}
function queryUser() {
return getJson<IUser>({
url: './getUser',
});
}
- 有意思的请求库类型定义
type ResponseType
= 'json' | 'text' | 'blob' | 'arrayBuffer' | 'formData';
type RequestResponse<T = any> = {
data: T;
response: Response;
}
interface ResponseError<D = any> extends Error {
name: string;
data: D;
response: Response;
request: {
url: string;
options: RequestOptionsInit;
};
type: string;
}
interface Context {
req: {
url: string;
options: RequestOptionsInit;
},
res: any,
}
interface RequestOptionsInit extends RequestInit {
charset?: 'utf8' | 'gbk';
requestType?: 'json' | 'form';
data?: any;
params?: object | URLSearchParams;
paramsSerializer?: (params: object) => string;
responseType?: ResponseType;
useCache?: boolean;
ttl?: number;
timeout?: number;
timeoutMessage?: string;
errorHanlder?: (error: ResponseError) => void;
prefix?: string;
suffix?: string;
throwErrIfParseFail?: boolean;
parseResponse?: boolean;
cancelToken?: any;
getResponse?: boolean;
validateCache?: (url: string, options: RequestOptionsInit) => boolean;
__umiRequesstCoreType__?: string;
[key: string]: any;
}
interface RequestOptionsWithResponse extends RequestOptionsInit {
getResponse: false;
}
interface RequestOptionsWithoutResponse extends RequestOptionsInit {
getResponse: true;
}
type RequestInterceptor = (
url: string,
options: RequestOptionsInit
) => {
url?: string;
options?: RequestOptionsInit;
};
type ResponseInterceptor = (
response: Response,
options: RequestOptionsInit
) => Response | Promise<Response>;
type OnionMiddleware = (ctx: Context, next: () =r> void) => void;
type OnionOptions = { global?: boolean; core?: boolean; defaultInstance?: boolean; };
interface RequestMethod<R = false> {
<T = any>(url: string, options: RequestOptionsWithResponse): Promise<RequestResponse<T>>;
<T = any>(url: string, options: RequestOptionsWithoutResponse): Promise<T>;
<T = any>(url: string, options?: RequestOptionsInit): R extends true ? Promise<RequestResponse<T>> : Promise<T>;
interceptors: {
request: {
use: (handler: RequestInterceptor, options?: OnionOptions) => void;
};
response: {
use: (handler: ResponseInterceptor, options?: OnionOptions) => void;
};
};
use: (handler: OnionMiddleware, options?: OnionOptions) => void;
}
declare var request: RequestMethod;
export default request;
- 单例模式
class Singleton {
private static instance: Singleton;
private constructor() {}
public static getInstance() {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
someMethod() {}
}
let someThing = new Singleton();
let instance = Singleton.getInstance();
- 其他一些使用
- Never 使用
Never 表示不存在的值的类型,有很多用户
- 原始类型的交叉类型
type N = string & number;
- 提示需要处理新增类型
type foo = { type: 'foo' }
type baz = { type: 'baz' }
type biz = { type: 'biz' }
type TType = foo | baz | biz;
function func(type: TType) {
switch(type.type) {
case 'foo':
break;
case 'baz':
break;
default:
const a: never = type;
console.log(`${a} undefined`);
break;
}
}
- 作为条件语句的值
type foo = { type: 'foo' }
type baz = { type: 'baz' }
function func<T>(type: any) {
type TType = T extends foo
? foo
: never;
// dosomething
}
// 等同于
function func<T extends foo>(type: any) {
type TType = T
// dosomething;
}
-
定义 addEventListener
- eg.1 不完美的实现方式
interface Event { timestamp: number; } interface MouseEvent extends Event { x: number; y: number; } interface KeyEvent extends Event { keyCode: number; } enum EventType { Mouse = 'Mouse', Keyboard = 'Keyboard', } interface Type2Event { [EventType.Mouse]: MouseEvent, [EventType.Keyboard]: KeyEvent, }; type EventParamType<T extends EventType> = Type2Event[T]; function addEventListener(eventType: EventType.Mouse, handler: (n: MouseEvent) => void): void; function addEventListener(eventType: EventType.Keyboard, handler: (n: KeyEvent) => void): void; function addEventListener(eventType: EventType, handler: any) { switch(eventType) { case EventType.Mouse: const ev: EventParamType<typeof eventType> = { x: 0, y: 1, timestamp: 123}; handler(ev); break; case EventType.Keyboard: handler(); break; default: const a: never = eventType; console.log(`${a} undefined`); break; } } addEventListener(EventType.Mouse, function(e) { console.log(e.x, e.y) }); addEventListener(EventType.Keyboard, function(e) { console.log(e.keyCode); });
- eg2
interface Event { timestamp: number; } interface MouseEvent extends Event { x: number; y: number; } interface KeyEvent extends Event { keyCode: number; } interface EventMap { 'Mouse': MouseEvent, 'Keyboard': KeyEvent, } interface AddEventListenerType { <T extends keyof EventMap>(eventType: T, handler: (e: EventMap[T]) => void): void; (eventType: string, handler: (e: Event) => void): void; } type FactoryType = <T>(e: T extends keyof EventMap ? EventMap[T] : never) => void const createFactory = (h: (e: Event) => void): FactoryType => { return (e) => h(e); }; const addEventListener: AddEventListenerType = (eventType: keyof EventMap, listener: (e: Event) => void) => { const callback = createFactory(listener); switch(eventType) { case 'Mouse': callback<typeof eventType>({ x: 1, y: 1, timestamp: 1 }); break; case 'Keyboard': callback<typeof eventType>({ keyCode: 1, timestamp: 1 }); break; default: const n: never = eventType; console.log(`${n} undefined`); break; } }; addEventListener('Mouse', (e) => console.log(e.x, e.y)); addEventListener('Keyboard', (e) => console.log(e.keyCode));
可以参考:https://github.com/microsoft/TypeScript/blob/main/lib/lib.dom.d.ts
- LeetCode 题目
type FuncName<T> = { [P in keyof T]: T[P] extends Function ? P : never }[keyof T];
type Async2Sync<T> = T extends ((input: Promise<infer X>) => Promise<Action<infer Y>>)
? (input: X) => Action<Y>
: (T extends (action: Action<infer W>) => Action<infer V> ? ((action: W) => Action<V>) : never);
type Connect = (module: EffectModule) => { [P in FuncName<typeof module>]: Async2Sync<typeof module[P]>}
interface Action<T> {
payload?: T;
type: string;
}
const connect: Connect = (m) => ({
asyncMethod: (input: number) => {
return {
payload: `hello ${input}!`,
type: 'delay'
};
},
syncMethod: (action: Date) => {
return {
payload: action.getMilliseconds(),
type: "set-message"
};
}
});
class EffectModule {
count = 1;
message = "hello!";
asyncMethod(input: Promise<number>) {
return input.then(i => ({
payload: `hello ${i}!`,
type: 'delay'
}));
}
syncMethod(action: Action<Date>) {
return {
payload: action.payload!.getMilliseconds(),
type: "set-message"
};
}
}
const effectModule = new EffectModule();
const connected = connect(effectModule);
- 构建函数实例化入参定义
interface LogType {
type: string;
data: Record<string, any>
}
function normalizeParamsHandler(a: string, b: number): LogType {
// dosomething
return { type: 'click', data: { a: '1', b: 2 } };
}
function specialParamsHandler(a: string, b: number, c: boolean): LogType {
// dosomething
return { type: 'click', data: { a: '1', b: 2, c: true } };
}
function createFactory<T>(handler: (...args: any[]) => LogType) {
type ParamType = T extends ((...args: any[]) => LogType)
? Parameters<T>
: never;
return function(...args: ParamType) {
const logData = handler(...args);
// dosomething
};
}
const normalizeFactory = createFactory<typeof normalizeParamsHandler>(normalizeParamsHandler);
const specailParamsHandler = createFactory<typeof specialParamsHandler>(specialParamsHandler);
九、参考资料
- Typescript 文档版本
- typescript高级用法之infer的理解与使用
- 柯里化函数怎么转成TypeScript版
- TypeScript Deep Dive
- TypeScript Deep Dive 中文版
- [TypeScript中的never类型具体有什么用? - 尤雨溪的回答](http://7. https://www.zhihu.com/question/354601204/answer/888551021)
- 2020你必须准备的50道Typescript面试题
- Typescript有什么冷门但是很好用的特性?
- Typescript Handbook
- Typescript 的 extends
- 聊聊TypeScript类型兼容,协变、逆变、双向协变以及不变性