精通 TypeScript:创建自定义类型与 AI 赋能的最佳实践

本文深入探讨了如何在 TypeScript 中创建和使用自定义类型,从基础语法、可选属性到高级技巧如联合类型、交叉类型和工具类型。更进一步,文章详细介绍了如何利用 GitHub Copilot 和 Codeium 等 AI 工具大规模地定义、重构和强制执行类型安全,并提供了项目配置、代码生成和治理的最佳实践。

阅读时长: 10 分钟
共 4607字
作者: eimoon.com

引言

TypeScript 是 JavaScript 的一个超集,它在 JavaScript 的运行时基础上增加了一个编译时类型检查器。这种结合使得开发者可以利用完整的 JavaScript 生态系统和语言特性,同时还能获得可选的静态类型检查、枚举(enums)、类(classes)和接口(interfaces)等强大功能。

尽管 TypeScript 内置的基础类型足以应对许多场景,但通过创建自定义类型,你可以确保类型检查器能够验证项目特定的数据结构。这不仅能减少 Bug,还能更好地文档化整个代码库中使用的数据结构。

本教程将向你展示如何使用 TypeScript 创建自定义类型,如何通过联合(unions)和交叉(intersections)组合它们,以及如何利用工具类型(utility types)增加自定义类型的灵活性。此外,我们还将探讨如何利用 GitHub Copilot 和 Codeium 等现代 AI 编程助手,来大规模地加速类型的定义和实施,并通过严格的编译器选项、运行时验证、代码生成和 CI 自动化,在大型项目中可靠地维护类型安全。

核心要点:

  • 自定义类型:使用 type 关键字定义对象结构,通过 ? 添加可选属性,并使用索引签名或 Record 创建灵活的键值映射。
  • 组合类型:利用联合类型 (|) 和交叉类型 (&) 创建强大的组合,并通过可辨识联合(discriminated unions)和 never 检查在 switch 语句中实现穷尽性检查。
  • 数组与元组:使用带 rest 元素的元组(如 [string, string, ...string[]])来定义“至少包含 N 个元素”的数组。
  • 模板字面量类型:使用反引号(backtick)创建类型,以约束字符串的格式(如 ``type Id = `user_${number}```)。
  • 类型断言与类型收窄:优先使用类型守卫(value is T)、in / instanceof 和控制流分析,而不是 as 断言。
  • 工具类型:应用 Record, Pick, Omit, 和 Partial 等工具类型,无需重写即可转换类型结构。
  • AI 赋能 TypeScript:利用 GitHub Copilot 和 Codeium 从 JSON 生成类型、建议可辨识联合,并重构以实现更严格的类型。结合 zod 等库进行运行时验证,确保边界数据的类型安全。
  • 代码生成管道:从 OpenAPI/GraphQL schema 自动生成并版本化 API 类型,并在生成文件与源文件不一致时阻止代码合并。
  • 项目配置:启用 TypeScript 的严格选项(如 strict, noImplicitAny, strictNullChecks),并使用 typescript-eslint 规则来强制代码规范。
  • 治理与隐私:建立 AI 使用的治理清单和配置清单(YAML manifest),限制 prompt 中包含敏感信息(如密钥/个人身份信息 PII)。
  • 自动化与 CI:将 tsc --noEmit 和 ESLint 集成到持续集成(CI)流程中,并使用 pre-commit 钩子来保持类型定义的同步。

前提条件

要顺利学习本教程,你需要:

  • 一个可以执行 TypeScript 程序的环境。你可以在本地机器上配置,需要安装 Node.js 和 npm(或 yarn),以及 TypeScript 编译器(tsc)。
  • 如果你不想在本地配置环境,可以使用官方的 TypeScript Playground 在线练习。
  • 熟悉 JavaScript,特别是 ES6+ 语法,如解构、rest 操作符和模块导入/导出。
  • (可选)为了获得更好的开发体验,建议使用支持 TypeScript 的代码编辑器,如 Visual Studio Code,并安装 AI 编程助手插件,如 GitHub Copilot 或 Codeium。

创建自定义类型

当程序处理复杂数据结构时,仅使用 TypeScript 的基础类型可能不足以精确描述。此时,声明自定义类型将非常有帮助。本节将教你如何创建能够描述代码中任何对象结构的类型。

自定义类型语法

在 TypeScript 中,创建自定义类型的语法是使用 type 关键字,后跟类型名称,然后赋值为一个包含类型属性的 {} 代码块。例如:

type Programmer = {
  name: string;
  knownFor: string[];
};

这个语法类似于对象字面量,其中键是属性名,值是该属性应有的类型。这里定义了一个 Programmer 类型,它必须是一个对象,包含一个 string 类型的 name 属性和一个字符串数组类型的 knownFor 属性。

属性之间的分隔符可以是分号 ;、逗号 ,,也可以省略。

使用自定义类型与使用基础类型的方式相同,在变量名后加上冒号和类型名即可:

type Programmer = {
  name: string;
  knownFor: string[];
};

const ada: Programmer = {
  name: 'Ada Lovelace',
  knownFor: ['Mathematics', 'Computing', 'First Programmer']
};

ada 常量现在可以通过类型检查。如果为属性赋了错误类型的值、遗漏了必需的属性或添加了未定义的属性,TypeScript 编译器都会报错。

嵌套自定义类型

自定义类型可以相互嵌套。假设你有一个 Company 类型,其 manager 字段的类型是另一个自定义类型 Person

type Person = {
  name: string;
};

type Company = {
  name: string;
  manager: Person;
};

你可以这样创建一个 Company 类型的值:

const manager: Person = {
  name: 'John Doe',
}

const company: Company = {
  name: 'ACME',
  manager,
}

TypeScript 的类型系统是基于结构(structural typing)的,这意味着只要一个对象的“形状”符合类型的要求,它就是兼容的,即使没有显式声明类型。因此,你也可以直接在 company 对象中定义 manager

const company: Company = {
  name: 'ACME',
  manager: {
    name: 'John Doe'
  }
};

可选属性

有时,某些属性不是必需的。要将一个属性标记为可选,可以在属性名后添加 ? 修饰符。

type Programmer = {
  name: string;
  knownFor?: string[];
};

现在 knownFor 属性是可选的,即使省略它,代码也能通过类型检查:

const ada: Programmer = {
  name: 'Ada Lovelace'
};

可索引类型

如果你需要创建一个可以包含任意数量属性的类型,只要这些属性遵循特定的类型签名,就可以使用可索引类型(Indexable Types)。

假设你需要一个 Data 类型,它可以容纳任意数量、任意类型的属性:

type Data = {
  [key: string]: any;
};

这里的 [key: string]: any 就是索引签名(index signature),它规定了对象的键必须是 string 类型,而值可以是 any 类型。

const someData: Data = {
  someBooleanKey: true,
  someStringKey: 'text goes here'
  // ...可以添加更多属性
}

你也可以在可索引类型中混合定义必需的属性:

type Data = {
  status: boolean;
  [key: string]: any;
};

const someData: Data = {
  status: true,
  someBooleanKey: true,
  someStringKey: 'text goes here'
}

现在,Data 类型的对象必须包含一个 boolean 类型的 status 属性。

创建具有最小元素数量的数组

结合 TypeScript 的数组(array)和元组(tuple)类型,你可以创建要求至少包含特定数量元素的数组类型。这通常通过 rest 操作符 ... 实现。

假设一个函数需要一个至少包含两个字符串的数组作为参数,你可以这样定义它的类型:

type MergeStringsArray = [string, string, ...string[]];

这个类型定义了一个元组,前两个元素必须是 string 类型,之后可以有零个或多个 string 类型的元素。

如果数组元素少于两个,TypeScript 会报错:

// 这会引发一个编译错误
const invalidArray: MergeStringsArray = ['some-string'];

组合类型

TypeScript 允许你通过组合现有类型来创建新类型。最常用的两种方式是联合类型(Unions)和交叉类型(Intersections)。

联合类型

联合类型使用 |(管道)操作符表示一个值可以是多种类型之一。

type ProductCode = number | string;

const productCodeA: ProductCode = 'this-works'; // 正确
const productCodeB: ProductCode = 1024;      // 正确

ProductCode 类型的值既可以是 string 也可以是 number

交叉类型

交叉类型使用 & 操作符将多个类型合并为一个新类型,新类型将拥有所有被交叉类型的所有属性。

例如,你可以为一个 API 响应定义一个通用状态类型和一个特定的用户数据类型:

type StatusResponse = {
  status: number;
  isValid: boolean;
};

type GetUserResponse = {
  user: { name: string; };
};

然后,你可以使用交叉类型将它们合并为完整的 API 响应类型:

type ApiGetUserResponse = StatusResponse & GetUserResponse;

let response: ApiGetUserResponse = {
    status: 200,
    isValid: true,
    user: {
        name: 'Sammy'
    }
}

ApiGetUserResponse 类型现在同时包含了 StatusResponseGetUserResponse 的所有属性。

模板字符串类型

从 TypeScript 4.1 开始,你可以使用模板字面量语法来创建类型,这对于约束字符串的特定格式非常有用。

例如,创建一个只接受以 get 开头的字符串的类型:

type StringThatStartsWithGet = `get${string}`;

const myString: StringThatStartsWithGet = 'getAbc'; // 正确

// 这会引发一个编译错误
const invalidStringValue: StringThatStartsWith-Get = 'something';

类型断言

当你处理来自外部库或 API 的 any 类型数据时,为了恢复类型安全,可以使用类型断言(Type Assertions)来告诉编译器一个值的具体类型。语法是 value as NewType

const valueA: any = 'something';

// 将 valueA 断言为 string 类型
const valueB = valueA as string;

现在,valueB 的类型是 string,你可以安全地调用字符串方法。请谨慎使用类型断言,因为它会绕过编译器的类型检查,最好在确认类型无误的情况下使用。

工具类型

TypeScript 提供了一些内置的工具类型(Utility Types),它们可以帮助你基于现有类型进行转换和操作,而无需从头创建新类型。这些工具类型都是泛型(Generics)。

Record<Key, Value>

Record 工具类型可以以更简洁的方式创建可索引类型。

// 使用索引签名
type Data_A = {
  [key: string]: any;
};

// 使用 Record 工具类型
type Data_B = Record<string, any>;

这两种定义方式是等效的。

Omit<Type, Fields>

Omit 工具类型用于从一个现有类型中创建一个新类型,并移除指定的某些属性。

type UserRow = {
  id: number;
  name: string;
  email: string;
  addressId: string;
};

// 创建一个不含 'id' 和 'addressId' 的新类型
type UserRowWithoutIds = Omit<UserRow, 'id' | 'addressId'>;

UserRowWithoutIds 类型将只包含 nameemail 属性。

Pick<Type, Fields>

PickOmit 相反,它用于从现有类型中选取指定的属性来创建一个新类型。

type UserRow = {
  id: number;
  name: string;
  email: string;
  addressId: string;
};

// 创建一个只包含 'name' 和 'email' 的新类型
type UserRowWithEmailOnly = Pick<UserRow, 'name' | 'email'>;

Partial<Type>

Partial 工具类型会将一个类型的所有属性都变为可选的。

type UserRow = {
  id: number;
  name: string;
  email: string;
  addressId: string;
};

// 创建一个所有属性都可选的新类型
type UserRowInsert = Partial<UserRow>;

UserRowInsert 类型等同于:

type UserRowInsert = {
  id?: number;
  name?: string;
  email?: string;
  addressId?: string;
};

利用 AI 工具大规模定义和强制执行类型

随着 TypeScript 在大型代码库中的普及,像 GitHub Copilot 和 Codeium 这样的 AI 编程助手可以极大地加速团队定义、重构和实施类型一致性的过程。

实用工作流

  • 从示例生成类型:粘贴 JSON 或 API 响应示例,然后给出提示(Prompt):为这个 payload 创建严格的 TypeScript 类型和 Zod schema。AI 可以同时生成编译时类型和运行时验证器。
  • 重构以实现严格模式:在 tsconfig.json 中启用 "strict": true 后,AI 可以帮助你安全地将 any 类型迁移到更精确的联合类型、品牌类型(branded types)或带有类型收窄的 unknown
  • 运行时验证:AI 可以为你的类型配对验证器(如 zod),在代码边界强制执行数据形状的正确性。
import { z } from 'zod';

// AI-suggested types and schema
const OrderItemSchema = z.object({
  sku: z.string(),
  qty: z.number().int().positive(),
});

