Typescript 体操入门

50 阅读3分钟

引言

TypeScript,作为JavaScript的超集,通过引入静态类型系统,旨在提高代码的可读性和可维护性。其类型系统的强大之处不仅在于能够准确地注解和推断变量的类型,还在于它提供了一系列高级工具类型,使得开发者可以在类型层面进行编程,执行所谓的"类型体操"。

基础动作

Partial

Partial<Type> 是TypeScript提供的一个工具类型,它将某个类型里的所有属性变为可选的。这在处理不完全对象时非常有用。比如,在函数参数或者设置默认值时,我们不需要提供所有属性。

interface Todo {
  title: string;
  description: string;
}

function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>): Todo {
  return { ...todo, ...fieldsToUpdate };
}

Required

Partial相反,Required<Type> 将类型的所有属性从可选转为必选。这对确保对象满足特定结构特别有用,尤其是在处理不允许省略任何属性的场景下。

typescriptCopy code
interface Props {
  a?: number;
  b?: string;
}

// 下面的代码会报错,因为属性'b'是必须的
const obj: Required<Props> = { a: 5 };

Readonly

Readonly<Type>将类型的所有属性设置为只读,这意味着一旦对象被创建,其属性就不能被修改。这对于创建不可变数据非常有用。

typescriptCopy code
interface Todo {
  title: string;
}

const todo: Readonly<Todo> = {
  title: "Delete inactive users",
};

// 下面的代码会报错,因为'title'属性是只读的
todo.title = "Hello";

Pick<Type, Keys>

Pick<Type, Keys>从类型中选取一组属性来构造一个新类型。这对于限制对象只包含某些属性非常有用,可以看作是对类型进行裁剪。

typescriptCopy code
interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type TodoPreview = Pick<Todo, "title" | "completed">;

const todo: TodoPreview = {
  title: "Clean room",
  completed: false,
};

Omit<Type, Keys>

Omit<Type, Keys>Pick相反,它从类型中排除一组属性。这在需要从对象类型中删除某些属性时非常有用。

typescriptCopy code
interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

// 移除'description'属性
type TodoPreview = Omit<Todo, "description">;

高级挑战

让我们通过一些具体的例子来深入理解前面提到的几个TypeScript类型挑战:

可串联构造器(Chainable)

typescriptCopy code
type Chainable<T = {}> = {
  option<K extends string, V>(key: K extends keyof T ? never : K, value: V): Chainable<T & Record<K, V>>;
  get(): T;
};

// 使用例子
const config: Chainable = {};
const result = config
  .option('foo', 123)
  .option('name', 'TypeScript')
  .option('bar', { value: 'Hello World' })
  .get();

// result 的类型为:
// {
//   foo: number;
//   name: string;
//   bar: { value: string; }
// }

在这个例子中,Chainable类型使用泛型T来累积之前调用option方法时传入的键值对。每次调用option时,都会返回一个新的Chainable类型,这个类型中包含了所有之前添加的键值对。

元组转合集(TupleToUnion)

typescriptCopy code
type TupleToUnion<T> = T extends (infer U)[] ? U : never;

// 使用例子
type TestTuple = ['1', '2', '3'];
type Result = TupleToUnion<TestTuple>; // '1' | '2' | '3'

这里,TupleToUnion类型通过条件类型和infer关键字来推断元组中的元素类型,并将其转换为一个联合类型。

最后一个元素(Last)

typescriptCopy code
type Last<T extends any[]> = T extends [...infer _, infer L] ? L : never;

// 使用例子
type TestArray = [1, 2, 3, 4];
type Result = Last<TestArray>; // 4

在这个例子中,Last类型使用条件类型和剩余参数语法来匹配元组的最后一个元素,并推断出其类型。

出堆(Pop)

typescriptCopy code
type Pop<T extends any[]> = T extends [...infer Rest, infer _] ? Rest : never;

// 使用例子
type TestArray = [1, 2, 3, 4];
type Result = Pop<TestArray>; // [1, 2, 3]

这里的Pop类型利用了和Last类似的技巧,但是它返回的是除了最后一个元素之外的所有元素组成的数组类型。

Promise.all的类型实现(PromiseAll)

typescriptCopy code
declare function PromiseAll<T extends any[]>(values: readonly [...T]): Promise<{ [K in keyof T]: T[K] extends Promise<infer R> ? R : T[K] }>;

// 使用例子
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise<string>((resolve) => resolve('Hello World'));

const result = PromiseAll([promise1, promise2, promise3]);
// result 的类型为 Promise<[number, number, string]>

PromiseAll类型实现模拟了Promise.all的行为,通过条件类型和infer关键字来推断并处理数组中每个Promise的解析结果类型。