遍历对象
在js中,我们通常使用 Object.keys()来遍历对象,例如:
const person = {
name:"xxx",
age:10
}
Object.keys(person).forEach(key => {
console.log(person[key]);
})
但是到了ts中这就不好使了,上面的代码会得到如下的错误:
原因是
Object.keys这个api返回的是一个 string[],进而导致 key 被推断为string,而 person 的键的类型是"name"|"age",因此 ts 便抱怨不能遍历。
解决办法是用一个通用函数来遍历
export const each = <T extends Record<string, any>>(
target: T,
cb: (key: keyof T) => void
) => {
Object.keys(target).forEach((key) => cb(key as keyof T));
};
通过这个函数我们把 key 断言为对象的键的类型,此时我们再调用就不会报错了:
同时在我们键入引号的时候也会有优化的提示。
过滤数组
type Person = {
name: string;
hobby?: string;
};
const list: Person[] = [
{
name: "张三",
hobby: "篮球",
},
{
name: "李四",
hobby: "足球",
},
{
name: "王五",
},
];
假设我们有一个Person的列表,我们想得到这个列表中的所有人的爱好组成的数组,很自然地,我们可以写出下面的代码:
const hobbies = list.map((i) => i.hobby).filter((i) => i);
我们先通过map返回了hobby,接着我们又用filter过滤掉了无效的值,所以理论上hobbies的类型应该是string[]; 然而,事实是hobbies的类型是(string|undefined)[]
原因在于hobby是一个可选的属性,所以它的类型是string|undefined,而filter函数的签名是
filter(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: any): T[];
它的返回值和输入的值是一样的类型,所以尽管我们能保证hobbies一定是一个string[],但是ts并不能分析到这个信息,因此只能推断为(string | undefined)[]。
那么如何解决这个问题呢?一个不好的办法是为 hobby 增加非空断言,进而断言hobby一定为string;
const hobbies = list.map((i) => i.hobby!).filter((i) => i);
然而这不太优雅,因hobby毕竟是有可能是undefined的,那么更好的解决方法是什么呢?
答案是自定义类型守卫。
const isNonNullable = <T>(val: T): val is NonNullable<T> => val != null;
const hobbies = list.map((i) => i.hobby!).filter(isNonNullable);
我们通过一个通用的类型守卫,将 null 和undefined 排除掉,并且断言为非空的类型,这样一来ts就能知道filter函数返回的是一个非空值,继而判断出hobbies是一个string[]类型;
这一切生效的原因在于,filter 方法还有另外一个重载签名
filter<S extends T>(predicate: (value: T, index: number, array: T[]) => value is S, thisArg?: any): S[];
它支持我们传入一个自定义的类型守卫,以帮助filter方法更准确地推断出类型;
让类型里的属性一目了然
type A = {
a: number;
aa: string;
};
type B = {
b: string;
bb: boolean;
};
type C = {
c: boolean;
cc: string;
};
假设我们有三个类型A,B,C,他们的类型如上,现在需要定义一个类型,它包括A中的a,B中的b和C中的c,这个需求非常简单,代码实现如下:
type D = A & Pick<B, "b"> & Pick<C, "c">;
然而,当我们鼠标移动到D上面的时候,显示的类型却为A & Pick<B, "b"> & Pick<C, "c">
这个可读性不是一般的差,我们也没法一目了然地知晓D的形状, 之所以这样是因为ts对类型的处理是懒处理的,所以需要显示地触发一下。
type Simplify<T> = { [Key in keyof T]: T[Key] };
type D = Simplify<A & Pick<B, "b"> & Pick<C, "c">>;
{ [Key in keyof T]: T[Key] }由于去重新构造了一个类型,故而触发了类型的计算,我们将其封装为一个 Simpify 的类型,便可以通过它查看到类型的具体形状。
让类型部分属性变成可选值
type Person = {
name: string;
age: number;
hobby: string;
address: string;
};
Person 类型是有四个必填属性,现在需要将hobby 和address 属性变成可选的,怎么做呢?
ts内置了一个类型Partial, 它能让一个对象的所有属性变成可选的
生成的类型所有参数变成了可选的,而这不符合我们的需求。 这个时候就需要结合Pick和Omit类型来构造我们需要的类型了
type PartialPerson = Partial<Pick<Person, "hobby" | "address">> & Omit<Person, "hobby" | "address">;
翻译一下就是 & 左边的类型是 取出Person 中的 hobby address字段并且将它们设置为可选的,即{hobby?:string|undefined},右边的类型是Person 中除了hobby 和address 之外的所有属性组成的类型,二者再做一个联合操作就能得到我们所需要的类型,为了更清晰地显示出来,我们用Simplify 包一下
由于整套逻辑都是可以复用的,所以我们可以抽离出一个公共的类型方法
type PartialWithKeys<T, K extends keyof T> = Simplify<Omit<T, K> & Partial<Pick<T, K>>>;
// 调用时
type PartialPerson = PartialWithKeys<Person, "hobby" | "address">;
善用函数重载
假设我们现在有一个函数double,如果是number类型就返回参数的两倍,如果是参数是string就用xxx,xxx这样的字符串。
function double(x: number | string): number | string {
if (typeof x === "number") {
return x * 2;
} else {
return x + ", " + x;
}
}
这个函数的实现没有任何问题,但是我们像这样console.log(double("").length);使用的时候弊端却来了
原因在于ts根据函数的签名只能知道返回值是number|string,所以在这里会报错;
解决办法是用函数重载改造:
function double(x: string): string;
function double(x: number): number;
function double(x: number | string): number | string {
if (typeof x === "number") {
return x * 2;
} else {
return x + ", " + x;
}
}
函数重载包括两个声明和实现两个部分,在上面的代码中,前两个就是函数的声明,而后一个就是函数的实现,这样一来,我们去调用的时候就不会出问题了:
除此之外,还可以为不同的签名加上不同的注释
/**
* 返回类似"x,x"的字符串
* @param x string
*/
function double(x: string): string;
/**
* 返回x的两倍
* @param x number
*/
function double(x: number): number;
function double(x: number | string): number | string {
if (typeof x === "number") {
return x * 2;
} else {
return x + ", " + x;
}
}
这样一来调用不同类型的参数时就会有不同的提示
总结
以上是5个实用的TypeScript小技巧,希望对大家有帮助,喜欢的话可以点个赞!