Reputation: 6163
I have the following code:
type T = { foo: string }
var t: T = { foo: 'foo' }
interface S { foo: string }
var s: S = t
So we know that T < S
.
How about this?
t = s
Ok so S < T
is also true.
We can imply that S == T
.
Now to introduce U
:
type U = { [key: string]: string }
var u: U = t
So T < U
. So far so good.
But wait!
u = s // Error!
This seems to violate the Liskov Substitution Principle (LSP):
if S is a subtype of T, then objects of type T may be replaced with objects of type S
Is this a violation of LSP? Does it matter if it is or not?
Principles aside, this looks rather silly:
u = s // Error!
u = <T>s // Ok!
Would this be considered a bug? Surely the compiler could have done that on its own no?
Upvotes: 1
Views: 371
Reputation: 329773
TypeScript's type system is unsound in places; you've found this issue in which type aliases but not interfaces are given implicit index signatures. Giving a type an implicit index signature is useful but unsafe in general. Consider:
const fooBar = { foo: "foo", bar: 123 };
const tFooBar: T = fooBar; // okay
const uFooBar: U = tFooBar; // okay?
const whoopsie = uFooBar.bar; // string at compile time, number at runtime?!
console.log(whoopsie);
The value fooBar
is a valid T
, because it has a foo
property of type string
. So you can assign it to tFooBar
. And then since TypeScript allows you to assign a value of type T
to a variable of type U
, you can assign tFooBar
to uFooBar
. And now the unsoundness is exposed if you read the bar
property of uFooBar
. It should be a string
according to U
, but it's a number
. Oops.
Implicit index signatures are useful because often functions require values with index signatures, and it's helpful for values whose known properties conform to the index signature to be accepted. So, we have this useful thing which can lead to type-unsafe behavior. What should be done?
Apparently the current rule for TypeScript is:
Apparently this last is intentional and not a bug, according to this comment by @RyanCavanaugh:
Just to fill people in, this behavior is currently by design. Because interfaces can be augmented by additional declarations but type aliases can't, it's "safer" (heavy quotes on that one) to infer an implicit index signature for type aliases than for interfaces. But we'll consider doing it for interfaces as well if that seems to make sense.
So the thought is that declaration merging might break interface-to-index-signature compatibility but type aliases can't. They're open to altering it, maybe, and if you have a compelling use case you might want to go to the Github issue and mention it.
Okay, hope that helps; good luck!
Upvotes: 2