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

Allow use of infer in the type parameter of a type declaration #52791

Open
5 tasks done
dinofx opened this issue Feb 15, 2023 · 6 comments
Open
5 tasks done

Allow use of infer in the type parameter of a type declaration #52791

dinofx opened this issue Feb 15, 2023 · 6 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@dinofx
Copy link

dinofx commented Feb 15, 2023

Suggestion

For many type declarations, it shouldn't be necessary to use a conditional type which repeats a constraint already found in type parameter.

For example, instead of:

type ConstructorParameters<T extends abstract new (...args: any    ) => any> =
                           T extends abstract new (...args: infer P) => any ? P : never;

This would be better expressed as:

type ConstructorParameters<T extends abstract new (...args: infer P) => any> = P;

🔍 Search Terms

infer type parameter conditional type type declaration

✅ Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

See above

📃 Motivating Example

TypeScript's own built-in types have other examples where constraints are repeated in both a type parameter and conditional type. I've also seen cases where developers just don't want to repeat constraints, so they define:

type RecordValueType<T> =
    T extends Record<any, infer V> ? V : never;

And then incorrect usage results in a type of never, rather than an error.

💻 Use Cases

  • Improved readability
  • Less repetition when authoring type declarations
  • Less frustration when consuming lazily-declared types
@RyanCavanaugh
Copy link
Member

I doubt this would meet the bar for inclusion but we can listen for feedback.

There'd probably have to be some new syntax for this, since infer can already appear inside a constraint position with a different meaning:

type J = string;
// RHS "J" here must refer to outer J, not infer J
type M<T, U extends (T extends {a: infer J } ? J : number)> = J;
type ConstructorParameters<T extends abstract new (...args: infer P) => any> = P;

This is sort of confusing from a lexical scoping perspective. infer currently only introduces an identifier into the truthy branch of a conditional type, but what's being proposed here would be available... to other type parameters? Only in the RHS? Not super clear what the right answer is.

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature labels Feb 16, 2023
@dinofx
Copy link
Author

dinofx commented Feb 17, 2023

I don't completely follow the counter-example. infer J above is inside a constraint position, but it's a conditional constraint. When using infer in an "unconditional" constraint, today you get the error:

'infer' declarations are only permitted in the 'extends' clause of a conditional type

Regarding scoping, if infer could be used in an "unconditional" constraint, then the entire RHS becomes the truthy branch. Similar to conditional infers, you couldn't refer to the type elsewhere in the constraint (although perhaps you could infer the type from multiple locations).

This feels like a logical progression following the addition of "extends Constraints on infer Type Variables" added in 4.7. When you use a parameterized type it is already a kind of conditional. The difference is if you don't satisfy the condition, you get a compile error rather than an alternate type.

@kenbellows
Copy link

I'm on board with this bigtime. I actually just asked a question about this exact thing on StackOverflow yesterday and got a comment pointing me here. I absolutely think this makes sense as a feature. When I learned about the infer keyword yesterday, my immediate first thought was, "Why is this only available in conditionals, and not generics broadly?" Thus my SO question. It strikes me as the more natural place for them, honestly. Not saying that they aren't also extremely useful within conditional scopes, but I'm surprised that's where they were added first and exclusively.

@kenbellows
Copy link

@RyanCavanaugh I don't think your example raises a problem at all. That's just basic scoping and variable shadowing stuff, isn't it? It looks like there's essentially a little scope created by the conditional statement, within the parentheses as you've written it:

(T extends {a: infer J } ? J : number)

I don't think I'd expect that J to be available outside this little scoped ternary, even if infer was possible in the generic. And isn't this sort of confusing syntax already possible today, just on the right hand side? I'm thinking about something like this:

type J = string;
type M<T> = [T extends {a: infer J } ? J : number, J];

In this example, the J in the first element of the RHS array refers to the inferred type, and the J in the second element refers to the outer type. So something like this works fine:

let x:M<{a: Set<number>}> = [new Set(), 'foo']

No TS errors. I think this particular point is moot. It's confusing only if you choose conflicting type names, you know? I don't think it's confusing for the parser.

@dinofx
Copy link
Author

dinofx commented May 5, 2023

This would be useful elsewhere too:

type Wrapper<T> = {
  wrapped: T;
}
function unwrap<W extends Wrapper<V>, V = W extends Wrapper<infer U> ? U : never>(w: W): V {
  return w.wrapped;
}
const result = unwrap({wrapped: 42});

When this would be preferred (maybe even without , V?):

function unwrap<W extends Wrapper<infer V>, V>(w: W): V {
  return w.wrapped;
}

@fatcerberus
Copy link

FWIW implementing this might change behavior since it requires TS to infer type variables from a constraint, which, at present, it doesn't do at all:

function foo<U, T extends Array<U>>(array: T): Array<NonNullable<U>> {
  // ...
};

const food: string[] = [ "foo", "bar" ];
foo(food);  // infers U = unknown

From what I understand, this is because inferring from constraints often produces bad inferences.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

4 participants