Skip to content

Confusing difference between types and interfaces when intersected with string indexes #24970

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
ekilah opened this issue Jun 14, 2018 · 8 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@ekilah
Copy link

ekilah commented Jun 14, 2018

TypeScript Version: 2.8.3

Search Terms: intersection types and interfaces seem to behave differently. Inconsistent 'property 'x' of type 'y' is not assignable to string index type

Code

interface IJSONObject {
  [key: string]: string | number | boolean | IJSONObject
}

// we want to enforce that some objects have an `id` field
// with a `type`. 
type TIdElement = {
  id: string
}

// here's an object with an `id` and a `day`.
// we've enforced the `id` member with a `type`.
type WithType = TIdElement & {
    day: Date,
}

// we could use an interface to enforce the same idea that
// things have an `id`
interface IIdElement {
  id: string
}

// here's another object with an `id` and a `day`,
// this time enforced with an `interface` instead.
type WithInterface = IIdElement & {
    day: Date,
}

// only the version where `id` was added via an intersection
// with an `interface` causes a type error.
//
// expected behavior: a type error should happen with both,
// because `Date` is not in `IJSONObject`'s index signature
interface ITest extends IJSONObject {
    baz: WithType // no error, why?
    foo: WithInterface    // error, as expected
}

Expected behavior:
a type error should happen with both,
because Date is not in IJSONObject's index signature

Actual behavior:
WithType doesn't cause an error, even though Date shouldn't be allowed.

Playground Link: link

Related Issues: possibly #18075 but I'm not really sure.

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Jun 14, 2018
@RyanCavanaugh
Copy link
Member

Intersection is a higher-order process that can occur during e.g. generic instantiation; intentionally this process can never create errors.

extends happens immediately and we do check that you fulfill the base type's contract.

@ekilah
Copy link
Author

ekilah commented Jun 14, 2018

@RyanCavanaugh I'm not sure I completely understand you. Both baz and foo here are added via extends, and both WithType and WithInterface are constructed with intersections (&).

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Jun 14, 2018

One of the type rules is that T & U is assignable to X if T or Uis assignable toX. {id: string}, one of the constituents of WithType, is assignable to IJSonObject`.

@ekilah
Copy link
Author

ekilah commented Jun 14, 2018

ok, so - given that {id: string} is one of the constituents of both WithType and WithInterface, I guess what you're implying is that there is some rule that allows types to be assignable to interfaces that have string index operators, but interfaces can't be assigned to other interfaces that have string index operators, without themselves also defining a matching string index.

the above behavior is something I've noticed, I believe, but not seen described anywhere with some convincing reasoning.

  • is my description accurate?
  • if so, is there a place where I can read more about this, or see it explained in any more detail?

(at the core of these questions is a deep confusion about the minute differences between type and interface, and it seems to be a topic of little in-depth discussion)

@MrDesjardins
Copy link

I am confused like @ekilah on this one. The interface in that situation has one of the constituents assignable to IJSONObject, why is it rejected? Your (Ryan) first reply is also confusing since both (type and interface) are using the intersection, and both are a member of a class that extends an interface.

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Jun 18, 2018

The spec defines the rules: https://github.com/Microsoft/TypeScript/blob/master/doc/spec.md

There is not a single place that explains all emergent behavior. Sometimes we define that X causes Y, and B causes C, and P causes Q, and M causes N, then someone notices that X + B - P + Q isn't identical to X + B - P + M even though M and Q are supposed to be pretty similar - it isn't so much that we specifically designed the X + B - P + M rule and have written down a convincing explanation of why that should be the case, but rather than the initial five propositions are all individually sensible and cannot be changed without making a lot of other things worse.

@ghost
Copy link

ghost commented Oct 30, 2019

I also have been baffled by this distinction. To give another explanation on @MrDesjardins and @ekilah questions: My guess is that type literals work in this case, because they get assigned an implicit index signature, whereas interfaces index signatures have to be declared explicitly. Test case:

interface A { [K: string]: string }
interface B { foo: string }
type O = { foo: string }

type BExtendsA = B extends A ? true : false // false
type OExtendsA = O extends A ? true : false // true

So, WithType compiles, as on of the intersection parts TIdElement gets an implicit index signature and is then assignable to IJSONObject. And both parts of WithInterface aren't sub types of IJSONObject.

type T1 = { foo: WithInterface } extends IJSONObject ? true : false // false
type T2 = { foo: WithType } extends IJSONObject ? true : false // true
// true, TIdElement has implicit index signature
type T3 = TIdElement extends IJSONObject ? true : false 
// false, no index sign. for IIdElement 
type T4 = IIdElement extends IJSONObject ? true : false 

@ekilah
Copy link
Author

ekilah commented Oct 30, 2019

@Ford04 that's an interesting angle, and I really appreciate you sharing it. Your explanation goes above and beyond any of @RyanCavanaugh 's three "attempts" to half-answer this question or provide some insight after already closing it as if it were obvious. I haven't seen mention of this "implicit index signature" before, but that issue you linked seems to describe a similar issue. Unfortunately it seems like people there have mostly been left to figure it out themselves as well.

For what it's worth, to this day I've never found a good resource that actually describes how types and interfaces differ internally or how we, as external clients of the language, should use them differently, and after using TypeScript for 1.5 years professionally now I think that is simply ridiculous. Here we are, trying to use the language as intended, and when we run across weird behaviors that don't make a lot of sense, the answer is not much more than a "working as intended" label.

@RyanCavanaugh I invite you and your team back to read this thread all the way through to see if you can give us a little better of an answer, all this time later. Individually, none of your answers above make much sense, and together, they sound fairly incoherent. With all the advances of the type system lately, it surprises me that these types of questions are still left mostly unanswered.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

3 participants