「这是我参与2022首次更文挑战的第8天,活动详情查看:2022首次更文挑战」。
1、this 类型
在 js 中,this 可以用来获取 全局对象、类实例对象、构建函数实例等引用。
在 ts 中,this 也是一种类型,我们先来看一个计算器的例子:
class Counter {
constructor(public count: number = 0) {}
public add(value: number) {
this.count += value;
return this;
}
public subtract(value: number) {
this.count -= value;
return this;
}
}
let counter = new Counter(10);
counter.add(3).add(3).subtract(1); // 15
在上述例子中,add 和 subtract 这两个方法返回的都是 this(实例对象),此类方法可以进行链式调用。
接下来,我们再来定义一个 PowCounter 类,继承 Counter
class PowCounter extends Counter {
constructor(public count: number = 0) {
super(count);
}
// 幂运算
public pow(value: number) {
this.count = this.count ** value;
return this;
}
}
const powCounter = new PowCounter(2);
powCounter.pow(3).subtract(1); // 2^3 - 1 = 8 - 1 = 7
2、索引类型
索引类型查询、索引访问
(1)索引类型查询:keyof
keyof 连接一个类型,然后返回一个由这个类型所有属性名组成的联合类型。
示例1:
interface InfoInterface {
name: string;
age: number;
}
let info: keyof InfoInterface; // 此时,info被赋值的内容就只能是 "name" 或者 "age" 了
info = "age"; // OK
info = "age1"; // Error:类型“"age1"”不可分配给类型“keyof InfoInterface”。
通过和泛型的结合使用,TS 就可以检查使用了动态属性名的代码:
示例2:
function getValue<T, K extends keyof T>(obj: T, names: K[]): T[K][] {
// n代表每一个属性名,返回每一个属性值
return names.map(n => obj[n]);
}
const info = {
name: 'dylan',
age: 18,
sex: 'man'
}
let values = getValue(info, ['name', 'age']);
console.log(values); // ['dylan', 18]:返回“name、age”属性值组成的数组
K extends keyof T:K 代表 T 的全部属性名组成的联合类型。- 参数中,obj 的类型是 T,names 的类型是 K 组成的数组。
- 返回值:T[K] 表示 T 的值,所以返回值类型是 T 的值组成的数组
(2)索引访问操作符: [ ]
其实和我们访问某个属性值是一样的,但是在 TS 中它可以访问某个属性的类型。
interface InfoInterface {
name: string;
age: number;
}
// 指定 NameTypes 的类型是 InfoInterface 中的 name 字段
type NameTypes = InfoInterface["name"]; // string:相当于是 string 类型
我们通过索引访问操作符 [] 访问到 name 字段(InfoInterface["name"]),它所访问到的类型就是 string 类型,所以 NameTypes 就是 string 类型。
我们再来看一个例子:
interface Objs<T> {
[key: number]: T; // 索引类型为number,值的类型为T
}
let keys: keyof Objs<number>; // 此时 keys 的类型为 number
但如果 key 的类型是 string 类型的话,实现接口的属性类型可以是 string | number 类型,因为这里数值类型会被转换为字符串类型。
interface Objs<T> {
[key: string]: T;
}
let keys: keyof Objs<number>; // let keys: string | number
let objs: Objs<number> = {
age: 18 // 因为Objs<number>中传入的是 number 类型,此类型代表 Objs 中的 T,所以 age 的值需要是 number 类型。
}
keyof 会返回类型不为 never、undefined、null 的属性名
interface Type {
a: never;
b: never;
c: string;
d: number;
e: undefined;
f: null;
g: object;
}
type Test = Type[keyof Type]; // type Test = string | number | object
3、映射类型
TS 中提供了借助旧类型创建新类型的方式,也就是映射类型。它能够以相同的方式,来转换旧类型中的每一个属性。
示例:
interface Info {
age: number;
name: string;
sex: string;
}
现在我们想定义一个新的类型,让 Info 中的参数都是只读的。
type ReadonlyType<T> = {
readonly [P in keyof T]: T[P]
};
type ReadonlyInfo = ReadonlyType<Info>;
// 鼠标放上去ReadonlyInfo,可以看到如下代码提示
// type ReadonlyInfo = {
// readonly age: number;
// readonly name: string;
// readonly sex: string;
// }
keyof T:由 T 中的所有参数名组成的数组P in keyof T:P 就代表 T的参数名数组 中的每一个参数名T[P]:代表参数值
TS 在内部实现 in 时,实际上使用的是 for...in 迭代器。
同理,我们来实现可选属性:
type SelectType<T> = {
[P in keyof T]?: T[P]
};
type SelectInfo = SelectType<Info>;
// 鼠标放上去 SelectInfo,可以看到如下代码提示
// type SelectInfo = {
// age?: number | undefined;
// name?: string | undefined;
// sex?: string | undefined;
// }
4、内置的映射类型
上面的只读、可选类型映射,TS中有内置的映射类型:Readonly、Partial
interface Info {
age: number;
name: string;
sex: string;
}
type ReadonlyInfo = Readonly<Info>;
type SelectInfo = Partial<Info>;
(1)Pick
原来对象上的一部分属性名,组成的类名
// 源码实现
type Pick<T, K extends keyof T> = {
[P in K]: T[P]
}
使用的时候,参数1为对象,参数2为这个对象的部分属性列表。返回的结果是这个对象的部分属性。
interface Info {
name: string;
age: number;
sex: string;
}
const info: Info = {
name: 'dylan',
age: 18,
sex: 'man'
}
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
const res: any = {};
keys.map(key => {
res[key] = obj[key];
});
return res;
}
const nameAndAge = pick(info, ['name', 'age']);
console.log(nameAndAge); // // { name: 'dylan', age: 18 }
- 泛型类型 T、K:K继承T的参数名(key)
- 参数1(obj):类型T
- 参数2(keys):K (T的参数名),组成的数组。K[]
- Pick<T, K>:返回原先 T 上的部分参数
(2)Record
将对象中的每一个属性转换为其他值
// 源码实现
type Record<K extends keyof any, T> = {
[P in K]: T;
};
实际使用:
function mapObject<K extends string | number, T, U>(obj: Record<K, T>, fn: (x: T) => U): Record<K, U> {
const res: any = {};
for(const key in obj) {
res[key] = fn(obj[key]);
}
return res;
}
- 传入三个泛型:K(继承 string | number)、T、U
- 参数1(obj):Record<K, T>
- obj 这个对象中的
key类型 需要从 K 中取,也就是string | number
- obj 这个对象中的
- 参数2:回调函数 fn
我们来调用一下上面的函数:
const names = { 0: 'hello', 1: 'world', 2: 'bye', 'haha': '123' };
const lengths = mapObject(names, s => s.length);
console.log(lengths); // { 0: 5, 1: 5, 2: 3, haha: 3 }
最后的返回值类型(Record<K, U>),代表返回一个对象,对象的属性名需要是 names 的属性名中的一个,对象的 属性值 需要是回调函数 fn 的返回值类型。
同态:两个相同类型的代数结构之间的结构保持映射。
刚刚我们所讲的这四个映射类型中,
Readonly、Partial、Pick是同态的。
Record则不是同态的,因为Record映射出的属性值是一个新的,和输入值的属性值是不同的。
5、拆包
我们先来看一下包装的操作:
type Proxy<T> = {
get(): T;
set(value: T): void;
};
type Proxify<T> = {
[P in keyof T]: Proxy<T[P]>
};
Proxify 的属性名是传入对象的属性名,属性值是包裹了一层 Proxy 的传入对象属性值。
现在我们来定义一个函数:
function proxify<T>(obj: T): Proxify<T> {
const result = {} as Proxify<T>;
for(const key in obj) {
result[key] = {
get: () => obj[key],
set: (value) => obj[key] = value,
};
}
return result;
}
- 传入一个对象,返回值是被 Proxify 处理过的对象
- result 的每一个属性值都变成了一个对象
定义完函数之后,我们来定义一个对象:
let props = {
name: 'dylan',
age: 18
};
let proxyProps = proxify(props);
console.log(proxyProps); // { name: get set, age: get set }
console.log(proxyProps.name.get()); // dylan
这时可以看到,proxyProps 上面的每一个属性都变成了一个对象,每个对象上面都有 get 和 set 方法。
接下来,我们基于上述代码,来进行拆包:
function unproxify<T>(t: Proxify<T>): T {
const result = {} as T;
for(const k in t) {
result[k] = t[k].get();
}
return result;
}
console.log(unproxify(proxyProps)); // {name: 'dylan', age: 18}