logo

TypeScript的一些实用功能

486
2023年09月22日
一些高级构造可能需要一段学习曲线,但可以显著增强你的类型安全性。本文将向你介绍其中的一些高级特性。

如今,很难想象一个严肃的基于 JavaScript 的应用程序没有 TypeScript 的支持。接口、元组、泛型和其他特性在 TypeScript 开发者中广为人知。虽然一些高级构造可能需要一定的学习曲线,但它们可以显著增强类型安全性。本文旨在向您介绍一些这些高级特性。

类型守卫

类型守卫帮助我们在条件块内获取有关类型的信息。有一些简单的方法可以使用 intypeofinstanceof 运算符进行类型检查,或者使用相等比较 (===)。

在这一部分,我想更加关注用户定义的类型守卫。这种守卫充当一个返回布尔值的简单函数。换句话说,返回值是一个类型谓词。让我们来看一个例子,当我们有基本用户信息和带有额外细节的用户时:

type User = { name: string };
type DetailedUser = { 
  name: string; 
  profile: { 
    birthday: string
  }
}

function isDetailedUser(user: User | DetailedUser) {
  return 'profile' in user;
}

function showDetails(user: User | DetailedUser) {
if (isDetailedUser(user)) {
  console.log(user.profile); // 错误:类型 'User | DetailedUser' 上不存在属性 'profile'。
          }
}

isDetailedUser 函数返回一个布尔值,但它并没有将这个函数标识为“定义对象类型的布尔值”。

为了实现期望的结果,我们需要稍微更新 isDetailedUser 函数,使用 “user is DetailedUser” 结构:

function isDetailedUser(user: User | DetailedUser): user is DetailedUser {
  return 'profile' in user;
}

索引访问类型

在您的应用程序中可能会出现这样的情况:您有一个大型对象类型,想要创建一个新类型,该类型使用原始类型的一部分。例如,我们的应用程序的一部分需要仅用户配置文件。User['profile'] 提取所需的类型并将其分配给 UserProfile 类型。

type User = {
  id: string;
  name: string;
  surname: string;
  profile: {
    birthday: string;
  }
}

type UserProfile = User['profile'];

如果我们想基于几个属性创建一个类型会怎样?在这种情况下,您可以使用一个名为 Pick 的内置类型。

type FullName = Pick<User, 'name' | 'surname'>; // { name: string; surname: string }

还有许多其他实用类型,比如 OmitExcludeExtract,它们可能对您的应用程序有所帮助。乍一看,它们都是索引类型,但实际上它们都是基于 Mapped 类型构建的。

带有数组的索引类型

您可能遇到过这样的情况,应用程序为您提供了一个联合类型,例如:

type UserRoleType = ‘admin’ | ‘user’ | ‘newcomer’;

然后,在应用程序的另一部分,我们获取用户数据并检查其角色。对于这种情况,我们需要创建一个数组:

const ROLES: UserRoleType[] = [‘admin’, ‘user’, ‘newcomer’];
ROLES.includes(response.user_role);

看起来很累人,不是吗?我们需要在数组内重复联合类型的值。很好的是,索引类型在这里也有帮助。

首先,我们需要使用 const 断言声明我们的数组,以消除重复并创建一个只读元组。

const ROLES = [‘admin’, ‘user’, ‘newcomer’] as const;

然后,使用 typeof 运算符和 number 类型,我们基于数组值创建一个联合类型。

type RolesType = typeof ROLES[number]; // ‘admin’ | ‘user’ | ‘newcomer’;

您可能对这个解决方案感到困惑,但您可能知道,数组是基于数字键的对象构造。这就是为什么在这个例子中,number 被用作索引访问类型。

条件类型和 Infer 关键字

条件类型定义一个依赖于条件的类型。通常,它们与泛型一起使用。根据泛型类型(输入类型),构造选择输出类型。

例如,内置的 NonNullable TypeScript 类型是基于条件类型构建的。

type NonNullable<T> = T extends null | undefined ? never : T
type One = NonNullable<number>; // number
type Two = NonNullable<undefined>; // never

infer 关键字与条件类型一起使用,不能在‘extends’子句之外使用。它充当‘类型变量创建者’。

我认为通过看一个真实的例子,您会更容易理解它。

案例:检索异步函数的结果类型。

const fetchUser = (): Promise<{ name: string }> => { /* implementation */ }

最简单的解决方案是导入类型声明并将其分配给变量。不幸的是,有些情况下结果声明是写在函数内部的,就像上面的例子一样。

这个问题可以通过两个步骤 解决:

  1. Awaited 实用类型在 TypeScript 4.5 中引入。为了学习目的,让我们看一个简化的变体。
export type Awaited<T> = T extends Promise<infer U> ? U : T;

使用条件类型和 infer 关键字,我们“提取”了承诺的类型并将其分配给 U 名称。这是一种类型变量声明。如果传递的类型与 PromiseLike 泛型兼容,构造将返回保存到 U 名称的原始类型。

  1. 从异步函数获取值。

使用内置的 ReturnType 提取函数的返回类型和我们的 Awaited 类型,我们实现了期望的结果:

export type Awaited ReturnType<T> = Awaited<Return Type<T>>;
本文链接:https://www.iokks.com/art/f3317a1b159a
本博客所有文章除特别声明外,均采用CC BY 4.0 CN协议 许可协议。转载请注明出处!