5个实用的TypeScript 小技巧

389 阅读5分钟

遍历对象

js中,我们通常使用 Object.keys()来遍历对象,例如:

const person = {
  name:"xxx",
  age:10
}

Object.keys(person).forEach(key => {
  console.log(person[key]);
})

但是到了ts中这就不好使了,上面的代码会得到如下的错误:

image.png 原因是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 断言为对象的键的类型,此时我们再调用就不会报错了:

image.png

image.png

同时在我们键入引号的时候也会有优化的提示。

过滤数组

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)[]

image.png

原因在于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);

我们通过一个通用的类型守卫,将 nullundefined 排除掉,并且断言为非空的类型,这样一来ts就能知道filter函数返回的是一个非空值,继而判断出hobbies是一个string[]类型;

image.png

这一切生效的原因在于,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">

image.png

这个可读性不是一般的差,我们也没法一目了然地知晓D的形状, 之所以这样是因为ts对类型的处理是懒处理的,所以需要显示地触发一下。

type Simplify<T> = { [Key in keyof T]: T[Key] };

type D = Simplify<A & Pick<B, "b"> & Pick<C, "c">>;

image.png

{ [Key in keyof T]: T[Key] }由于去重新构造了一个类型,故而触发了类型的计算,我们将其封装为一个 Simpify 的类型,便可以通过它查看到类型的具体形状。

让类型部分属性变成可选值

type Person = {
  name: string;
  age: number;
  hobby: string;
  address: string;
};

Person 类型是有四个必填属性,现在需要将hobbyaddress 属性变成可选的,怎么做呢?

ts内置了一个类型Partial, 它能让一个对象的所有属性变成可选的

image.png

生成的类型所有参数变成了可选的,而这不符合我们的需求。 这个时候就需要结合PickOmit类型来构造我们需要的类型了

type PartialPerson = Partial<Pick<Person, "hobby" | "address">> & Omit<Person, "hobby" | "address">;

翻译一下就是 & 左边的类型是 取出Person 中的 hobby address字段并且将它们设置为可选的,即{hobby?:string|undefined},右边的类型是Person 中除了hobbyaddress 之外的所有属性组成的类型,二者再做一个联合操作就能得到我们所需要的类型,为了更清晰地显示出来,我们用Simplify 包一下

image.png

由于整套逻辑都是可以复用的,所以我们可以抽离出一个公共的类型方法

type PartialWithKeys<T, K extends keyof T> = Simplify<Omit<T, K> & Partial<Pick<T, K>>>;

// 调用时
type PartialPerson = PartialWithKeys<Person, "hobby" | "address">;

image.png

善用函数重载

假设我们现在有一个函数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);使用的时候弊端却来了

image.png

原因在于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;
  }
}

函数重载包括两个声明和实现两个部分,在上面的代码中,前两个就是函数的声明,而后一个就是函数的实现,这样一来,我们去调用的时候就不会出问题了:

image.png

image.png

除此之外,还可以为不同的签名加上不同的注释

/**
 * 返回类似"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;
  }
}

这样一来调用不同类型的参数时就会有不同的提示

image.png

image.png

总结

以上是5个实用的TypeScript小技巧,希望对大家有帮助,喜欢的话可以点个赞!