Skip to content

Unexpected error when generic extends recursive type (alias vs interface). #49751

Closed

Description

Bug Report

You'd expect alias vs interface to behave indifferently when used as a generic argument. Moreover, when defining a shape, the use of interface should be preferred as, to my understanding, it is more semantically correct and has better error reporting.

However it particularly fails in the following piece of code when passed to a generic parameter extending a recursive type.

Is this error intended? If so, what is the intended difference to be represented here?

Thank you.

🔎 Search Terms

alias interface error generic extends recursive json type

🕗 Version & Regression Information

v4.7.4

⏯ Playground Link

Playground link with relevant code

💻 Code

type Primitive = string | number | bigint | boolean;

interface Layout {
    [k: string]: Primitive | Array<Primitive | Layout> | Layout;
}

type Test<T extends Layout> = T;

type MyLayout1 = {
    outer: Array<{
        inner: string;
    }>;
}
interface MyLayout2 {
    outer: Array<{
        inner: string;
    }>;
}

type A = Test<MyLayout1>;
type B = Test<MyLayout2>; // ERROR!

🙁 Actual behavior

Error

🙂 Expected behavior

No error

Activity

  1. fatcerberus commented on Jul 1, 2022

    @fatcerberus

    The error has nothing to do with the type being recursive or not. It happens because plain { ... } object types have an implied index signature (this is what allows you to assign object literals to Record types), while interfaces are only assignable to index-signature types if the interface has a compatible index signature itself. The error message tells you exactly what's wrong:

    Type 'MyLayout2' does not satisfy the constraint 'Layout'.
      Index signature for type 'string' is missing in type 'MyLayout2'.
    

    This is in fact part of the "better error reporting" for interfaces that you allude to. 😉

  2. miguel-leon commented on Jul 1, 2022

    @miguel-leon
    Author

    Alright, thank you for the explanation Bruce Pascoe (@fatcerberus).

    However, the center of the issue is the ability to have a generic parameter extend a recursive type.
    Does the actual reason means that interfaces can't be used as arguments to these types of generic?
    Or does it means that having a generic extend a recursive type is not supported, is not a thing?

    Is the consequence for this error intended or is it a bug? If so, what is the intended difference? Does it have sense to have this error here?

    You understand what I mean?

  3. fatcerberus commented on Jul 1, 2022

    @fatcerberus

    Like I said, it has nothing to do with the recursion and everything to do with the index signature in Layout. If Layout were instead { foo: Primitive | Array<Primitive | Layout> | Layout } with corresponding types for MyLayout1 and MyLayout2 then the interface/POJO distinction wouldn't matter.

    But yes, if your generic is constrained to a type with an index signature, then you can't use interfaces with it unless they also have a compatible index signature.

  4. miguel-leon commented on Jul 1, 2022

    @miguel-leon
    Author

    It's ok. I understand what you're saying, and I'm sorry if my example feels like I'm asking about that particular error and nothing more. It just happened to be the one that I could use because of the semantics.

    But as I just mentioned, that is not the real issue I'm trying to highlight. It's what's behind, the consequence. I don't know how else to put it. The developer intention (because it seems to work as would be the intention using the type alias).
    My questions remain unanswered.

    Thanks anyway.

  5. fatcerberus commented on Jul 1, 2022

    @fatcerberus

    Then I have absolutely no idea what you're asking, because the error you're pointing out is due to the lack of index signature in an interface and has nothing at all to do with your generic type being recursive.

    The problem you're running into boils down to this...

    interface Fooey { [x: string]: string } 
    type Foo1 = { foo: string };
    interface Foo2 { foo: string };
    let foo1: Foo1 = { foo: "foobar" };
    let foo2: Foo2 = { foo: "barfoo" };
    let x: Fooey = foo1;  // ok
    let y: Fooey = foo2;  // error, missing index signature

    ...which, yes, is by design.

  6. fatcerberus commented on Jul 1, 2022

    @fatcerberus

    For the record: If your real question here wasn't about the specific error and was more of "how do I do this?" - this is not a support forum.

  7. somebody1234 commented on Jul 1, 2022

    @somebody1234
  8. miguel-leon commented on Jul 1, 2022

    @miguel-leon
    Author

    You sound a little bit annoyed by not being able to understand and now implying I'm misusing this place as a support forum.
    So let me break it down and try to explain it the simplest I can in steps.

    1. I'm putting a generic argument to guard for (extends) a recursive type, which itself is "generic" in the sense that their keys are whatever string (not symbols, not numbers).

    2. The (only) way to declare it is by having to use the "index signature" (for better or for worst, that's not the point).

    3. I then have a type MyLayout2 that semantically complies with this recursive type (in other words, it is substitutable).

    4. But as you already know, it doesn't work. However the functionally and semantically equivalent MyLayout1 (also substitutable) does work.

    • Question: Is the fact that, the types substitutable to Layout, one works and the other does not a design? If so, what's the reasoning? or is it a bug? or is it because Typescript lacking features to better declare the recursive type?, etc.

    I understood you the first time. It is not the nature of that particular error I'm reporting about. Please stop going there like in your previous examples with fooey, otherwise the comments on this issue will get very confusing as to what is tried to be discussed. No need to explain it again. Not a support forum after all.

    Thank you.

  9. somebody1234 commented on Jul 1, 2022

    @somebody1234

    except it doesnt semantically comply. Layout requires arbitrary keys to be valid, MyLayout2 does not, hence MyLayout2 is not substitutable.

  10. fatcerberus commented on Jul 1, 2022

    @fatcerberus

    I understood you the first time. It is not the nature of that particular error I'm reporting about.

    Then I have no clue what you want me to say. This particular error is the only thing preventing your code as written from working, so if you already understand why that error happens, there is no further question to answer. There is no bug here; your code will need to be refactored.

  11. miguel-leon commented on Jul 1, 2022

    @miguel-leon
    Author

    well.. this seems like the answer is the question itself and we'll just keep going around.

    nevermind then, maybe it's just that Typescript lacks more advanced support for doing something like this and I mistook it for a fault in design (what I tried to report).

    because, bottom line, from certain point of view it doesn't make sense, it's like there's no way to guard an interface from being something like a JSON-like object (not containing functions or classes).

    and it certainly feels like the fact that it works declaring it as a type alias is just a fluke. It's not consistent.

    but with you guys just focused around the technicality of the index signature, this is going nowhere. I'm just closing then because there's no deeper insight being took.

    thank you.

  12. fatcerberus commented on Jul 1, 2022

    @fatcerberus

    but with you guys just focused around the technicality of the index signature

    Because that’s literally the source of the behavior you’re questioning. You are correct that it is not consistent, but the inconsistency is by design: the type alias working there is convenient, but not safe in general, so you can use interface to get stricter behavior.

    You will find many inconsistencies like this in TypeScript, because soundness is not a design goal. The design of the type system is largely guided by pragmatism in practice.

  13. somebody1234 commented on Jul 1, 2022

    @somebody1234

    for posterity: the index signature is part of the type, not a technicality. and so this is 100% equivalent to saying this:

    interface Layout {
      a: string;
    }
    
    interface MyLayout {
      b: string;
    }
    
    // why doesn't MyLayout extend Layout :(
  14. miguel-leon commented on Jul 1, 2022

    @miguel-leon
    Author

    Great. I accept the response of convenience making the inconsistencies by design. That was what my report was about.

    also for posterity:

    function fn(x: Layout) {
        x.foo = 123; // shouldn't happen if x type not strictly equal to Layout
    }
    
    const x: MyLayout1 = { outer: [{ inner: '' }]};
    
    fn(x); // should not be possible. a generic <T extends Layout> should be required.

    The reason: convenience and practicality. Instead of making generics more difficult, let's just have it work with type alias but not with interfaces through the feature of index signatures.

  15. locked as resolved and limited conversation to collaborators on Oct 22, 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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions