steinybot
steinybot

Reputation: 6163

Does this Typescript example violate the Liskov Subtitution Principle?

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

Answers (1)

jcalz
jcalz

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:

  • object literals / anonymous types are given implicit index signatures
  • type aliases are given implicit index signatures
  • interfaces are NOT given implicit index signatures

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!

Link to code

Upvotes: 2

Related Questions