Skip to content

Functioning NoInfer behavior in generic conditional typesΒ #57050

Closed as not planned

Description

πŸ” Search Terms

issue #14829
pull #56794 (The pull for 14829)

βœ… Viability Checklist

⭐ Suggestion

These are conditional type examples in the 2.8 release doc:

type Foo<T> = T extends { a: infer U; b: infer U } ? U : never;
type T10 = Foo<{ a: string; b: string }>; // string
type T11 = Foo<{ a: string; b: number }>; // string | number
type Bar<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void }
  ? U
  : never;
type T20 = Bar<{ a: (x: string) => void; b: (x: string) => void }>; // string
type T21 = Bar<{ a: (x: string) => void; b: (x: number) => void }>; // string & number

The proposal is that the such generic types support NoInfer, for example:

type Foo<T> = T extends { a: infer U; b: NoInfer<infer U> } ? U : never;
type T10 = Foo<{ a: string; b: string }>; // string
type T11 = Foo<{ a: string; b: number }>; // (expecting string)
type Bar<T> = T extends { a: (x: infer U) => void; b: (x: NoInfer< infer U>) => void }
  ? U
  : never;
type T20 = Bar<{ a: (x: string) => void; b: (x: string) => void }>; // string
type T21 = Bar<{ a: (x: string) => void; b: (x: number) => void }>; // (expecting string)

πŸ“ƒ Motivating Example

Testing the latest dev version with pull #56794, these results were obtained:

type Foo<T> = T extends { a: infer U; b: NoInfer<infer U> } ? U : never;
>Foo : Foo<T>
>a : U
>b : NoInfer<U>

type T10 = Foo<{ a: string; b: string }>; // string
>T10 : string
>a : string
>b : string

type T11 = Foo<{ a: string; b: number }>; // never (expecting string)
>T11 : never                     <- WAS EXPECTING STRING
>a : string
>b : number


type Bar<T> = T extends { a: (x: infer U) => void; b: (x: NoInfer< infer U>) => void }
>Bar : Bar<T>
>a : (x: infer U) => void
>x : U
>b : (x: NoInfer<infer U>) => void
>x : NoInfer<U>

  ? U
  : never;

type T20 = Bar<{ a: (x: string) => void; b: (x: string) => void }>; // string
>T20 : string
>a : (x: string) => void
>x : string
>b : (x: string) => void
>x : string

type T21 = Bar<{ a: (x: string) => void; b: (x: number) => void }>; // (expecting string)
>T21 : never                     <- WAS EXPECTING STRING
>a : (x: string) => void
>x : string
>b : (x: number) => void
>x : number

The location marked with <- WAS EXPECTING STRING show results of never where string was expected.

πŸ’» Use Cases

  1. What do you want to use this for?
    • generic type inference

Activity

  1. changed the title [-]NoInfer behavior in conditional types[/-] [+]Functioniing NoInfer behavior in generic conditional types[/+] on Jan 13, 2024
  2. changed the title [-]Functioniing NoInfer behavior in generic conditional types[/-] [+]Functioning NoInfer behavior in generic conditional types[/+] on Jan 13, 2024
  3. ahejlsberg commented on Jan 16, 2024

    @ahejlsberg
    Member

    Those results are as expected. No inferences are made from the b properties, so type string is inferred from the a properties. Then, when the inferred type string is substituted for U, the b properties fail to match and the conditional types resolve to never.

  4. craigphicks commented on Jan 16, 2024

    @craigphicks
    Author

    So here the "No" in "NoInfer" stands for "No!", as in "Veto". It's not uncoupled.

  5. fatcerberus commented on Jan 16, 2024

    @fatcerberus

    You were expecting NoInfer<infer U> to act as an unknown in the check clause of the conditional type? If that's the case you could have just written that. NoInfer means "don't use the type parameter here to guide inference" (so NoInfer<infer U> is an oxymoron); it still gets substituted for a concrete type if it can be inferred from elsewhere.

  6. craigphicks commented on Jan 16, 2024

    @craigphicks
    Author

    Bruce Pascoe (@fatcerberus)

    If that's the case you could have just written that. NoInfer means "don't use the type parameter here to guide inference"

    It would be nice if you could do that without rewriting the whole type except for the parts you don't want. I will explain that more clearly below. I was wrong again :( Just need to Omit the troublesome props.

  7. craigphicks commented on Jan 16, 2024

    @craigphicks
    Author

    Using the test file below some good and some confusing (only to me) results were obtained:

    Without any NoInfer we get results

    type T01 = G01<A<string> & B<string>>;
    >T01 : string | B<string>                                   //  NOT WANTED
    
    type T02 = G02<A<string> | B<string>>;
    >T02 : string                                                      // WANTED
    

    After adding NoInfer to the original types iterators, the results become:

    type T10s = G10<A1<string> & B1<string>>;
    >T10s : string                                              // WANTED
    
    type T11s = G10<A1<string> | B1<string>>;
    >T11s : never                                             // NOT WANTED

    and then the UNWANTED result has changed to WANTED, but unfortunately the previously WANTED result has changed to UNWANTED.

    Finally I can do as Bruce Pascoe (@fatcerberus) wisely suggests, without using NoInfer -

    type RemIter<X extends {[Symbol.iterator]:any} > = Omit<X, typeof Symbol.iterator>;
    type RemIterA<T> = Omit<A<T>, typeof Symbol.iterator>;
    type RemIterB<T> = Omit<B<T>, typeof Symbol.iterator>;
    
    type G20<AB> = AB extends RemIterA<infer U> & RemIterB<infer U> ? U : never;
    type G21<AB> = AB extends RemIterA<infer U> | RemIterB<infer U> ? U : never;
    
    type T20s = G20<A<string> & B<string>>;
    type T21s = G21<A<string> | B<string>>;
    

    to get

    type T20s = G20<A<string> & B<string>>;
    >T20s : string              //  WANTED 
    
    type T21s = G21<A<string> | B<string>>;
    >T21s : string              //  WANTED
    

    which does indeed work perfectly. And although it is a bit verbose, it is applicable without leaving any trace of NoInfer to clean up later, which might be an advantage anyway.

    That cleared up my understanding of NoInfer! Thank you Anders Hejlsberg (@ahejlsberg)! Hopefully this conversation wasn't wasn't a waste and might be even be helpful to others who might think of using NoInfer where they should be locally omitting a property before inference, as in the final example.

    Test File

    test file

    // @strict: true
    // @declaration: true
    // @target: esnext
    
    interface A<T> {
        t:T;
        f(x:T):T;
        [Symbol.iterator]():IterableIterator<T>;
    }
    
    interface B<T> {
        t:T;
        f(x:T):T;
        [Symbol.iterator]():IterableIterator<B<T>>;
    }
    
    
    type G01<AB> = AB extends A<infer U> & B<infer U> ? U : never;
    type G02<AB> = AB extends A<infer U> | B<infer U> ? U : never;
    
    type T01 = G01<A<string> & B<string>>;
    type T02 = G02<A<string> | B<string>>;
    
    
    interface A1<T> {
        t:T;
        f(x:T):T;
        [Symbol.iterator]():IterableIterator<NoInfer<T>>;
    }
    
    interface B1<T> {
        t:T;
        f(x:T):T;
        [Symbol.iterator]():IterableIterator<B<NoInfer<T>>>;
    }
    
    type G10<AB> = AB extends A1<infer U> & B1<infer U> ? U : never;
    type G11<AB> = AB extends A1<infer U> | B1<infer U> ? U : never;
    
    type T10s = G10<A1<string> & B1<string>>;
    type T11s = G10<A1<string> | B1<string>>;
    
    
    type RemIter<X extends {[Symbol.iterator]:any} > = Omit<X, typeof Symbol.iterator>;
    type RemIterA<T> = Omit<A<T>, typeof Symbol.iterator>;
    type RemIterB<T> = Omit<B<T>, typeof Symbol.iterator>;
    
    
    type G20<AB> = AB extends RemIterA<infer U> & RemIterB<infer U> ? U : never;
    type G21<AB> = AB extends RemIterA<infer U> | RemIterB<infer U> ? U : never;
    
    type T20s = G20<A<string> & B<string>>;
    type T21s = G21<A<string> | B<string>>;
  8. typescript-bot commented on Jan 19, 2024

    @typescript-bot
    Contributor

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

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

    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