TypeScript工具类型体操:提升类型安全的强有力工具

117 阅读10分钟

TypeScript 是 JavaScript 的一个超集,添加了类型系统和对 ES6 及以后版本的支持。以其静态类型系统为开发者提供了一种更结构化、更安全的方式来编写代码。TypeScript的类型系统不仅包括基本类型,还提供了一套强大的工具类型(Utility Types)。

它们允许开发者以声明性的方式操作和构建复杂的类型结构。无论是Vue3 + Ts还是React + Ts,在我们平时进行项目开发时,灵活运用一些工具类型就会显得游刃有余。

本文将介绍一些常用的TypeScript工具类型,以及如何使用它们来提升代码的类型安全性。废话不多说,走你!

1.gif

Awaited<Type>

用于处理异步操作中 await 表达式的结果类型。当你对一个 Promise 或任何可以 await 的值使用 await 关键字时,Awaited<Type> 会展开这个 Promise 的解析类型。等待一个 Promise 解析并返回其结果。可以确保 await 表达式的结果总是被正确地类型化,从而提高代码的健壮性和可维护性。

  • 基本用法

当你 await 一个 PromiseThenable 对象时,Awaited<Type> 会提取出这个 Promise 的成功状态的类型。如果 Promise 可能解析为多种类型,Awaited<Type> 将尝试合并这些类型。

// 假设我们有一个异步函数,返回一个 Promise
async function fetchNumber(): Promise<number> {
  return 42;
}

// 使用 Awaited<Type> 来获取 await 表达式的结果类型
type A = Awaited<Promise<number | undefined>>;

// A 的类型是 number | undefined
  • 合并类型

如果 Promise 可能解析为多种类型,Awaited<Type> 会尝试合并这些类型。例如,如果 Promise 可能解析为 stringnullAwaited<Type> 将结果类型合并为 string | null

async function fetchStringOrNull(): Promise<string | null> {
  return "Hello"; // 或者可能返回 null
}

type A = Awaited<ReturnType<typeof fetchStringOrNull>>;

// A 的类型是 string | null
  • 与 TypeScript 的类型守卫结合使用

Awaited<Type> 可以与 TypeScript 的类型守卫结合使用,以在运行时确定 Promise 的具体类型。

async function fetchUser(): Promise<User | null> {
  try {
    // 假设这里有异步逻辑来获取用户
    return { id: 1, name: "Alice" };
  } catch {
    return null;
  }
}

// 使用类型守卫来处理 null 情况
async function getUserName(): Promise<string> {
  const user = await fetchUser();
  if (user === null) {
    throw new Error("User not found");
  }
  return user.name;
}

// 使用 Awaited<Type> 获取 getUserName 函数的返回类型
type A = Awaited<ReturnType<typeof getUserName>>;

// A 的类型是 string

Omit<Type,Keys>

用于从一个类型Type排除一组指定的键Keys,生成一个新的类型。这个工具类型非常有用,特别是在你需要创建一个新类型,这个新类型与原始类型类似,但缺少一些属性时。

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>

Omit<Type, Keys>接受两个参数:

  • Type:你想要修改的原始类型。
  • Keys:要排除的属性键的联合类型,可以是字符串字面量、数字字面量或symbol
interface Todo {
  title: string;
  description: string;
  completed: boolean;
  createdAt: number;
}

type TodoPreview = Omit<Todo, "description" | "createdAt">;
// {
//    title: string;
//    completed: boolean;
// }

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

Pick<Type,Keys>

用于从一个类型 Type挑选出一组指定的键 Keys,然后从原始类型中创建一个新的类型,这个新类型只包含这些被挑选的键及其对应的类型。

type Pick<T, K extends keyof T> = { [P in K]: T[P]; };

Pick<Type, Keys> 接受两个参数:

  • Type:你想要从中选择属性的原始类型。
  • Keys:要选择的属性键的联合类型,通常是字符串字面量、数字字面量或 symbol 类型的值的联合。
interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

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

// {
//   title: string;
//   completed: boolean;
// }

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

Partical<Type>

用于将一个类型 Type 转换成所有属性都是可选的类型。这意味着原始类型 Type 中的每个属性,在 Partial<Type> 中都可以不提供值。

type Partial<T> = { [K in keyof T]?: T[K]; };

Partial<Type> 接受一个参数:

  • Type:你想要使其属性变为可选的原始类型。
interface Todo {
  title: string;
  description: string;
}
type PTodo = Partial<Todo>;

// {
//     title?: string | undefined;
//     description?: string | undefined;
// }

const todo1: PTodo = {
  title: "organize desk",
};

练习: 实现Optional

使用Partical<Type>Pick<Type,Keys>Omit<Type,Keys> 实现Optional

