引言
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 类型现在同时包含了 StatusResponse 和 GetUserResponse 的所有属性。
模板字符串类型
从 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 类型将只包含 name 和 email 属性。
Pick<Type, Fields>
Pick 与 Omit 相反,它用于从现有类型中选取指定的属性来创建一个新类型。
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. any、unknown 和 never 有什么区别?
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 开发效率,通过智能提示和自动完成功能,实现更好的类型管理和更高效的编码流程。
关于
关注我获取更多资讯