关于 hide-unsafe-assertions
这项 TypeScript 技能介绍如何将不安全的类型断言限制在函数实现中,同时保持公开签名类型明确。它指导开发者验证、说明并测试这些断言。
主要功能
- 01保持公开签名类型明确
- 02将断言限制在实现内部
- 03断言前验证值
- 04说明断言安全的原因
- 05测试含断言的函数
适用场景
- 01封装类型定义不完善的库
- 02为 API 函数提供明确的返回类型
- 03处理 TypeScript 无法推断的情况
- 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