整理一些好用的TS写法

1,356 阅读6分钟

1. 工具类型

Partial

所有的字段都会被成可选的

interface AppConfig {
  title: string;
  version: number;
  theme: 'light' | 'dark';
}

type PartialConfig = Partial<AppConfig>;
// Equivalent to: { title?: string; version?: number; theme?: 'light' | 'dark'; }

Partial 跟我们 clone 对象一样,只会对外层进行可选处理,它无法处理嵌套类型

interface App {
  config: AppConfig;
  size: number;
  createTime: string;
}

// Equivalent to: { config?: Partial<AppConfig>; size?: number; createTime?: string; }
const app :Partial<App> = {
    title: 'App Title',
  },
  size: 100,
  createTime: '2023-10-01',
}

此时 TS 会报错:error: config: {Type '{ title: string; }' is missing the following properties from type 'AppConfig': version, theme.

但我们可以借助 keyof 实现 DeepPartial,从而解决这个问题:

type DeepPartial<T> = T extends object
  ? {
      [P in keyof T]?: DeepPartial<T[P]>;
    }
  : T;
  
// Equivalent to: { config?: Partial<AppConfig>; size?: number; createTime?: string; }
const newApp : DeepPartial<App> = {
  config: {
    title: 'App Title',
  },
  size: 100,
  createTime: '2023-10-01',
}
Pick

从一个类型中选中指定属性

interface AppConfig {
  title: string;
  version: number;
  theme: 'light' | 'dark';
}

type TitleConfig = Pick<AppConfig, 'title' | 'version'>;
// Equivalent to: { title: string; version: number; }
Omit

从一个类型中排除指定属性

interface AppConfig {
  title: string;
  version: number;
  theme: 'light' | 'dark';
}

type WithoutTheme = Omit<AppConfig, 'theme'>;
// Equivalent to: { title: string; version: number; }
Readonly

将所有属性变为只读

interface AppConfig {
  title: string;
  version: number;
  theme: 'light' | 'dark';
}

type ReadonlyConfig = Readonly<AppConfig>;
// Equivalent to: { readonly title: string; readonly version: number; readonly theme: 'light' | 'dark'; }
Required

将所有属性变为必选

type RequiredConfig = Required<{
  description?: string | null;
}>;
// Equivalent to: { description: string | null; }

这里要注意:

description?: string | nulldescription: string | null | undefined 是不一样的。

前一个代表 description 是可选的,后一个代表 description 的值可能是 undefined

Required 只会移除 ?, 而不会去处理 undefined

type RequiredConfig = Required<{
  description: string | null | undefined;
}>;
// Equivalent to: {  description: string | null | undefined; }
Record

创建一个对象类型,键为指定的类型,值为指定的类型

type RecordConfig = Record<'vueApp' | 'ngApp', AppConfig>;
// Equivalent to: { vueApp: AppConfig; ngApp: AppConfig; }
Exclude

从联合类型中排除指定的类型

type ExcludeConfig = Exclude<'a' | 'b' | 'c', 'b' | 'd'>;
// Equivalent to: 'a' | 'c'
Extract

从联合类型中提取交集的类型

type ExtractConfig = Extract<'a' | 'b' | 'c', 'b' | 'd'>;
// Equivalent to: 'b'
NonNullable:

从联合类型中排除 null 和 undefined

type NonNullableUnitType = NonNullable<string | null | undefined>;
// Equivalent to: string

NonNullable 与上面介绍 Required 时有一样的问题, 但不同的是Required 只会移除 ?, 而不会去处理 undefined,而 NonNullable 正好相反,它只会移除 undefined 而不会去处理 ?

并且它无法对对象类型进行非空处理

type NonNullableType = NonNullable<{
  description: string | null | undefined;
}>;
// 你预期的结果: { description: string; }
// 实际结果: { description: string | null | undefined; }

我们可以基于 NonNullable Map一个新的工具类型,实现上述需求

type NonNullablePlus<T> = {
  // 这里的 -? 用来处理可选符号 `?`
  [K in keyof T]-?: NonNullable<T[K]>;
};

type NonNullableType = NonNullablePlus<{
  description?: string | null | undefined;
}>;
// Equivalent to: { description: string; }
Parameters

提取函数参数类型, 返回数组

declare function f1(arg: { a: number; b: string }, arg2: string): void;
type F1Parameters = Parameters<typeof f1>;
// Equivalent to: [arg: { a: number; b: string; }, arg2: string]

type T1 = Parameters<() => void>;
// Equivalent to: []
ConstructorParameters

提取类的构造函数参数, 返回数组

class C {
  constructor(a: number, b: string) {}
}
type TCConstructorParameters = ConstructorParameters<typeof C>;
// Equivalent to: [a: number, b: string]
ReturnType

提取函数返回值类型

declare function f1(): { a: number; b: string };;
type F1ReturnType = ReturnType<typeof f1>;
// Equivalent to: { a: number; b: string; }
InstanceType

提取类的类型

class C {
  x = 0;
  y = 0;
  constructor(a: number, b: string) {}
}
type CType = InstanceType<typeof C>;
// Equivalent to: C
NoInfer

阻止对所含类型的推断。除了阻止推断之外,NoInfer<Type>Type 完全相同。

  1. 防止泛型类型推断
function createStreetLight<C extends string>(
  colors: C[],
  defaultColor?: NoInfer<C>,
) {
  // ...
}
createStreetLight(["red", "yellow"], "red");  // OK
createStreetLight(["red", "yellow", "green"], "blue");  // Error

这里,NoInfer<C> 确保 defaultColor 的类型必须显式匹配 C,而不会被推断为其他类型。

  1. 要求用户显式指定泛型类型
function useValue<T>(value: NoInfer<T>): T {
  return value;
}
const inferredValue = useValue("hello"); // unknown:类型推导被阻止,必须显式的指定类型
const explicitValue = useValue<string>("hello"); // string

// 不使用 NoInfer
function useValue<T>(value: T): T {
  return value;
}
const inferredValue = useValue("hello"); // hello,类型被推导为 hello

2. 模板字面量类型

它可以基于字符串模板创建类型。

Capitalize<StringType> 可以将字符串字面量类型的第一个字符转换为大写。

type Event = 'click' | 'hover';
type EventHandler = `on${Capitalize<Event>}`;

const handler: EventHandler = 'onClick'; // 或 'onHover'

除了 Capitalize 外,还有其它几个

  • Uncapitalize<StringType> :将字符串字面量类型的第一个字符转换为小写。
  • Uppercase<StringType>:将字符串字面量类型的所有字符转换为大写。
  • Lowercase<StringType> :将字符串字面量类型的所有字符转换为小写。

你可以结合 keyof 自己创建一些更有用的工具类型

type UppercaseKeys<T> = {
  [K in keyof T as Uppercase<K & string>]: T[K];
};

type Example = UppercaseKeys<{ name: string; age: number }>;
// Equivalent to: { NAME: string; AGE: number }

3. keyof

3.1 获取对象类型的键

type Person = {
  name: string;
  age: number;
  isStudent: boolean;
};

type PersonKeys = keyof Person;
// Equivalent to: 'name' | 'age' | 'isStudent'

3.2 使用 keyof 限制键的类型

function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const person = { name: 'Alice', age: 25 };

const name = getValue(person, 'name'); // OK, 返回类型为 string
const age = getValue(person, 'age'); // OK, 返回类型为 number
// const invalid = getValue(person, 'height'); // Error: 类型 '"height"' 不可分配给参数

3.3 map 类型

可以用 Required 作为例子

/**
 * Make all properties in T required
 */
type Required<T> = {
    [P in keyof T]-?: T[P];
};

这里 -? 的作用是从属性中移除可选标记(?)。它的作用是将类型中的所有可选属性变为必选属性。

另外还记得开篇时写的 DeepPartial 吗? 除了 Partial 之外,我们还可以基于 keyof 实现 DeepRequired, NonNullable ,感兴趣的话可以自己试试看。

4. infer

它用于条件中的类型推导。

Typescript 官网拿 ReturnType 这一经典例子来演示它的作用

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

如果 T 继承了 (...args: any[]) => any 类型,则返回类型 R,否则返回 any。其中的 R 是从传入参数类型中推导出来的,infer相当于一个类型占位。

可以结合上面 ReturnType 中的例子来看

declare function f1(): { a: number; b: string };;
type F1ReturnType = ReturnType<typeof f1>;
// Equivalent to: { a: number; b: string; }

5. as const

as const 将一个对象或数组的所有属性变为 只读,并将其值的类型从宽泛的类型(如 stringnumber)缩小为更具体的类型(如 'value' 或 1)。

const data = { age: 1 };
type DataType =  typeof data;
// Equivalent to: { age: number; }

const dataConst = { age: 1 } as const;
type DataConstType =  typeof dataConst;
// Equivalent to: { readonly age: 1; }

它可以与 typeof 一起使用,从常量中提取更具体的类型。

const COLORS = {
  RED: 'red',
  GREEN: 'green',
  BLUE: 'bl?

type Color = typeof COLORS[keyof typeof COLORS];
// Equivalent to: 'red' | 'green' | 'blue'


const directions = ['up', 'down', 'left', 'right'] as const;

type Direction = typeof directions[number];
// Equivalent to: 'up' | 'down' | 'left' | 'right'

6. 泛型约束

通过 extends 关键字,可以对泛型进行约束,限制泛型的类型范围。

function getLength<T extends { length: number }>(arg: T): number {
  return arg.length;
}

getLength('Hello'); // OK, string 有 length 属性
getLength([1, 2, 3]); // OK, 数组有 length 属性
// getLength(42); // Error: number 没有 length 属性

7. 根据枚举值,动态生成类型

enum ShapeType {
    Square = 1,
    Circle = 2
}

interface Square {
    size: number;
}

interface Circle {
    radius: number;
}

type MutableRecord<T, U> = {
  [SubType in keyof T]: {
    [K in keyof U]: SubType;
  } & { [K in keyof T[SubType]]: T[SubType][K] };
}[keyof T];

type Shape = MutableRecord<
  {
    [ShapeType.Square]: Square;
    [ShapeType.Circle]: Circle;
  },
  { type: ShapeType }
>;

const circle: Shape = {
  type: ShapeType.Circle,
  radius: 1, // 这里只会出现 radius
};
const square: Shape = {
  type: ShapeType.Square,
  size: 1, // 这里只会出现 size
};


持续更新中...