const OrderSchema = z.object({
  id: z.string(),
  totalCents: z.number().int().nonnegative(),
  status: z.enum(['pending', 'paid', 'failed']),
  items: z.array(OrderItemSchema).min(1),
});

type Order = z.infer<typeof OrderSchema>;

function parseOrder(input: unknown): Order {
  return OrderSchema.parse(input);
}

大型项目推荐配置

  • tsconfig.json:启用严格模式以最大化 AI 建议的价值。
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true
  }
}
  • ESLint 与 typescript-eslint:强制执行类型规范,并让 AI 自动修复许多问题。
// .eslintrc.cjs
module.exports = {
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:@typescript-eslint/recommended-type-checked',
  ],
  parser: '@typescript-eslint/parser',
  parserOptions: { project: ['./tsconfig.json'] },
  rules: {
    '@typescript-eslint/no-explicit-any': 'warn',
    '@typescript-eslint/no-unsafe-assignment': 'error',
    '@typescript-eslint/consistent-type-definitions': ['error', 'type'],
    '@typescript-eslint/no-floating-promises': 'error'
  }
};
  • CI/CD:在 CI 流程中添加类型检查和 linting,以防止类型问题进入主分支。
// package.json (scripts)
{
  "scripts": {
    "typecheck": "tsc -p tsconfig.json --noEmit",
    "lint": "eslint . --ext .ts,.tsx",
    "validate": "npm run typecheck && npm run lint"
  }
}

类型代码生成管道

自动化类型生成,以减少手动维护,并确保 AI 建议与权威数据源(source-of-truth)保持一致。

  • 从 OpenAPI 到 TypeScript 类型:使用 openapi-typescript
{
  "scripts": {
    "codegen:openapi": "openapi-typescript openapi/schema.yaml -o src/types/api.d.ts"
  }
}
  • 从 GraphQL 到 TypeScript 类型:使用 @graphql-codegen/cli
# codegen.yml
schema: schema.graphql
generates:
  src/types/graphql.ts:
    plugins:
      - typescript
      - typescript-operations

治理清单与配置

在你的代码仓库中维护一份 AI 使用的治理文档(例如 docs/AI-GOVERNANCE.md),明确规定允许使用的模型、数据隐私政策、代码生成策略等,并定期审查。

常见问题解答 (FAQs)

1. interface vs type — 应该用哪个?

  • 当你期望一个对象结构可以被继承(extends)或合并(声明合并)时,使用 interface
  • 对于联合类型、交叉类型、映射类型等更复杂的操作,使用 type
  • 对于普通的对象结构,两者皆可,关键是保持团队内部的一致性。

2. anyunknownnever 有什么区别?

  • any:完全放弃类型安全,应避免在生产代码中使用。
  • unknown:更安全的顶级类型,在使用前必须进行类型收窄。
  • never:表示永远不会出现的值,常用于穷尽性检查。

3. 如何安全地进行类型收窄,避免使用 as 断言?

优先使用类型守卫(value is string)、控制流分析(typeof value === 'string')、instanceof 操作符和可辨识联合的判别字段。

4. 如何为多变体类型建模并实现穷尽性检查?

使用可辨识联合(discriminated union)和一个 never 检查:

type Shape = { kind: 'circle'; r: number } | { kind: 'rect'; w: number; h: number };

function area(s: Shape): number {
  switch (s.kind) {
    case 'circle':
      return Math.PI * s.r * s.r;
    case 'rect':
      return s.w * s.h;
    default: {
      const _exhaustive: never = s; // 如果有新的 variant 未处理,这里会报错
      return _exhaustive;
    }
  }
}

总结

通过创建自定义类型来表示代码中的数据结构,可以为你的 TypeScript 项目带来极大的灵活性和健壮性。这不仅增强了代码的类型安全,还通过将业务对象类型化,提高了代码库的文档性和团队协作的开发体验。

此外,集成 AI 工具可以显著提升你的 TypeScript 开发效率,通过智能提示和自动完成功能,实现更好的类型管理和更高效的编码流程。

关于

关注我获取更多资讯

公众号
📢 公众号
个人号
💬 个人号
使用 Hugo 构建
主题 StackJimmy 设计