第 42 条:避免基于个人经验设计类型
要点
- 不要根据零散数据手动写类型,容易误解结构或漏判空值
- 优先用官方/社区提供的类型,没有就用工具从规范生成。自动生成的类型能精准反映系统复杂性,减少人为失误
正文
本章其他条目讨论了良好类型设计的诸多好处,也展示了糟糕设计可能引发的问题。精心设计的类型让 TypeScript 使用体验愉悦,而拙劣的设计则令人痛苦。但这确实给类型设计带来不小压力。
在开发过程中,部分类型可能来自外部:规范文档、文件格式、API 或数据库结构。面对测试数据库中的几行数据或某个 API 端点的响应样本时,人们常倾向于手动编写类型声明。请抵制这种冲动!更好的做法是从权威来源导入类型或根据规范自动生成。
基于个人经验编写类型时,你仅考虑了已见到的示例,可能遗漏导致程序崩溃的关键边界情况。使用更官方的类型时,TypeScript 能帮你规避这类风险。
在第 30 条中,我们使用过计算 GeoJSON 要素边界框的函数。其类型定义可能如下:
function calculateBoundingBox(f: GeoJSONFeature): BoundingBox | null {
let box: BoundingBox | null = null
const helper = (coords: any[]) => {
// ...
}
const { geometry } = f
if (geometry) {
helper(geometry.coordinates)
}
return box
}
定义 GeoJSON 要素类型时,不要仅凭代码库中的几个样例就草率定义接口:
interface GeoJSONFeature {
type: 'Feature'
geometry: GeoJSONGeometry | null
properties: unknown
}
interface GeoJSONGeometry {
type: 'Point' | 'LineString' | 'Polygon' | 'MultiPolygon'
coordinates: number[] | number[][] | number[][][] | number[][][][]
}
虽然这个类型定义能通过检查,但它真的正确吗?类型检查的可靠性完全取决于我们自定义的类型声明。更稳妥的做法是采用 GeoJSON 官方规范。幸运的是,DefinitelyTyped 上已有现成的类型声明,通过以下命令安装:
$: npm install --save-dev @types/geojson
安装官方类型后,TypeScript 会立即暴露出原有定义的问题:
import { Feature } from 'geojson'
function calculateBoundingBox(f: Feature): BoundingBox | null {
let box: BoundingBox | null = null
const helper = (coords: any[]) => {
// ...
}
const { geometry } = f
if (geometry) {
helper(geometry.coordinates)
// ~~~~~~~~~~~
// Property 'coordinates' does not exist on type 'Geometry'
// Property 'coordinates' does not exist on type 'GeometryCollection'
}
return box
}
问题在于这段代码假设所有几何体都会有 coordinates 属性。虽然点、线、面等几何类型确实如此,但 GeoJSON 还包含几何集合类型(GeometryCollection)——这种异构集合类型恰恰没有 coordinates 属性。
当你对 GeometryCollection
类型的地理要素调用 calculateBoundingBox
方法时,会抛出 "无法读取 undefined 的属性 0" 的错误。这是个真实存在的 bug!而正是通过使用社区提供的类型定义,我们提前发现了这个问题。
修复这个 bug 的一个方案是:明确禁止传入 GeometryCollection
类型。
const { geometry } = f
if (geometry) {
if (geometry.type === 'GeometryCollection') {
throw new Error('GeometryCollections are not supported.')
}
helper(geometry.coordinates) // OK
}
TypeScript 能够基于类型检查自动细化 geometry
的类型,因此访问 geometry.coordinates
是允许的。至少这样能给用户更清晰的错误提示。但更好的解决方案是支持 GeometryCollections
!可以通过提取辅助函数来实现:
const geometryHelper = (g: Geometry) => {
if (g.type === 'GeometryCollection') {
g.geometries.forEach(geometryHelper)
} else {
helper(g.coordinates) // OK
}
}
const { geometry } = f
if (geometry) {
geometryHelper(geometry)
}
我们手写的 GeoJSON 类型仅基于自己对格式的有限理解,没有考虑到 GeometryCollections
的情况,这导致了对代码正确性的盲目自信。使用基于官方规范的社区类型,能确保代码处理所有可能值,而不仅限于你见过的那些。
API 调用同理:
- 如果 API 有官方 TypeScript 客户端,优先使用
- 即使没有,通常也能从官方规范生成类型
以 GraphQL 为例,其自带的 schema 已完整定义了所有查询、变更和类型。有很多工具可为 GraphQL 查询添加 TypeScript 类型支持,简单搜索就能找到解决方案。
对于 REST API,许多服务会提供 OpenAPI 规范(原 Swagger)。这个 JSON 文件完整描述了所有端点、HTTP 方法(GET/POST 等)以及基于 JSON Schema 的类型定义。例如一个博客评论 API 的 OpenAPI 规范可能如下:
// schema.json
{
"openapi": "3.0.3",
"info": { "version": "1.0.0", "title": "Sample API" },
"paths": {
"/comment": {
"post": {
"requestBody": {
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/Comment" }
}
}
}
},
"responses": {
"200": {
/* ... */
}
}
}
},
"components": {
"schemas": {
"CreateCommentRequest": {
"properties": {
"body": { "type": "string" },
"postId": { "type": "string" },
"title": { "type": "string" }
},
"type": "object",
"required": ["postId", "title", "body"]
}
}
}
}
paths
部分定义了 API 的接口路径,并将它们与 components/schemas
中的数据类型关联起来。我们生成类型所需的所有信息都在这里。
从 OpenAPI 规范提取类型有多种方法,其中一种是把 schemas
部分的内容提取出来,然后用 json-schema-to-typescript
这样的工具转换成 TypeScript 类型。
$ jq .components.schemas.CreateCommentRequest schema.json > comment.json
$ npx json-schema-to-typescript comment.json > comment.ts
$ cat comment.ts
// ....
export interface CreateCommentRequest {
body: string;
postId: string;
title: string;
}
这样生成的类型定义既清晰又规范,能让你以类型安全的方式调用 API。TypeScript 会自动标出请求体的类型错误,并将正确的响应类型传递到代码各处。关键点在于:这些类型不是你手动写的,而是从可靠的官方规范自动生成的。如果某个字段是可选的或允许为 null,TypeScript 会强制你处理这些可能性。
接下来可以添加运行时验证,并把这些类型直接关联到对应的 API 接口。有很多工具能帮你实现这一点(第 74 条会回到这个例子)。但要注意保持生成的类型与 API 规范同步(第 58 条会讨论同步策略)。
如果没有官方规范怎么办?这时你需要从实际数据生成类型。像 quicktype
这类工具能帮忙,但要警惕:这样生成的类型可能遗漏边界情况(除非数据量有限,比如已知的 1000 个 JSON 文件目录,这时可以确保全覆盖)。
其实你已经在享受代码生成的好处了——TypeScript 的浏览器 DOM 类型声明(第 75 条详述)就是从 MDN 的 API 描述自动生成的。这确保了复杂系统的精确建模,帮你的代码规避错误。
关键点总结
- 不要根据零散数据手动写类型,容易误解结构或漏判空值
- 优先用官方/社区提供的类型,没有就用工具从规范生成。自动生成的类型能精准反映系统复杂性,减少人为失误