mcpField
H
hide-unsafe-assertions

hide-unsafe-assertions

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

关于 hide-unsafe-assertions

这项 TypeScript 技能介绍如何将不安全的类型断言限制在函数实现中,同时保持公开签名类型明确。它指导开发者验证、说明并测试这些断言。

主要功能

  1. 01保持公开签名类型明确
  2. 02将断言限制在实现内部
  3. 03断言前验证值
  4. 04说明断言安全的原因
  5. 05测试含断言的函数

适用场景

  1. 01封装类型定义不完善的库
  2. 02为 API 函数提供明确的返回类型
  3. 03处理 TypeScript 无法推断的情况
  4. 04避免断言扩散到调用方

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/marius-townhouse/effective-typescript-skills --skill hide-unsafe-assertions

安装后在 Claude Code 中输入/hide-unsafe-assertions确认已加载。

详细介绍

在类型良好的函数中隐藏不安全的类型断言

概述

保持类型签名简洁;将断言隐藏在实现中。

如果函数内部需要类型断言或 any,这是可以的,只要公开的签名正确。用户看到的是类型良好的 API;不安全代码则被限制在内部。

何时使用此技能

  • 函数实现需要类型断言
  • TypeScript 无法理解你的逻辑
  • 为类型定义不佳的库编写封装
  • 内部逻辑复杂,但外部 API 简洁

铁律

Never compromise type signatures for implementation convenience.
Hide assertions inside well-typed functions.

请记住:

  • 类型签名是你的 API 契约
  • 实现是隐藏的细节
  • 用户不应该看到断言
  • 对断言密集的代码进行全面测试

检测:暴露不安全代码

// Bad: unsafe return type exposes any
async function fetchPeak(peakId: string): Promise<unknown> {
  return checkedFetchJSON(`/api/peaks/${peakId}`);
}

// Every caller must assert:
const peak = await fetchPeak('denali') as MountainPeak;  // Tedious!

更好的做法:隐藏断言

async function fetchPeak(peakId: string): Promise<MountainPeak> {
  return checkedFetchJSON(`/api/peaks/${peakId}`) as Promise<MountainPeak>;
}

// Callers get clean types:
const peak = await fetchPeak('denali');
//    ^? MountainPeak

断言被隐藏在内部;调用方获得简洁的 API。

在隐藏的断言内部进行验证

async function fetchPeak(peakId: string): Promise<MountainPeak> {
  const maybePeak = await checkedFetchJSON(`/api/peaks/${peakId}`);
  
  // Validation adds safety to the assertion
  if (!maybePeak || typeof maybePeak !== 'object' || !('name' in maybePeak)) {
    throw new Error(`Invalid peak data: ${JSON.stringify(maybePeak)}`);
  }
  
  return maybePeak as MountainPeak;
}

这样,断言就有了运行时保障。

TypeScript 无法理解时

function shallowEqual(a: object, b: object): boolean {
  for (const [k, aVal] of Object.entries(a)) {
    if (!(k in b) || aVal !== b[k]) {
      //                     ~~~~
      // Element implicitly has an 'any' type
      return false;
    }
  }
  return Object.keys(a).length === Object.keys(b).length;
}

我们知道 k in b 为真,但 TypeScript 无法将这一点与 b[k] 联系起来。

错误的修复方式:弱化签名
// DON'T: weakening b to any exposes unsafety
function shallowEqual(a: object, b: any): boolean {
  // ...
}

shallowEqual({x: 1}, null);  // No error! Crashes at runtime.
正确的修复方式:隐藏断言
function shallowEqual(a: object, b: object): boolean {
  for (const [k, aVal] of Object.entries(a)) {
    // Hidden assertion - we've checked k in b
    if (!(k in b) || aVal !== (b as any)[k]) {
      return false;
    }
  }
  return Object.keys(a).length === Object.keys(b).length;
}

shallowEqual({x: 1}, null);
//                   ~~~~
// Argument of type 'null' is not assignable to parameter of type 'object'.

类型签名保持简洁;断言的作用范围很小。

将函数重载用作隐藏断言

// Overload presents clean signature to callers
async function fetchPeak(peakId: string): Promise<MountainPeak>;
async function fetchPeak(peakId: string): Promise<unknown> {
  return checkedFetchJSON(`/api/peaks/${peakId}`);
}

const denali = fetchPeak('denali');
//    ^? Promise<MountainPeak>

实现返回 unknown,但调用方看到的是 MountainPeak。

限定断言的作用范围

// DON'T: Assertion on whole object
const config: Config = {
  a: 1,
  b: 2,
  c: { key: value }
} as any;  // No checking on a, b!

// DO: Assertion only on problem area
const config: Config = {
  a: 1,
  b: 2,  // These are still checked
  c: { key: value as any }  // Only this is unsafe
};

说明断言有效的原因

function shallowEqual(a: object, b: object): boolean {
  for (const [k, aVal] of Object.entries(a)) {
    // `(b as any)[k]` is safe because we've verified `k in b`
    if (!(k in b) || aVal !== (b as any)[k]) {
      return false;
    }
  }
  return Object.keys(a).length === Object.keys(b).length;
}

注释有助于后续维护者理解断言。

全面测试

包含隐藏断言的函数需要额外测试:

describe('fetchPeak', () => {
  it('handles valid peak data', async () => {
    const peak = await fetchPeak('denali');
    expect(peak.name).toBe('Denali');
    expect(peak.elevationMeters).toBe(6190);
  });

  it('throws on invalid data', async () => {
    mockFetch({ invalid: 'data' });
    await expect(fetchPeak('unknown')).rejects.toThrow('Invalid peak');
  });

  it('handles missing fields', async () => {
    mockFetch({ name: 'Partial' });  // Missing fields
    await expect(fetchPeak('partial')).rejects.toThrow();
  });
});

抵御施压的守则

1. “直接更改返回类型”

施压:“让它返回 unknown,这样就不用写断言了”

**回应:**这会把不安全性推给每个调用方。

**行动:**保持签名简洁;将断言隐藏在实现中。

2. “断言很危险”

施压:“我们应该完全避免使用断言”

**回应:**有时确实需要断言。只要将其限制在内部并进行测试,就可以接受。

**行动:**隐藏、验证、记录并测试。

危险信号——停止并重新考虑

  • 为图方便,在函数签名中使用 any 或 unknown
  • 断言散布在调用代码中
  • 为避免实现错误而更改签名
  • 未经测试、断言密集的代码

常见的自我辩解(都站不住脚)

借口事实
“这样更诚实”把不安全性推给调用方更糟
“断言不好”将断言限制在内部是可以的
“用户可以自行缩窄类型”不应该要求用户这么做

快速参考

// DON'T: Expose unsafety in signature
function fetch(): Promise<unknown> { ... }
const data = await fetch() as Data;  // Assertion at every call site

// DO: Hide assertion in implementation
function fetch(): Promise<Data> {
  return api.fetch() as Promise<Data>;  // Hidden, one place
}

// DO: Narrow scope
const obj = { a: value as any };  // Not: whole object as any

// DO: Document and test
// This assertion is valid because... [explanation]
return data as Data;

归根结底

隐藏不安全代码;公开简洁的类型。

有时类型断言和 any 类型不可避免。将它们保留在函数实现中,而不是签名中。说明它们有效的原因,并进行全面测试。

参考资料

基于 Dan Vanderkam 的《Effective TypeScript》第 45 条:在类型良好的函数中隐藏不安全的类型断言。

评论

支持 Markdown