Typed JavaScript at Any Scale,Typescript 是添加了类型系统的 JavaScript,适用于任何规模的项目。
keyof T
索引查询结果为该类型上所有公有属性 key 的联合。
interface Eg1 {
name: string,
readonly age: number,
}
/** T1的类型实则是name | age */
type T1 = keyof Eg1
class Eg2 {
private name: string;
public readonly age: number;
protected home: string;
}
/** T2实则被约束为 age, 而 name 和 home 不是公有属性,所以不能被keyof获取到 */
type T2 = keyof Eg2;
T[K]
索引访问interface Person {
name: string,
readonly age: number,
}
/** string */
type V1 = Person['name']
/** string | number */
type V2 = Person['name' | 'age']
/** Error:Property 'address' does not exist on type 'Person'. */
type V3 = Person['name' | 'address']
/** string | number */
type V4 = Person[keyof Person]
T[keyof T]
的方式,可以获取到 T
所有 key
的类型组成的联合类型; T[keyof K]
的方式,获取到的是 T
中的 key
且同时存在于K
时的类型组成的联合类型; 注意:如果[]
中的 key有不存在 T中的,则ts也不知道该key最终是什么类型,所以会报错;
交叉类型是取多个类型的并集,但是如果相同 key
但是类型不同,则该 key
为 never
。
1. extends 用于接口,表示继承,接口支持多重继承,语法为逗号隔开:
interface T1 {
name: string,
}
interface T2 {
sex: number,
}
/**
* @example
* T3 = {name: string, sex: number, age: number}
*/
interface T3 extends T1, T2 {
age: number,
}
2. extends 表示条件类型,可用于条件判断,extends
前面的参数为联合类型时则会分解(依次遍历所有的子类型进行条件判断)联合类型进行判断。然后将最终的结果组成新的联合类型。如果不想被分解,可以通过简单的元组类型包裹。
/**
* @example
* type A3 = 1 | 2
*/
type P = T extends 'x' ? 1 : 2;
type A3 = P<'x' | 'y'>
/**
* @example
* type A4 = 2;
*/
type P = [T] extends ['x'] ? 1 : 2;
type A4 = P<'x' | 'y'>
集合论中,如果一个集合 A 的所有元素在集合B中都存在,则A是B的子集;
类型系统中,如果一个类型的属性更具体,则该类型是子类型。(对于 inteface 中,属性更少则说明该类型约束的更宽泛,是父类型,对于联合类型,包括种类越多的类型是父类型)
Animal
和 Dog
在变成数组后,Array
依旧可以赋值给Array
,因此对于 type MakeArray = Array
来说就是协变的:interface Animal {
name: string;
}
interface Dog extends Animal {
break(): void;
}
const animal: Animal;
const dog: Dog;
const animals: Array
const dogs: Array
/** 可以赋值 */
animal = dog;
/** 可以赋值 */
animal = dogs
3. 逆变:具有父子关系的多个类型,在通过某种构造关系构造成的新的类型,如果关系逆转(子变父,父变子)就是逆变的。Animal
和 Dog
在进行 type Fn
构造器构造后,父子关系逆转:
animalFn 在调用时约束的参数,缺少 dogFn 的参数所需的 break,此时会导致错误。
4. 双向协变:Ts在函数参数的比较中实际上默认采取的是只有当源函数参数能够赋值给目标函数或者反过来时才能赋值成功的双向协变的策略。可以通过tsconfig.js
中修改 strictFunctionType
属性来严格控制协变和逆变。infer
关键词的功能主要是用于extends
的条件类型中让 TS 自己推到类型,并将推导结果存储在其参数绑定的类型上,比如 infer P
就是将结果存在类型 P
上,infer
关键词只能在 extends
条件类型上使用。
infer
推导的名称相同并且都处于逆变的位置,则推导的结果将会是交叉类型。infer
推导的名称相同并且都处于协变的位置,则推导的结果将会是联合类型。进阶——infer
实现一个推导数组所有元素的类型:
/**
* 约束参数T为数组类型,
* 判断T是否为数组,如果是数组类型则推导数组元素的类型
*/
type FalttenArray> = T extends Array ? P : never;
const
进行常量声明且不可修改,如果进行修改的话会直接 Cannot assign to 'a' because it is a constant.
进行异常抛错。但是如果值是一个引用类型的话,依旧可以对其内部的属性进行修改。那么从只读
的概念上来说,显然不具备当前的能力。可以使用以下四种方式对属性或者是变量进行声明达到只读的效果。
/** 1. Readonly */
interface X {
x: number;
}
let rx: Readonly = { x: 1 };
rx.x = 12; // error
/** 2. readonly modifier for properties */
interface Rx {
readonly x: number;
}
let rx: Rx = { x: 1 };
rx.x = 12; // error
/** 3. ReadonlyArray */
let a: ReadonlyArray = [1, 2, 3];
let b: readonly number[] = [1, 2, 3];
a.push(102); // error
b[0] = 101; // error
/** 4. as const */
let a = [1, 2, 3] as const;
a.push(102); // error
a[0] = 101; // error
Readonly 实现原理:
/**
* 主要实现是通过映射遍历所有key,
* 然后给每个key增加一个readonly修饰符
*/
type Readonly = {
readonly [P in keyof T]: T[P]
}
通过extends
的方式继承父类然后通过 ? :
表达式来进行一个类型三目运算符
的操作进行一个类型的条件判断。
interface Animal {
live(): void;
}
interface Dog extends Animal {
woof(): void;
}
type Example1 = Dog extends Animal ? number : string;
type Example2 = RegExp extends Animal ? number : string;
命名空间(namespace)
常用于组织一份类型区域防止类型之间的重命名冲突,需要配置 declare
输出到外部环境才能够使用,使用 declare namespace
在工程项目中可以不需要引入任何类型而直接可以访问,非常便捷。
declare namespace myLib {
function makeGreeting(s: string): string;
let numberOfGreetings: number;
}
declare
是用于声明存在的
declare var/let/const
用来声明全局的变量。declare function
用来声明全局方法(函数)declare class
用来声明全局类declare namespace
用来声明命名空间declare module
用来声明模块需要注意的是,declare
与declare global
它们功能是一样的。在d.ts
中,使用declare
即可,默认是全局的,它
与declare global
作用是相等的。而在模块文件中定义declare
,如果想要用作全局就可以使用declare global。
/** types/foo/index.d.ts */
declare global {
interface String {
prependHello(): string;
}
}
/** 注意即使此声明文件不需要导出任何东西,仍然需要导出一个空对象,用来告诉编译器这是一个模块的声明文件,而不是一个全局变量的声明文件。 */
export {};
/** src/index.ts 使用 */
'bar'.prependHello();
若果需要扩展原有模块的话,需要在类型声明文件中先引用原有模块,再使用 declare module
扩展原有模块:
/** types/moment-plugin/index.d.ts */
import * as moment from 'moment';
declare module 'moment' {
export function foo(): moment.CalendarKey;
}
/** src/index.ts */
import * as moment from 'moment';
import 'moment-plugin';
moment.foo();
declare module
也可用于在一个文件中一次性声明多个模块的类型:
/** types/foo-bar.d.ts */
declare module 'foo' {
export interface Foo {
foo: string;
}
}
declare module 'bar' {
export function bar(): string;
}
/** src/index.ts */
import { Foo } from 'foo';
import * as bar from 'bar';
let f: Foo;
bar.bar();
模板字符串能够对文本进行一定程度上的约束。
/** global.d.ts */
declare type HTTP = `http://${string}`
declare type HTTPS = `https://${string}`
/** @/config/index.d.ts */
type baseApi = HTTP | HTTPS;
约定当前值中必须包含http://
或者是https://
才算校验成功。
函数重载
大多数用于多态函数,它能够定义不同的参数类型。需要有多个重载签名
和一个实现签名
。
重载签名
:就是对参数形式的不同书写,可以定义多种模式。实现签名
:对函数内部方法的具体实现。function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string | void {
if (typeof x === 'number') {
return Number(x.toString().split('').reverse().join(''));
} else if (typeof x === 'string') {
return x.split('').reverse().join('');
}
}
get/set存取器
保证了类中变量的私有化,在外部时时不能直接对其更改的。在class
中声明一个带_
下标的变量,那么就可以通过get
, set
对其进行设置值。
如果访问了私有化类型的变量会在编译时就发出告警。
枚举(enum
)的使用场景在于可以定义部分行为和状态,对其某个任务的行为定义在枚举当中,这样做可以进行一些状态复用,避免在页面写太多魔数而不利于维护。
泛型是TypeScript
在类型推导很难进行推导。相比于使用 any 类型,使用泛型来创建可复用的组件要更好,因为泛型会保留参数类型。
type Generics = {
name: string
age: number
sex: T
}
function setSex (sex: T) {
}
class Person {
private sex: T;
constructor(readonly type: T) {
this.sex = type;
}
}
Record
能够快速创建对象类型。它的使用方式是Record
,能够快速的为object
创建统一的key
和value
类型。
Record 的实现原理:
/**
* 核心实现就是遍历 K,将值设置为 T
*/
type Record = {
[P in K]: T
}
值得注意的是 keyof any
得到的是 string | number | symbol
,原因在类型 key的类型只能为 string | number | symbol。
Pick
:主要作用是从一组属性中拿出某个属性,并将其返回。Pick
的使用方法是Pick
,如(P)类型中拥有name
,age
,desc
三个属性,那么K
为 name
则最终将取到只有name
的属性,其他的将会被排出。Pick 实现原理:
type Pick = {
[P in K]: T[P];
};
// 或者
type Pick = {
[P in keyof T]: T[P];
}
Omit
:主要作用是从一组属性中排除某个属性,并将排除属性后的结果返回。Omit
的使用方法是Omit
,与Pick
的结果是相反的,如果说Pick
是取出,那么Omit
则是过滤的效果。Omit 的实现原理:
/**
* 利用Pick实现Omit
*/
type Omit = Pick>;
// 或者
/**
* 利用映射类型Omit
*/
type Omit2 = {
[P in Exclude]: T[P]
}
Exclude
: 从一个联合类型中排除掉属于另一个联合类型的子集。Exclude
使用形式是Exclude
,如果联合类型 T
中的在 S
中不存在那么就会返回,相当于取差集。
Exclude的实现原理:
/**
* 遍历 T 中的所有子类型,如果该子类型约束于 U(存在于U、兼容于U),则返回 never 类型,否则返回该子类型
* never 表示一个不存在的类型,never 与其他类型的联合后,是没有 never 的。
*/
type Exclude = T extends U ? never : T;
Extract
:跟Exclude相反,从从一个联合类型中取出属于另一个联合类型的子集。Extract
就是取交集。会返回两个联合类型
中相同的部分。
Extract 的实现原理:
type Extract = T extends U ? T : never;
Partial
是一个将类型转为可选类型的工具,对于不明确的类型来说,需要将所有的属性转化为可选的?.
形式,转换成为可选的属性类型。
/**
* 核心实现就是通过映射类型遍历T上所有的属性,
* 然后将每个属性设置为可选属性
*/
type Partial = {
[P in keyof T]?: T[P];
}
进阶——将指定的key变成可选类型:
/**
* 将指定的key变成可选类型:
* 主要通过K extends keyof T约束K必须为keyof T的子类型
* keyof T得到的是T的所有key组成的联合类型
*/
type PartialOptional = {
[P in K]?: T[P];
}
Partial
、Readonly
和 Pick
都属于同态的,即他们的实现需要 keyof T 遍历输入类型 T来拷贝属性,因此属性修饰符(例如readonly、?:)都会被拷贝。而 Record
是非同态的,不需要拷贝属性,因此不会拷贝属性修饰符。
Parameters 获取函数的参数类型,将每个参数类型放在一个元组中,实现原理:
/**
* @desc 具体实现
*/
type Parameters any> = T extends (...args: infer P) => any ? P : never;
/**
* 或者
*/
type Parameters = T extends (...args: infer P) => any ? P : never;
ReturnType 获取函数的返回值类型,实现原理:
/**
* @desc ReturnType的实现其实和Parameters的基本一样
* 无非是使用infer R的位置不一样。
*/
type ReturnType any> = T extends (...args: any) => infer R ? R : any;
ConstructorParameters 获取类的构造函数的参数类型,存在一个元组中,实现原理:
/**
* 核心实现还是利用infer进行推导构造函数的参数类型
*/
type ConstructorParameters any> = T extends abstract new (...args: infer P) => any ? P : never;
首先约束参数 T
为拥有构造函数的类(使用 abstract 关键字将类型定义为抽象类,则既可以赋值为抽象类,也可以赋值为普通类;反之如果只是定义为普通类,则只能赋值为普通类),实现时,判断T
是满足约束的类时,利用infer P
自动推导构造函数的参数类型,并最终返回该类型。
/**
* 定义一个普通类
*/
class MyClass {}
/**
* 定义一个抽象类
*/
abstract class MyAbstractClass {}
/** 可以赋值 */
const c1: typeof MyClass = MyClass */
/** 报错,无法将抽象构造函数类型分配给非抽象构造函数类型
const c2: typeof MyClass = MyAbstractClass
/** 可以赋值 */
const c3: typeof MyAbstractClass = MyClass
/** 可以赋值 */
const c4: typeof MyAbstractClass = MyAbstractClass
需要注意的是:
typeof 类
作为类型时,约束的是——满足该类的类型;即该类型获取的是该类上的静态属性和方法。进阶——获取构造函数返回值的类型:
type InstanceType any> = T extends abstract new (...args: any) => infer R ? R : any;
/**
* Uppercase
* @desc 构造一个将字符串转大写的类型
*/
type Eg1 = Uppercase<'abcd'>;
/**
* Lowercase
* @desc 构造一个将字符串转小大写的类型
*/
type Eg2 = Lowercase<'ABCD'>;
/**
* Capitalize
* @desc 构造一个将字符串首字符转大写的类型
*/
type Eg3 = Capitalize<'Abcd'>;
/**
* Uncapitalize
* @desc 构造一个将字符串首字符转小写的类型
*/
type Eg4 = Uncapitalize<'aBCD'>;
实现一个 EventEmitter 类,该类中存在两个方法 on / emit。on(functionName, function)
方法可以订阅一个 functionName 的函数。emit(functionName, ...args)
方法可以调用 on 方法中注册的函数,获得对应的函数类型。
class EventEmitter> {
private fnMap = new Map void>();
on(key: K, fn: (...args: T[K]) => void): void {
this.fnMap.set(key, fn);
}
emit(key: K): (...args: T[K]) => void {
const fn = this.fnMap.get(key);
return (...args: T[K]) => {
fn && fn(...args);
};
}
}
const eventEmitter = new EventEmitter<{
getName: [string],
getAge: [number],
}>();
eventEmitter.on('getName', (name: string) => {
console.log(name)
});
eventEmitter.emit('getName')('edemao');
eventEmitter.on('getAge', (age: number) => {
console.log(age)
});
eventEmitter.emit('getAge')(26);