Skip to content
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

Functioning NoInfer behavior in generic conditional types #57050

Closed
6 tasks done
craigphicks opened this issue Jan 13, 2024 · 6 comments
Closed
6 tasks done

Functioning NoInfer behavior in generic conditional types #57050

craigphicks opened this issue Jan 13, 2024 · 6 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@craigphicks
Copy link

craigphicks commented Jan 13, 2024

πŸ” 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
@craigphicks craigphicks changed the title NoInfer behavior in conditional types Functioniing NoInfer behavior in generic conditional types Jan 13, 2024
@craigphicks craigphicks changed the title Functioniing NoInfer behavior in generic conditional types Functioning NoInfer behavior in generic conditional types Jan 13, 2024
@ahejlsberg
Copy link
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.

@ahejlsberg ahejlsberg added the Working as Intended The behavior described is the intended behavior; this is not a bug label Jan 16, 2024
@craigphicks
Copy link
Author

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

@fatcerberus
Copy link

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.

@craigphicks
Copy link
Author

craigphicks commented Jan 16, 2024

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

@craigphicks
Copy link
Author

craigphicks commented Jan 16, 2024

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 @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 @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>>;

@typescript-bot
Copy link
Collaborator

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

@typescript-bot typescript-bot closed this as not planned Won't fix, can't repro, duplicate, stale Jan 19, 2024
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

4 participants