最近在使用TypeScript时遇到以下情况,在实践中会经常出现:
interface Options {
foo?: number;
bar?: string;
}
const defaultOptions = {
foo: 123,
bar: 'abc',
} as const satisfies Options;
function f(options: Options) {
const fullOptions = { // Type is inferred as {foo: number; bar: string} (regardless of satisfies)
...defaultOptions,
...options,
} as const satisfies Required<Options>; // This is not guaranteed and should be marked as error!
// The actual type is still just Options because the props of defaultOptions may be overwritten.
console.log(fullOptions);
return fullOptions.bar.toUpperCase();
}
f({foo: 456}); // Works
f({foo: 456, bar: undefined}) // Runtime error!
正如你所看到的,TypeScript 并没有在
satisfies Required<Options>
部分抱怨,尽管它应该抱怨。这是故意行为还是错误?
TypeScript 的行为符合预期;请参阅 microsoft/TypeScript#57408 获取权威答案。
一般来说,对于可选属性,TypeScript 不会区分缺失的属性和存在
undefined
值的属性。对于很多目的来说这都很好。对于 const opts = {foo: 456}
和 const opts = {foo: 456, bar: undefined}
,opts.bar
都是 undefined
。但在某些情况下,存在明显的差异,对象传播的行为就是这样一种情况。
很长一段时间,事情就是这样。但很多人都不满意,microsoft/TypeScript#13195 上的功能请求得到了很多社区支持。最终,TypeScript 引入了
--exactOptionalPropertyTypes
编译器选项,如在microsoft/TypeScript#43947中实现的那样。启用该编译器选项后,如果您从可选属性中 read,您仍然会看到
undefined
,但如果您尝试将 undefined
to 写入可选属性,则会出现错误(除非 undefined
被明确指定)包含在其值类型中)。如果我们启用它,那么以下代码会产生编译器警告:
f({foo: 456, bar: undefined}) // error!
// Argument of type '{ foo: number; bar: undefined; }' is not assignable to
// parameter of type 'Options' with 'exactOptionalPropertyTypes: true'.
// Type 'undefined' is not assignable to type 'string'.
并且您不会意外地使用
undefined
覆盖现有属性。
因此,如果您真的关心这一点,您可能需要启用
--exactOptionalPropertyTypes
。请注意,此编译器选项不包含在--strict
编译器选项套件中。最初这将是 --strictOptionalProperties
并包含在内。但如果您阅读 microsoft/TypeScript#44421 上的讨论,您会发现它被认为是一个重大更改。问题在于,现实世界中有大量代码(包括 TypeScript 自己的库)使用可选属性。但由于 {x?: string}
和 {x?: string | undefined}
在历史上是等价的,那么像 {x?: string}
这样的现有声明会发生什么情况呢?原作者是打算允许还是不允许undefined
?没有办法以编程方式判断。这意味着有人必须仔细检查每一个可选属性并做出决定。否则,TypeScript 的下一个版本将在所有地方强制执行“disallow undefined
”,并且很多事情都会被破坏。
因此,无论好坏,他们将其从
--strict
中移出,在 microsoft/TypeScript#44626重命名为
--exactOptionalPropertyTypes
,并告诉人们使用它需要自行承担风险。也许有一天,生态系统将准备好将该标志包含在 --strict
中,但是直到这种情况发生之前,您需要决定您自己的代码中的收益是否大于风险。