mcpField
S
structural-typing

structural-typing

github.com/marius-townhouse开发645 次安装38 周前更新
代码仓库

关于 structural-typing

这项 TypeScript 学习技能讲解结构类型、可赋值规则及何时需要更严格的检查,帮助开发者理解灵活 API、类和测试替身。

主要功能

  1. 01讲解基于结构的可赋值性
  2. 02厘清额外属性的处理方式
  3. 03介绍类的结构类型规则
  4. 04展示使用品牌类型进行严格检查

适用场景

  1. 01设计接口和函数参数
  2. 02无需模拟库即可创建测试替身
  3. 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