Skip to content

In function argument, applying a generic type that extends an object type to a conditional type operator doesn't specialise #56011

Closed as not planned

Description

🔎 Search Terms

"inference conditional", "automatic inference", "generic function argument", "object type"

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about type system behaviour, generics, type guards

⏯ Playground Link

Link

💻 Code

type Vals = string | { [key: string]: Vals };
type ObjVal = { [key: string]: Vals };
type Id<T extends Vals> = T extends ObjVal
  ? { [K in keyof T]: Id<T[K]> }
  : string
type IdObj<T extends ObjVal> = { [K in keyof T]: Id<T[K]> };

type Family = {
  a: Vals;
};

function unexpectedlyDoesntWork<B extends Family>(input: Id<B>): void {
  input.a satisfies Id<B["a"]>;
}

function worksFine<B extends Family>(input: IdObj<B>): void {
  input.a satisfies Id<B["a"]>;
}

🙁 Actual behavior

In unexpectedlyDoesntWork, input seems to be type inferred as Id<Vals> i.e. Vals, and so input.a is inferred as Vals. Hence the satisfies clause fails. This is not the most precise interpretation, since B is known to extend Family and hence be of object type with a member a, and so Id<B> should specialise to { [K in keyof B]: Id<B[K]> }.

🙂 Expected behavior

I expect input.a to be inferred as type Id<B["a"]> . This occurs in the worksFine function, where the only difference is that the function value has applied the non-conditional type operator IdObj to B.

Additional information about the issue

No response

Activity

  1. MartinJohns commented on Oct 6, 2023

    @MartinJohns
    Contributor

    It's a design limitation. There are numerous issues about this. Resolving of conditional types involving unbound generic type arguments are deferred until the type is known. In your example the type of B is not known within the function, so the conditional type is not resolved.

    Duplicate of #53455 and others.

  2. johanrosenkilde commented on Oct 6, 2023

    @johanrosenkilde
    Author

    Thank you for the swift response! I see. And by "design limitation" you mean that there is no intention of trying to support this, even if it is theoretically possible?

    I want to add that it seems to me that this case is more resolvable than #53455. In that issue, it's theoretically impossible to resolve the conditional (in isolation within the body of the function) because the unbound generic type U could either extend undefined or not.

    Contrarily, in the example of this issue, the unbound generic type B extends Family and so must be an object type, which means that the conditional could be resolved.

    In other words, and with naivety of the hairy details of the TypeScript type checker, I don't see why conditionals that can be resolved at type checking time necessarily need to be deferred.

  3. fatcerberus commented on Oct 6, 2023

    @fatcerberus

    There are actually cases where the compiler can resolve conditional types over generics if it can prove they always resolve a particular way. However, in your given example, the conditional type is distributive (i.e. it maps over unions) - which to my knowledge is always deferred because the compiler has no way of knowing how many times the conditional has to be applied. It might even be 0 times, if the input type happens to be never!

  4. fatcerberus commented on Oct 6, 2023

    @fatcerberus

    btw, your Playground and posted code differ. The issue form tells you not to do this

    This code and the Playground code should be the same, do not use separate examples.

  5. johanrosenkilde commented on Oct 6, 2023

    @johanrosenkilde
    Author

    btw, your Playground and posted code differ. The issue form tells you not to do this

    Sorry, I realized after posting the first link that I could simplify the example. After doing so, I forgot to refresh the link to the playground. Will do so now.

  6. johanrosenkilde commented on Oct 6, 2023

    @johanrosenkilde
    Author

    However, in your given example, the conditional type is distributive (i.e. it maps over unions) - which to my knowledge is always deferred because the compiler has no way of knowing how many times the conditional has to be applied. It might even be 0 times, if the input type happens to be never!

    In my example the first application of the conditional may be statically determined: since B extends Family and Family is an object type, then B must be an object type. Except if B is never, but that seems like a special case that is anyway treated differently in the conditional (e.g. Id<never> is never despite this not being either of the return types of the conditional).

  7. fatcerberus commented on Oct 6, 2023

    @fatcerberus

    Id<never> is never because never is considered an empty union, so there are no types to evaluate the conditional against. It’s like .map() over an empty array.

    Anyway, you know that B is an object type, but you don’t know whether it’s a union of object types - in which case Id<B> would also resolve to a union. TS must therefore defer it because it doesn’t know how many times it will have to apply the conditional. Now you might say here, well, the type you get will always be compatible, but assignability checks don’t work that way; there’s no higher-order machinery that can say “this will always resolve to a compatible type and is therefore assignable” without actually being able to resolve it. It’s just not part of the design of the compiler. Hence, Design Limitation.

  8. typescript-bot commented on Oct 9, 2023

    @typescript-bot
    Contributor

    This issue has been marked as "Duplicate" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

  9. johanrosenkilde commented on Oct 9, 2023

    @johanrosenkilde
    Author

    Thanks for explaining this, Bruce Pascoe (@fatcerberus)

    there’s no higher-order machinery that can say “this will always resolve to a compatible type and is therefore assignable” without actually being able to resolve it. It’s just not part of the design of the compiler. Hence, Design Limitation.

    Of course, I can't argue with this not knowing the design of the compiler. It's just still surprising to me that such a mechanism should be problematic to implement. It seems to me a matter of implementing a pattern match saying that a type internally represented as:

    [T := Possible Union<B extends some object type>] extends object type ? trueCase<T> : falseCase<T>
    

    may immediately be reduced internally to

    PossibleUnion<trueCase<B extends some object type>
    

    But anyway, issue closed, I'll just have to work around it, and please don't feel you need to respond to this.

  10. fatcerberus commented on Oct 9, 2023

    @fatcerberus

    Johan Rosenkilde (@johanrosenkilde) FWIW, your PossibleUnion abstraction illustrates the issue quite well - notice that still exists in the imagined reduced type. So the partially resolved type still necessarily has a quantifier and TS doesn't reason about that kind of thing in type checks. It just sees a deferred type. That's what I meant about the lack of higher-order machinery. I don't expect the compiler to be rearchitected to support that, but stranger things have happened when Anders Hejlsberg (@ahejlsberg) drops a PR out of nowhere so...1 😅

    Footnotes

    1. Nope, Design Limitation isn't necessarily a death sentence for an issue. It just means "we can't implement/fix this without adding new abstractions and possibly rethinking how adjacent features work." ↩

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

    DuplicateAn existing issue was already created

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions