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

Proposal: Partial Type Argument Inference #26242

Open
weswigham opened this issue Aug 6, 2018 · 60 comments
Open

Proposal: Partial Type Argument Inference #26242

weswigham opened this issue Aug 6, 2018 · 60 comments
Assignees
Labels
Needs Proposal Suggestion

Comments

@weswigham
Copy link
Member

@weswigham weswigham commented Aug 6, 2018

After exploring the concept in #23696, we've come to the conclusion that implicitly making type arguments available by name would unnecessarily expose previously unobservable implementation details. As such, I'll be breaking that proposal down into two parts. One is just the partial inference (which is here), which can stand on its own, as discussed in #20122 and #10571.

To recap, partial type argument inference is the idea that some number of type arguments in a type argument list may be provided, while others are elided and fulfilled via inference using the rest. Specifically, in #10571 a number of different syntax proposals will brought forward:

For the following examples, assume we have a

declare foo<A,B,C>(): [A, B, C];

Variant 1 - elided entries

foo<,,string>(); // returns [{}, {}, string]

This is the most terse option, as simply the lack of inputs implies inference sites. This would almost seem to logically follow from how not providing a list also causes inference to occur at all sites. This does have issues, however: specifying a final parameter as needing inference would require a trailing , in the list (something we currently explicitly disallow), and lists could very easily become confusing, as a , is very easy to skip over.

Variant 2 - Sigil marker

foo<*, *, string>(); // returns [{}, {}, string]

As the second most terse option, this also has appeal; however I think it also fails on a few points. First, * is non-obvious what it means; it implies a "wildcard" of some kind, but in the context of types that could mean an inferred type, a bound, or an existential. Second, as a single-character sigil, we're unlikely to meaningfully provide completions for it even though it is contextually relevant. Finally, we're considering other work to do with existentials and generated type parameters in the future which we'd like to be able to use the * as an indicator for.

Variant 3 - Keyword marker

a. auto

foo<auto, auto, string>(); // returns [{}, {}, string]

b. infer

foo<infer, infer, string>(); // returns [{}, {}, string]

Neither of these are as terse as the others, but both are still likely substantially shorter than providing the entire list of types by hand in situations where partial inference is desired. Of the two keywords, auto may be shorter, but currently carries no meaning within the language. infer on the other hand is already used for marking inference positions within conditional types. The infer method was explored in #22368, however was taken much father - almost fully replicating the arbitrary placement and naming the operator affords within conditional types.

In the end, I'm advocating for variant 3b - the infer placeholder, with none of the extra features afforded in #22368 (we can always add them later if there is demand).

@RyanCavanaugh RyanCavanaugh added Suggestion In Discussion labels Aug 6, 2018
@treybrisbane
Copy link

@treybrisbane treybrisbane commented Aug 7, 2018

My vote is for either 1 or 3b :)

@AlCalzone
Copy link

@AlCalzone AlCalzone commented Aug 7, 2018

Maybe adopt the ? from the partial application proposal

foo<?, ?, string>(); // returns [{}, {}, string]

which already stands for optional in TypeScript. Actually, the meaning of would be pretty similar to the proposal and could allow for something like this:

type Foo<T, U, V> = T | U | V;
type Bar = Foo<?, string, ?>; // equal to type Bar<A, B> = A | string | B;

@jsiwhitehead
Copy link

@jsiwhitehead jsiwhitehead commented Aug 9, 2018

Will this include the option for simply omitting trailing type arguments, and having them automatically set as inferred?

Ie for these to be equivalent:

foo<string, infer, infer>();

foo<string>();

@weswigham
Copy link
Member Author

@weswigham weswigham commented Aug 9, 2018

@jsiwhitehead consider:

declare const foo: {
  <A, B>(): A & B;
  <A>(): A;
};
foo<string>(); // The signature this refers to would be ambiguous if `infer`s were autofilled

So I don't think so - those likely won't be equivalent.

@jsiwhitehead
Copy link

@jsiwhitehead jsiwhitehead commented Aug 9, 2018

@weswigham sorry I wasn't fully clear, I meant in the case when the trailing type arguments are optional / have defaults, so in situations where they can already be left out.

The change I'm asking about is how the unprovided optional types are resolved. Currently there are two different cases:

  • No types are provided explicitly => all types are inferred
  • At least one type is provided explicitly => no inference, all unprovided types just take their default

Hopefully this proposal for partial inference could include allowing inference to continue working in the second case (as requested in #19205, which is closed as a duplicate of #10571).

E.g.

declare function foo<A, B = any>(b: B): [A, B];

foo<number>(null); // Resolves to [number, any], should resolve to [number, null]

Or does that also lead to new ambiguities?

@weswigham
Copy link
Member Author

@weswigham weswigham commented Aug 10, 2018

Or does that also lead to new ambiguities

In your example no, for more complex types with multiple signatures, yes. It's also technically a break to do that since we'd suddenly be doing inference where previously people we relying on defaults. By making it explicit with foo<number, infer>(null) we avoid both issues.

@jsiwhitehead
Copy link

@jsiwhitehead jsiwhitehead commented Aug 10, 2018

I'm struggling to see how this could lead to an ambiguity that wasn't already there sorry, even with multiple signatures. The change I'm asking about is only about how unprovided type arguments are resolved, not about the process Typescript uses to choose which signature to use.

E.g. there is an ambiguity in the following, but that is the case already, and currently is just resolved by taking the first possible match, which is fine.

declare const foo: {
  <A, B = any>(b: B): [A, B];
  <A>(b: any): A;
};
foo<string>(null); // This resolves to [string, any], but should resolve to [string, null]

Sorry to keep digging on this, but I'm trying to type a selector based API which will be vastly less usable if infer has to be written for all types:

do<string>('key1', 'key2', ..., 'keyN', (value1, value2, ..., valueN) => ...)

vs

do<string, infer, infer, ..., infer (N times)>('key1', 'key2', ..., 'keyN', (value1, value2, ..., valueN) => ...)

Of course, if the fact that this would technically be a break means it's a no go either way, then that's just how it is!

@sledorze
Copy link

@sledorze sledorze commented Aug 11, 2018

@weswigham We are wondering how this feature would play with a new encoding we're likely to use in fp-ts.

Given:

right: <L = never, A = 'reason is you cannot partially bind Type Params to `right`'>(a: A) => Either<L, typeof a>

What would be the typing for:

// x3: Either<string, number> or Either<string, ???>
const x3 = right<string, infer>(1)

The thread is discussed here:
gcanti/fp-ts#543 (comment)

Thanks in advance.

@opiation
Copy link

@opiation opiation commented Aug 14, 2018

Not sure if I'm a little late to the game here but I'd like to give a simple use case for when this might be be useful.

Given the following:

interface Options<S = {}, Payloads = {}> {
  reducers: {
    [key in keyof Payloads]: (state: S, payload: Payloads[key]) => S
  }
  state: S
}

function identity<S, Payloads = {}>
  (options: Options<S, Payloads>): Options<S, Payloads> {
  return options
}

const options = {
  reducers: {
    add: (state, payload: number) => ({
      ...state,
      answer: state.answer + payload
    })
  },
  state: {
    answer: 42
  }
}

Type inference works wonderfully as expected when no type arguments are supplied to the identity function...

// Both State and ReducerPayloads are inferred correctly provider `state` and `payload` type safety
const fullyInferred = identity(options)

When one explicitly types the S however, type inference for Payloads is lost and defaults to {} despite the inferable type information for Payloads being more specific and arguably safer to use.

// When explicitly specifying the State however, ReducerPayloads is no longer inferred and
// defaults to {}.  We effectively lose type inference for `partiallyInferred.reducers`
const partiallyInferred = identity<{ answer: number }>(options)

Using infer here would allow the API consumer to specify when the inferable type should be used in place of the default.

const partiallyInferred = identity<{ answer: number }, infer>(options)

If there's already a means of achieving this partial inference in this example, feel free to share it here as it would seem quite useful.

@insidewhy
Copy link

@insidewhy insidewhy commented Aug 31, 2018

@jsiwhitehead I feel your pain, I've been writing an API that uses a type involving many generic string literals (they are used to build action creators for ngrx). It's annoying having to iterate every argument even with the version of typescript from this branch.

I wonder if maybe a trailing ** could ease our pain. So instead of needing:

<T, *, *, *, *, *>() => {}

where the last 5 here are string literals and I need to add a new * everytime I need to add a new string literal to my class, I could write:

<T, **>() => {}

to infer all subsequent generics

@flushentitypacket
Copy link

@flushentitypacket flushentitypacket commented Sep 8, 2018

@ohjames I like that. Further, that:

<T>() => {}

is equivalent to

<T, **>() => {}

@insidewhy
Copy link

@insidewhy insidewhy commented Sep 10, 2018

@flushentitypacket Yes, I wish the language was designed that way in the first place, however now it's too late to implement things that way, given it will conflict with default generic parameters?

@aleclarson
Copy link

@aleclarson aleclarson commented Sep 27, 2018

I wonder if maybe a trailing ** could ease our pain.
<T, *, *, *, *, *>() => {} => <T, **>() => {}

Maybe, a trailing "elided entry" could imply all remaining parameters are inferred.

<T,>() => {}

edit: Actually, this would be an issue for multi-line parameters.

@falsandtru
Copy link
Contributor

@falsandtru falsandtru commented Sep 27, 2018

related: #21984

@lukescott
Copy link

@lukescott lukescott commented Dec 17, 2018

Could you simply make trailing types optional, while making leading types required? It would fully depend on how the types were ordered, but I see that as a feature rather than a limitation. Optional function arguments work similarly.

Given:

declare foo<A,B,C>(): [A, B, C];

This means you can do any of these:

foo()
foo<string>()
foo<string,string>()
foo<string,string,string>()

But not:

foo<,string>()
foo<,,string>()
foo<,string,string>()
foo<,string,>()

This wouldn't require any special syntax parsing. It would simply fix the expected arguments error.

@insidewhy
Copy link

@insidewhy insidewhy commented Dec 17, 2018

@lukescott That proposal is here: #10571
It's also how type parameters work in C++. I think TypeScript probably inherited this limitation from C#. Looks like it's being fixed in both languages, will probably land in C# first.

@lukescott
Copy link

@lukescott lukescott commented Dec 17, 2018

@ohjames I do see omitting trailing types mentioned in #10571, but it looks like it advocates for an auto keyword, which would open the door to foo<auto,string>(). Later comments in the issue mention _ and *. I'm not sure I see much of a difference. IMO, leading types should be required. Even with that, making trailing types optional solves a lot of use-cases.

@insidewhy
Copy link

@insidewhy insidewhy commented Dec 17, 2018

@lukescott read the initial comment on that issue, the talk of * etc. is tangential to the issue.

Edit: Maybe it is lacking a bit of focus. If there isn't an issue for simply omitting trailing types then maybe someone should open one. Might it conflict with type parameter defaults though?

@lukescott
Copy link

@lukescott lukescott commented Dec 17, 2018

@ohjames

Maybe it is lacking a bit of focus. If there isn't an issue for simply omitting trailing types then maybe someone should open one.

That was my thinking as well. There is a lot of overlap between each of these proposals, with similar thoughts being shared. I haven't seen any mention to rein this into an MVP. IMO, the current proposals are too broad.

Might it conflict with type parameter defaults though?

I'm not sure how. At least any more than the current proposals would. Requiring leading types is more restrictive. Default types are also restrictive:

function foo<A,B = string,C>(a: A, b: B, c: C) {}
// error on C: Required type parameters may not follow optional type parameters.

Do you have something in mind?

@insidewhy
Copy link

@insidewhy insidewhy commented Dec 18, 2018

@lukescott I think it's something like:

function totoro<A, B = object>(a: A, b: B) { ... }

Currently if I call this function thusly:

totoro('friend', new Date(2018, 3, 19))

Inside of totoro the B will still be of type object. However if we allow omitting parameters, in this case B could be inferred as Date. That would then make this a backwards incompatible change. Honestly I think it'd be better to do it like C++, and have it only fallback to the default type when inference is not possible; but for the sake of not breaking backwards compatibility this level of inference could be restricted to type parameters that do not specify defaults.

@lukescott
Copy link

@lukescott lukescott commented Dec 18, 2018

@ohjames
I think what you're saying is:

function totoro<A, B = object>(a: A, b: B): [A,B] {
	return [a,b]
}

const result1 = totoro('friend', new Date(2018, 3, 19))
const result2 = totoro<string>('friend', new Date(2018, 3, 19))

result1 comes back with [string, Date] and result2 comes back with [string, object]. Inside the function is {} unless you do B extends object = object.

It would seem like either proposal has the same effect on this. In either case you could either change how it works and infer Date, or use object as it currently works.

Personally I would prefer to break compatibility here and use default only when the type can not be inferred.

@insidewhy
Copy link

@insidewhy insidewhy commented Dec 18, 2018

@lukescott Thanks for fixing my example. Glad you agree with the compatibility break but not sure if others will see it the same as us. It's been bothering me ever since C# adopted this limitation. Feel like it takes a lot away without bringing anything.

@Richiban
Copy link

@Richiban Richiban commented Jan 22, 2019

Can't we support a {type} = {Expression} syntax at the call site?

I have a feeling that f<,,,,,,string,,,,,>() will be simply impossible to read with more than three type parameters.

My syntax would allow for supplying just the single type parameter that is causing a complete lack of type inference. For example, when calling the below function f the type D cannot be inferred, but the others can:

function f<A, B, C, D>(a : A, b : B, c : C) : D {
    ...
}

I suggest making it possible to call this with the syntax:

var d = f<D = string>(1, 2, 3);

@ExE-Boss
Copy link
Contributor

@ExE-Boss ExE-Boss commented Feb 13, 2019

I would also like to be able to omit all inferred types when unambiguous:

interface SpecialArray<T> extends Array<T> {}

let a: SpecialArray<_> = [1]
// and
let b: SpecialArray<> = [1]
// are both equivalent to:
let c: SpecialArray<number> = [1];

@RyanCavanaugh RyanCavanaugh added the Needs Proposal label Feb 25, 2019
@mmiszy
Copy link

@mmiszy mmiszy commented May 17, 2021

Since #22368 was closed and not merged, is there a simpler PR coming just for supporting partial generics inference?

@y-nk
Copy link

@y-nk y-nk commented May 20, 2021

Nothing to add except my support for this proposal 🙏

@Stevemoretz
Copy link

@Stevemoretz Stevemoretz commented May 20, 2021

Need this.

@kkirby
Copy link

@kkirby kkirby commented Jun 5, 2021

This would be a great addition to TypeScript. I think, personally, it would make more sense for the generic function to define which generics are inferred (and could be overridden):

function createAction<
    Props extends unknown[],
    Name extends string = infer
>(name: Name): (...args: Props) => { name: Name, props: Props } {
    ...
}

This way, the caller only has to provide the Props generic, and the Name generic would be inferred. But, if for some reason the user wanted to override Name, they could.

@yamiteru
Copy link

@yamiteru yamiteru commented Aug 9, 2021

Since there is still no real solution to this I find currying to be a pretty viable solution:

const x = <I>(v: I) => <O>((v: I) => O) => O

// fully typed
x<number>(10)<string>((n) => `${n * 2}`);

// partial inference NO.1
x(10)<string>((n) => `${n * 2}`);

// partial inference NO.2
x<number>(10)((n) => `${n * 2}`);

// full inference
x(10)((n) => `${n * 2}`);

@schickling
Copy link

@schickling schickling commented Aug 9, 2021

That's also what I've landed on @the-yamiteru but when designing APIs that are supposed to be used by both JS and TS developers this is quite an unfortunate solution as non-TS users now asking themselves why there's another function call needed where's often not strictly needed.

@weswigham is there any movement to be expected on this area?

@yamiteru
Copy link

@yamiteru yamiteru commented Aug 9, 2021

@schickling Yeah that's true. Either way I'm going to assume most of the JS devs use TS and/or they don't mind an extra function call. At least for now. But it's a real pain in the ass when typing system dictates the shape of the implementation.

@insidewhy
Copy link

@insidewhy insidewhy commented Aug 9, 2021

Is there really any scenario where you would want an error instead of inference? 99% of the time, wouldn't inference also be preferable to using the default?

Only the latter even breaks backwards compatibility, you could turn off inference for type arguments with defaults but it would be a symptom of baggage, not a desirable trait.

IMO inference should just be turned on, add it to TypeScript 5.0 so backwards compatibility will be less expected. It may break a few libraries, and these can be fixed, but it'll lead to a much nicer future for TypeScript.

Having to litter my code with = infer when it should be the default... not such a nice future. Bite the bullet.

I think the only desirable usecase for infer is as an argument in order to infer type arguments that are not as the end of the argument list.

@jcalz
Copy link
Contributor

@jcalz jcalz commented Jan 28, 2022

This is one of those issues that keeps. coming. up. in Stack Overflow questions, both directly ("how can I specify this type parameter and have the compiler infer the rest") and indirectly ("yes this solution works but isn't there some way to get rid of that weird seemingly no-op curried function call you're doing at the beginning?")

In the absence of just making <T, U>(u: U) => xxx work magically with when called like f<T>(u), it would still be so much nicer to be able to write <T, U=infer>(u: U) => xxx instead of the current <T,>() => <U,>(u: U) => xxx with f<T>()(u), or <T, U>(dummyT: T, u: U) => xxx with f(null! as T, u).

@yamiteru
Copy link

@yamiteru yamiteru commented Apr 12, 2022

This is my proposal. Right now it's either all or nothing. But we can provide a default type which might prove useful. We might assign a special keyword (infer is the most logical here) as a default type which would tell the TS server that this type should be inferred.

I also believe it should be possible to infer an extended or non-extended type as seen below. Another important feature is type skipping which should work the same way as array destructuring (const [, , c] = arr) since types in a type generic are basically an array of types.

// Proposed syntax
const test = <
  A, // required
  B = infer, // inferred
  C extends string[], // required, extended
  D extends Record<string, number> = infer // inferred, extended
>(a: A, b: B, c: C, d: D) = { ... };

// Types
type AA = boolean;
type BB = "hello" | "ahoj";
type CC = ("one" | "two" | "three")[];
type DD = Record<"one" | "two" | "three", number>;

// Values
const a = true;
const b = "hello";
const c = ["one", "two"];
const d = { "one": 1, "two": 2 };

// Full inference
const x = test(a, b, c, d);

// Partial inference
const y = test<AA, , CC>(a, b, c, d);

// No inference
const z = test<AA, BB, CC, DD>(a, b, c, d);

@infacto
Copy link

@infacto infacto commented May 18, 2022

I would prefer a solution without defining anything as on method call. I want to define it in the method declaration.

Something like:

class MyService {
  public showModal<R, T?, D?>(component: ComponentType<T>, data: DialogConfig<D>): DialogRef<T, R> {
    return this.foo(component, data);
  }
}

In this case I only want to set the return value type R. The other should resolved from args or unknown (default behavior).

// This is expected, but does not work. All 3 types are required.
// Optional generics are not wanted, because I want to infer from args.
const result = await this.showModal<string>(MyComp, MyData);

// With your proposal it would work. But is more to write on every call.
// `infer` or `*` or `?` whatever...
const result = await this.showModal<string, infer, infer>(MyComp, MyData);

I would suggest to just change the behavior. We could declare generics with T? but actually the generics already are optional when not set manually. And required generics are also not build-in supported. But works with something like <T = void> which forces to set types.

Therefore just still handle the remaining generics as currently work when no type is set. Infer from args or fallback to unknown. I see no breaking change here. ...

Anyway, I would suggest the other direction than described here. Define explicit optional generics which infer from args when not set. Like <A, B?, C?>. Instead of define types on every use like this.foo<number, infer, infer>().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Proposal Suggestion
Projects
Development

No branches or pull requests