先记住这个答案
泛型参数上的 extends 限定合法类型参数,并让函数实现能够依赖相应基础能力;satisfies 则在表达式位置检查这个值是否可赋给指定类型,同时保留表达式自身的类型信息。两者不是互换语法:受约束的泛型可以保留输入的额外字段,而新鲜对象字面量使用 satisfies 时还会接受目标类型的额外属性检查。satisfies 的上下文也可能影响字面量推断,不能说它对类型推断完全无影响。配置作者可以在定义处检查字段,通用函数再用泛型约束维持输入输出关系。
- extends 约束类型参数并提供实现中的能力保证
- satisfies 检查当前表达式的可赋值性
- 额外字段与上下文推断要按具体位置分析
泛型约束为什么能保留额外字段
通用函数只要求 path,并不意味着它拒绝所有其他能力。T 可以是带自定义字段的更具体对象,函数返回 T 时还能保留这些信息。它限制的是最低结构要求,不是把每个输入裁剪成基础接口,也不自动执行精确字段检查。
示例中泛型助手保留 retries,而按固定配置类型检查的新鲜字面量不能随意添加该字段。正确的 satisfies 表达式仍保留自身字段类型。这种差异提醒作者先决定接口是否真的允许扩展,而不是把两种写法看成任意替换的校验工具。
type Options = { path: string; cache?: boolean };
function define<T extends Options>(value: T): T { return value; }
const extended = define({ path: '/items', retries: 2 });
const checked = { path: '/items', cache: true } satisfies Options;
// @ts-expect-error 固定 Options 没有声明 retries
const rejected = { path: '/items', retries: 2 } satisfies Options;extended 保留数字 retries,checked 的 cache 在这个上下文中保留 true 字面量。新鲜字面量的额外键被拒绝,这不是泛型忽略类型检查,而是两处表达的契约不同。
保留类型不等于完全不影响推断
把表达式直接注解为 Options,变量的静态视图就以该目标类型为准;使用 satisfies 则保留表达式结果的自身信息。但目标提供的上下文可能影响字面量选择,例如某些布尔字段不再推断为宽 boolean,因此后续赋值行为可能不同。
如果配置需要在多个合法状态间修改,应给它合适的可变状态类型,不要单凭保留信息更精确就选择最窄形式。反过来,固定配置中的具体键名与值可能是后续类型运算的重要依据,定义处检查又比散落断言更容易维护。
怎样把两者组合成清楚接口
配置定义者可以用 satisfies 检查允许字段和拼写,再交给带泛型约束的助手进行注册或包装。助手必须诚实描述是否返回原对象、是否归一化或删除字段;若实现改变结构,就不能为了保留提示而直接承诺返回原始 T。
这两种机制最终都在编译后消失。配置若来自文件、网络或用户输入,不能靠附加 satisfies 语法验证实际字段。应先解析外部数据,再在内部类型层维护关系。评审时同时查看定义位置、函数签名和运行实现,才能判断约束是否对齐。
容易答错的地方
- satisfies 就是另一个写法的泛型 extends
- 它检查具体表达式,而泛型约束限制类型参数并服务通用实现;出现位置、额外属性检查和可保持的关系都可能不同。
- 保留表达式类型意味着推断完全不变
- 目标类型提供的上下文可能影响字面量推断,尤其在配置字段上容易观察到差异;应核对后续可写性与实际需要的类型范围。
面试官还会怎么问?
为什么 define 接受多字段却仍算有约束?
它仍要求基础字段存在且类型正确,只是允许 T 比基础契约更具体;若需要限制精确字段集合,应明确设计这项额外要求。
配置中拼错可选字段时适合哪种检查?
在定义处按固定配置类型使用 satisfies 通常能帮助发现新鲜字面量的未知键;同时要确认配置是否有索引签名等允许扩展的声明。
两种写法会把输入对象复制一份吗?
不会,类型检查不会执行复制;是否返回原引用或构造新对象取决于实际函数体,调用方需要查看对应运行行为契约。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。