interface Article {
  title: string
  content: string
  date: Date
  author: string
}
//当我创建一个文章时,我不想传递date、author,想使用默认值,于是我又需要实现下面的接口
// interface ArticleOptions {
//   title: string
//   content: string
//   date?: Date
//   author?: string
// }
//但是这两个接口出现了重复声明,现在我想使用Optional高级类型,实现ArticleOptions接口

//以下是实现
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>

type ArticleOptions = Optional<Article, "date" | "author">

ReturnType<Type>

ReturnType<T> 是一个内置的工具类型,用于获取函数类型<T>的返回值类型。它接受一个函数类型作为参数,并提取出该函数的返回值类型。

下面是 ReturnType 的详细解释:

type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : any
  • ReturnType 是一个泛型类型(generic type),使用 <T> 来表示。
  • T 表示被检索返回值类型的函数类型。使用 extends T 必须是一个函数类型。
  • 函数类型的签名是 (...args: any[]) => any,表示这个函数接受任意数量的参数,并且返回任意类型的值。
  • 使用条件类型(conditional type)进行检查和提取。
    • infer R 用于提取函数的返回值类型,并将其赋 类型变量。 args: any[]) => infer R ? R : any 表示如果 T 是一个函数类型,则返回 R 类型,否则返回 any 类型。

以下是使用 ReturnType 的示例:

function greet(): string {
  return "Hello!"
}

type GreetReturnType = ReturnType<typeof greet> // 类型推断: string
// 在这个示例中,我们使用 greet 函数,并通过 typeof greet 获取其函数类型。然后,我们将 typeof greet 作为参数传递给 ReturnType,以获得函数返回值的类型。

//ReturnType
type greetType = () => string
const greet: greetType = () => {
  return ",,,"
}

//这种写法使用 typeof 操作符来获取函数 greet 的类型,获取的就是greetType,也就是下面的第二种写法
type GreetReturnType = ReturnType<typeof greet>
type GreetReturnType = ReturnType<greetType>

ReturnType<typeof greet>ReturnType<greet>是有区别的:

ReturnType<greet>这种写法直接将 函数greet作为泛型参数 传递给 ReturnType,不使用 typeof 操作符。它表明你已知 greet是一个函数且想要获取其返回值类型,而不关心这个函数的具体类型。 在大多数情况下,它们会得到相同的结果

通过使用 ReturnType,我们可以更加灵活地操作和利用函数的返回值类型,例如进行类型检查、定义新的类型等。这个工具类型在处理函数相关的类型操作时非常有用。

Exclude<UnionType, ExcludedMembers>

Exclude<UnionType, ExcludedMembers>通过从 UnionType排除所有可分配给 ExcludedMembers 的联合成员来构造一个类型。

type Exclude<T, U> = T extends U ? never : T
  • TU 是泛型参数。
  • 如果 TU 的子类型(即可以赋值给 U),那么结果类型为 never
  • 如果 T 不是 U 的子类型,那么结果类型为 T

Exclude<T, U> 的作用是排除类型 T 中可以继承类型 U 的部分,返回剩余的类型。通常会用于条件类型(Conditional Types)中,根据条件排除一些类型。

type A = "a" | "b" | "c"
type B = "a" | "b"
type ExclusiveType = Exclude<A, B> // 'c'
// 由于 B 包含 'a' 和 'b',所以 ExclusiveType 将排除这些共同的部分,剩余的类型为 'c'。

Extract<Type, Union>

通过从 Type提取所有可分配给 Union 的联合成员来构造一个类型。

type Extract<T, U> = T extends U ? T : never;
  • T extends U ? T : never 是一个条件类型,它使用了 TypeScript 的条件类型守卫语法。
  • 如果 T 可以赋值给 U(即 TU 的一个成员),则结果类型是 T;否则,结果类型是 never,表示类型不兼容。
type T0 = Extract<"a" | "b" | "c", "a" | "f">;
//  type T0 = "a"
type T1 = Extract<string | number | (() => void), Function>;
//  type T1 = ()=>void

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; x: number }
  | { kind: "triangle"; x: number; y: number };

type T2 = Extract<Shape, { kind: "circle" }>
//  type T2 = { kind: "circle"; radius: number }

Record<Keys, Type>

用于创建具有指定属性和对应类型的对象类型。

type Record<K extends keyof any, T> = {
  [P in K]: T
}
  • K 是泛型参数,表示属性名的类型。
  • T 是泛型参数,表示属性值的类型。
  • { [P in K]: T; } 是通过映射类型将每个属性名 K 映射到对应的属性值类型 T

通过使用 Record 类型工具,我们可以快速创建包含特定属性和对应类型的对象类型。以下是一个示例:

type Person = {
  name: string
  age: number
}

const myObj: Record<"key1" | "key2" | "key3", Person> = {
  key1: { name: "Alice", age: 25 },
  key2: { name: "Bob", age: 30 },
  key3: { name: "Charlie", age: 35 },
}
//这样的操作使得 myObj 对象具有固定的键集合,并且可以确保每个键对应的属性值类型符合 Person 类型的定义。

//示例二
const layouModules: any = import.meta.glob("../layout/routerView/*.{vue,tsx}")
const viewsModules: any = import.meta.glob("../views/**/*.{vue,tsx}")
const dynamicViewsModules: Record<string, Function> = Object.assign(
  {},
  { ...layouModules },
  { ...viewsModules }
)

Readonly<Type>

Readonly<T>: 创建一个新类型,其属性都变为只读的。

interface Todo {
  title: string;
}

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

todo.title = "Hello";//❌ Cannot assign to 'title' because it is a read-only property.

Required<Type>

Required<T>: 创建一个新类型,其属性都变为必需的。

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

const obj: Props = { a: 5 };

const obj2: Required<Props> = { a: 5 }; //❌  Property 'b' is missing in type '{ a: number; }' but required in type 'Required<Props>'.

Parameters<Type>

从函数类型 Type参数中使用的类型构造元组类型

用于获取一个类型参数的详细信息。它通常用于在泛型中获取参数列表,以便在运行时进行类型检查。

declare function f1(arg: { a: number; b: string }): void;
 
type T0 = Parameters<() => string>;     
	//type T0 = []

type T1 = Parameters<(s: string) => void>;
	//type T1 = [s: string]

type T2 = Parameters<<T>(arg: T) => T>;
    //type T2 = [arg: unknown]

type T3 = Parameters<typeof f1>;
     //type T3 = [arg: {
        //a: number;
        //b: string;
    //}]
                     
type T4 = Parameters<any>;
     //type T4 = unknown[]
                     
type T5 = Parameters<never>;
     //type T5 = never
                     
type T6 = Parameters<string>;
	//error: Type 'string' does not satisfy the constraint '(...args: any) => any'.
	//type T6 = never
                     
type T7 = Parameters<Function>;                     
	//Type 'Function' does not satisfy the constraint '(...args: any) => any'.
	//Type 'Function' provides no match for the signature '(...args: any): any'.
    //type T7 = never

InstanceType<Type>

用于获取一个构造函数或工厂函数类型 Type 的实例类型。当你有一个类或构造函数,并希望获取该类或构造函数创建的对象的类型时,这个工具类型就非常有用。

InstanceType<Type> 接受一个参数:

  • Type:一个构造函数类型,通常是类类型或具有 new() 调用签名的函数类型。
type InstanceType<T extends new (...args: any[]) => any> = T extends new (...args: any[]) => infer R ? R : never;
  • T extends new (...args: any[]) => any 表示 Type 是一个构造函数。
  • infer R 是一个类型推断,用于获取 new (...args: any[]) => RR 的类型。
  • 如果 T 是一个构造函数,InstanceType 将返回构造函数的实例类型;否则,返回 never

示例:

//类
class User {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

//使用 `InstanceType` 来获取 `User` 类的实例类型:
type UserInstance = InstanceType<typeof User>; 
// UserInstance 的类型是 { name: string; age: number; }



// 泛型构造函数
// 当构造函数是泛型的,`InstanceType` 也可以正确工作:

class GenericClass<T> {
  value: T;
  constructor(value: T) {
    this.value = value;
  }
}


type GenericInstance = InstanceType<typeof GenericClass>;
// GenericInstance 的类型是 new <T>(value: T) => { value: T; }  // GenericClass<unknown>

再比如说vue中,当我们在自定义组件上使用ref时,给其组件标注类型:

<template>
  <NoticeDetail ref="noticeRef" @update-page="getPage"></NoticeDetail>
</template>

<script setup lang="ts">
import { onMounted, ref } from "vue";

import NoticeDetail from "@/components/NoticeDetail/index.vue";

defineOptions({ name: "Notice" });
//组件类型标注
const noticeRef = ref<InstanceType<typeof NoticeDetail>>();

</script>

总结

TypeScript的工具类型是构建健壮类型系统的强大工具。它们不仅提高了代码的可读性和可维护性,而且在编译时就能帮助开发者发现潜在的错误。通过合理使用这些工具类型,我们可以编写出更加安全、可靠的应用程序。

随着TypeScript的不断发展,其类型系统也在不断丰富,为开发者提供了更多的类型操作选项。以上列举的并不是全部的工具类型,但是基本上是我们开发过程中可能需要用到的。

❤今天的分享就到这里,希望可以帮助到你!假如你对文章感兴趣,可以来我的公众号:小新学研社。

6.gif