关于 structural-typing
这项 TypeScript 学习技能讲解结构类型、可赋值规则及何时需要更严格的检查,帮助开发者理解灵活 API、类和测试替身。
主要功能
- 01讲解基于结构的可赋值性
- 02厘清额外属性的处理方式
- 03介绍类的结构类型规则
- 04展示使用品牌类型进行严格检查
适用场景
- 01设计接口和函数参数
- 02无需模拟库即可创建测试替身
- 03排查意外的类型赋值
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ marius-townhouse/ effective-typescript-skills --skill structural-typing安装后在 Claude Code 中输入/structural-typing确认已加载。
详细介绍
熟悉结构化类型
概述
TypeScript 使用结构化类型:形状符合要求,就能匹配。
与名义类型(类型必须显式声明)不同,TypeScript 会检查结构。理解这一点可以避免意外情况,并解锁强大的模式。
何时使用此技能
- 对 TypeScript 接受“错误”值感到意外
- 设计接口和函数参数
- 使用模拟对象编写单元测试
- 调试“看似不可能”的类型错误
- 了解为何允许额外属性
铁律
NEVER assume types are "sealed" - they always allow extra properties.
请接受以下事实:
- 只要具有必需属性,就可以赋值
- 额外属性不会使值无效
- 比较类时看的是结构,而不是身份
检测:“密封类型”假设
如果你对 TypeScript 接受某个值感到意外,你可能是在假设它采用名义类型。
interface Vector2D {
x: number;
y: number;
}
function calculateLength(v: Vector2D) {
return Math.sqrt(v.x ** 2 + v.y ** 2);
}
// ✅ Works as expected
calculateLength({ x: 3, y: 4 }); // 5
// ✅ Also works! Has x and y, so it's a valid Vector2D
const namedVector = { x: 3, y: 4, name: 'Pythagoras' };
calculateLength(namedVector); // 5
// ✅ Even 3D vectors work (but give wrong results!)
const vector3D = { x: 3, y: 4, z: 5 };
calculateLength(vector3D); // 5 (ignores z!)
结构化类型原则
如果某个值至少具有所有必需属性,且属性类型兼容,那么它就可以赋给相应类型。
interface Point {
x: number;
y: number;
}
// All of these are valid Points:
const p1: Point = { x: 1, y: 2 }; // Exact match
const p2: Point = { x: 1, y: 2, z: 3 }; // Extra property (via variable)
const p3: Point = { x: 1, y: 2, name: 'origin' }; // Different extra property
// But not this:
const p4: Point = { x: 1 }; // Error: missing 'y'
这对函数为何重要
interface Vector2D { x: number; y: number; }
interface Vector3D { x: number; y: number; z: number; }
function normalize(v: Vector3D) {
const length = Math.sqrt(v.x ** 2 + v.y ** 2 + v.z ** 2);
return {
x: v.x / length,
y: v.y / length,
z: v.z / length,
};
}
// This is a bug, but TypeScript doesn't catch it:
function calculateLength2D(v: Vector2D) {
return Math.sqrt(v.x ** 2 + v.y ** 2);
}
// normalize calls calculateLength2D internally
function normalize(v: Vector3D) {
const length = calculateLength2D(v); // Bug: ignores z!
// Vector3D is assignable to Vector2D
}
类中的结构化类型
class SmallContainer {
num: number;
constructor(num: number) {
if (num < 0 || num >= 10) {
throw new Error('Must be 0-9');
}
this.num = num;
}
}
const a = new SmallContainer(5); // OK
// This also type-checks, but bypasses validation!
const b: SmallContainer = { num: 2024 }; // No error!
// Because SmallContainer structurally is just { num: number }
优势:轻松测试
结构化类型让测试更简单——不需要模拟库:
interface Database {
runQuery(sql: string): any[];
}
function getUsers(db: Database) {
return db.runQuery('SELECT * FROM users');
}
// In tests, just create an object with the right shape:
test('getUsers', () => {
const mockDb = {
runQuery(sql: string) {
return [{ name: 'Alice' }, { name: 'Bob' }];
}
};
const users = getUsers(mockDb); // Works! No type error
expect(users).toHaveLength(2);
});
“多余属性检查”例外
对象字面量会被特殊处理——TypeScript 会标记额外属性:
interface Point { x: number; y: number; }
// Extra property in object literal: Error!
const p: Point = { x: 1, y: 2, z: 3 };
// ~ Object literal may only specify known properties
// But via intermediate variable: No error
const temp = { x: 1, y: 2, z: 3 };
const p: Point = temp; // OK
这是一项易用性功能,并未改变结构化类型规则。详情请参阅多余属性检查技能。
结构化类型引发问题时
问题:向量维度错误
// Solution 1: Use optional never to forbid property
interface Vector2D {
x: number;
y: number;
z?: never; // Explicitly disallows z
}
// Solution 2: Use branded types (see branded-types skill)
type Vector2D = { x: number; y: number } & { _brand: 'Vector2D' };
问题:绕过类验证
// Solution: Make the class have unique properties
class SmallContainer {
private readonly _brand = 'SmallContainer'; // Can't be faked
num: number;
// ...
}
抗压应对方案
1. “这不应该被允许”
压力:“TypeScript 应该拒绝具有额外属性的值”
**回应:**那是名义类型。TypeScript 使用结构化类型。
**行动:**如果需要更严格的检查,请使用品牌类型等技术。
2. “我的类应该与众不同”
压力:“只有我这个类的真实实例才应该有效”
**回应:**类采用结构化类型。添加私有字段以区分它们。
**行动:**使用私有字段或品牌,实现类似名义类型的行为。
危险信号——停下来重新考虑
- 认为额外属性会使值无效
- 期待类的身份起作用
- 对 TypeScript 接受“错误”值感到意外
- 认为类型是“密封的”
- 使用 TypeScript 无法识别的验证逻辑
常见的自我辩解(都不成立)
| 借口 | 事实 |
|---|---|
| “类型不对” | 只要形状符合要求,类型就没问题。 |
| “我的类会验证” | 结构匹配的对象可以绕过构造函数。 |
| “额外属性不应该起作用” | 在 TypeScript 中,它们确实可以。 |
快速参考
| 场景 | 结构化类型行为 |
|---|---|
| 值上的额外属性 | 允许(对象字面量除外) |
| 类实例 | 根据结构比较,而非类身份 |
| 函数参数 | 任何结构兼容的值都可以 |
| 对象字面量赋值 | 会标记多余属性(特殊情况) |
归根结底
TypeScript 检查的是形状,而不是身份。
如果某个值具有所有必需属性,且类型兼容,那么它就可以赋值。这让测试更容易、API 更灵活,但也可能带来意外。如果需要更严格的检查,请使用品牌类型等技术。
参考
内容基于 Dan Vanderkam 的《Effective TypeScript》,条目 4:熟悉结构化类型。
评论
支持 Markdown