admin管理员组文章数量:1315258
I have a utility type that checks if a given type has no properties:
// (Incorrectly) tests if T is empty
type IsEmpty<T> = Record<string, never> extends T ? true : false
However, that test will return true if an object with only optional properties is tested:
type MyMixedType = {
a: number,
b?: number,
}
// Picks the required property only, won't be empty (test will be false)
type testA = IsEmpty<Pick<MyMixedType, 'a'>>
// Picks the optional property only, should not be empty, but test is true
type testB = IsEmpty<Pick<MyMixedType, 'b'>>
TS Playground link
My question is: Why? Why does Record<string, never> extends {b?: number}
? Or any object with only optional properties, for that matter.
I have a utility type that checks if a given type has no properties:
// (Incorrectly) tests if T is empty
type IsEmpty<T> = Record<string, never> extends T ? true : false
However, that test will return true if an object with only optional properties is tested:
type MyMixedType = {
a: number,
b?: number,
}
// Picks the required property only, won't be empty (test will be false)
type testA = IsEmpty<Pick<MyMixedType, 'a'>>
// Picks the optional property only, should not be empty, but test is true
type testB = IsEmpty<Pick<MyMixedType, 'b'>>
TS Playground link
My question is: Why? Why does Record<string, never> extends {b?: number}
? Or any object with only optional properties, for that matter.
1 Answer
Reset to default 1The particular example you've chosen is intentional, since never
is assignable to anything, but the overall situation is considered a bug in TypeScript, as per microsoft/TypeScript#27144... but that bug is probably never going away, so you can probably just consider it a design limitation at this point.
TypeScript is intentionally unsafe when it comes to optional properties and to index signatures. Object types missing such members are assignable to objects with them. For example, {a: string}
is assignable to both {a: string, [k: string]: string }
and to {a: string, b?: string}
. That's technically unsafe; something like {a: "abc", b: boolean}
is assignable to {a: string}
, and therefore a series of allowed assignments can result in runtime explosions:
const orig = { a: "abc", b: false };
const widen: { a: string } = orig; // okay, considered safe
const indexSig: { a: string, [k: string]: string } = widen; // allowed, but unsafe!
indexSig.b?.toUpperCase(); // compiles, but RUNTIME ERROR!
const optional: { a: string, b?: string } = widen; // allowed, but unsafe!
optional.b?.toUpperCase() // compiles, but RUNTIME ERROR!
That's unfortunate, but it's intentional. The following is just too convenient to give up:
const foo = { a: "abc" };
const bar: { a: string, b?: string } = foo; // okay
If they made the optional
assignment above invalid, then the bar
assignment would also be invalid for the same reason. People would hate that.
Usually this isn't a big deal, because assignability in TypeScript isn't necessarily transitive, and if you make direct assignments instead of an indirect series, you get the expected errors:
const indexSig: { a: string, [k: string]: string } = orig; // error!
const optional: { a: string, b?: string } = orig; // error!
So it's intentional for something like Record<string, XXX>
to be assignable to {b?: XXX}
, and probably for Record<string, never>
to be assignable to {b?: number}
since never
is assignable to number
.
The above is all as intended. The bug is that TypeScript allows you to cross-assign index signatures to optional properties, even when the property types directly contradict. That means a direct assignment that should obviously fail is allowed:
let indexSig: { a: string, [k: string]: string } = { a: "abc", b: "def" }; // okay
let optional: { a: string, b?: number } = { a: "abc", b: 123 }; // okay
optional = indexSig; // ALLOWED?!
optional.b?.toFixed(); // compiles, but RUNTIME ERROR!
It's filed as microsoft/TypeScript#27144, and it's not good. They should fix it, right?
Well they tried to implement a fix for it at microsoft/TypeScript#27591. And unfortunately it interacted poorly with some real world code, and it's not clear what should be done about that. The situation is as described in this comment:
From my observation, this change is:
Technically correct in most (probably all?) cases.
But not practically useful in the majority of cases.
Hard to recover from in many cases.
I vote not to take this change unless I see something that changes my mind. For example:
Data that show a majority of good, easy-to-understand bugs being caught.
An easy explanation of how to fix the errors mechanically.
And nothing happened after that. So, for better or worse, index signatures are assignable to optional properties, even when the property type is incompatible. Thus not only is Record<string, never>
assignable to {b?: number}
(which is intentional), but also Record<string, string>
is assignable to {b?: number}
(which is unintentional but apparently not changing anytime soon).
Playground link to code
本文标签: typescriptWhy does Recordltstringnevergt extends b numberStack Overflow
版权声明:本文标题:typescript - Why does Record<string, never> extends {b?: number}? - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1741979336a2408301.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
Record<string, never> extends {}
, and{} extends {b?: number}
, I don't see why you would expect a different result. – Robby Cornelissen Commented Jan 30 at 8:16type IsEmpty<T> = T extends Record<string, never> ? true : false
– Robby Cornelissen Commented Jan 30 at 8:33{}
was the quite wide type "any non nullish value"? How can theRecord<string, never> extends {}
detour be valid then? That feels like multiplying both sides in a mathematical equation by 0. – Holzchopf Commented Jan 30 at 10:09{a: string}
is assignable to{a: string, b?: number}
and{a: string}
is assignable to{a: string, [k: string]: string | number}
, which is unsafe but very convenient) and this interacts badly which each other in the case of incompatible types likeRecord<string, string> extends {a?: number}
. Does that fully address the q? If so I'll write an answer. If not, what's missing? – jcalz Commented Jan 30 at 13:36