Skip to content

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

Description

@ekilah

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.

Activity

  1. RyanCavanaugh commented on Jun 14, 2018

    @RyanCavanaugh
    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.

  2. ekilah commented on Jun 14, 2018

    @ekilah
    Author

    Ryan Cavanaugh (@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 (&).

  3. RyanCavanaugh commented on Jun 14, 2018

    @RyanCavanaugh
    Member

    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`.

  4. ekilah commented on Jun 14, 2018

    @ekilah
    Author

    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)

  5. MrDesjardins commented on Jun 16, 2018

    @MrDesjardins

    I am confused like Monroe Ekilah (@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.

  6. RyanCavanaugh commented on Jun 18, 2018

    @RyanCavanaugh
    Member

    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.

  7. ekilah commented on Oct 30, 2019

    @ekilah
    Author

    Ford04 that's an interesting angle, and I really appreciate you sharing it. Your explanation goes above and beyond any of Ryan Cavanaugh (@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.

    Ryan Cavanaugh (@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.

  8. locked as resolved and limited conversation to collaborators on Oct 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Working as IntendedThe behavior described is the intended behavior; this is not a bug

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions