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

Optional Generic Type Inference #14400

Open
kube opened this issue Mar 2, 2017 · 47 comments
Open

Optional Generic Type Inference #14400

kube opened this issue Mar 2, 2017 · 47 comments
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@kube
Copy link

kube commented Mar 2, 2017

#13487 added default generic types, but it's still not possible to infer a generic type:

type Return<T extends () => S, S = any> = S

Here S takes its default type any, but T could permit inference of a subset type of any for S:

const Hello = () => 'World'

type HelloReturn = Return<typeof Hello> // any

Here HelloReturn still has any type, and TypeScript could infer S as the literal type 'World', which is a subset of any.

Use Case

Here's an example of a fully declarative workaround for #6606, using generic type inference:

type Return<T extends () => S, S = any> = S
const Hello = () => 'World'

type HelloReturn = Return<typeof Hello>  // 'World'

Default Type

If return of T is not a subset of S, it should throw an error:

type ReturnString<T extends () => S, S = string> = S
const Hello = () => 'World'
type HelloReturn = ReturnString<typeof Hello>  // 'World'
const calculateMeaningOfLife = () => 42
type MeaningOfLife = ReturnString<typeof calculateMeaningOfLife> // ERROR: T should return a subset of String

Multiple Possible Type Inferences

The current implementation always returns the superset (which is just the default generic type), solving this issue would require to return the subset (the most precise type of all the inferred possibilities).

If a type has multiple possible type inferences, TypeScript should check that all these types are not disjoint, and that they all are subsets of the Default Generic Type.

The inferred type is the most precise subset.

@kube kube changed the title Optional Generic Type Optional Generic Type Inference Mar 5, 2017
@KiaraGrouwstra
Copy link
Contributor

KiaraGrouwstra commented Mar 5, 2017

Thanks for creating this; I'll follow this thread as well.

A use-case I'm seeing here is clarifying types in function composition in the presence of generics on the first parameter-function -- that is, the one place that'd cause ambiguity to TS. (Related thread on this issue here.)

For example, for Ramda.js pipe<V0, T1, T2>(fn0: (x0: V0) => T1, fn1: (x: T1) => T2): (x0: V0) => T2;, there are three generics in total, but manually clarifying just the parameters of the entering function (in this case that's just V0) should suffice to hint TS as to what the types throughout the rest of the pipeline (here T1 as well as end return type T2) should be.

I'd see this proposal such as to enable that.
That said, I'd wonder if manually specifying = any should be required for this to work. (any seems like a default value so = any seems an oxymoron. Looks like this is discussed in #13609.)

@KiaraGrouwstra
Copy link
Contributor

I just realized, might generics 'arities' not be used to disambiguate calls for functions with over-loaded type signatures? i.e. if foo has both <A>(): ... and <A, B>(): ..., if we wanted to invoke foo<myA>() so as to call the binary version, it might get misinterpreted as the unary version. I wonder how we could disambiguate that -- foo<myA, >?

@mhegazy mhegazy added Suggestion An idea for TypeScript In Discussion Not yet reached consensus labels Mar 7, 2017
@Dessix
Copy link
Member

Dessix commented Mar 8, 2017

This would bring forward some nice possibilities in using inference to validate function parameters, and I'd very much like to see it happen 👍

@niieani
Copy link

niieani commented Mar 19, 2017

Such a simple idea, but adds so much power to the language! Would love to see it happen!

Related:

@EvanMachusak
Copy link

EvanMachusak commented Mar 26, 2017

Yeah, yeah, but John, your language designers were so preoccupied with whether or not they could that they stopped to wonder whether or not they should.

Typescript is beginning to delve into the territory of too complicated. This is a hard shove off a cliff, in my opinion. Generic syntax is relatively well understood. Generic constraints are relatively well understood. Generic constraints in the form of a pair of lambda expressions would confuse almost everyone who's seen a generic in another language before.

@Dessix
Copy link
Member

Dessix commented Mar 26, 2017

@EvanMachusak Seeing the other languages in existence, is allowing inference of generics that complicated to the user in trade for the power it adds to the language?

P.S. Who's John?

@EvanMachusak
Copy link

@Dessix which other languages have generic inference patterns that resemble this proposal?

@niieani
Copy link

niieani commented Mar 27, 2017

JavaScript's a dynamically typed language, and therefore it requires levels of expression in typing that statically typed languages don't. Nobody is forcing you to use these advanced ways of typing, better yet, you could even create a linter rule that forbids their usage.

On the other hand, this feature would enable proper and stricter typings for so many real-world use-cases that it would be very welcome, even if used only for typing external libraries @DefinitelyTyped. Moreover, I've mentioned two related, complex cases, in which this proposal trumps the necessity for additional complexity, i.e. it proposes to make the language simpler, rather than more complex.

@EvanMachusak
Copy link

@niieani Maybe I'm wrong here, but the appeal of TypeScript versus plain old JavaScript is that we can apply static language concepts and rules to a language (JavaScript) that lacks them. Our team became interested in it because it helped us discover bugs statically. I'm not following your argument that because JavaScript (not TypeScript) is dynamically (aka un-)typed, we must introduce exotic syntax to the language to serve use cases that I would argue are detracting from the purpose of TypeScript.

For example, there is a reason that anonymous types are typically only inferred at the scope level in most languages, and it's not just because of lazy compiler designers. #6606 is something that shouldn't be solved even if can be. If you need a reference to an anonymous type outside of a function scope, you should declare it as a type. It's extra effort to do so but making it too easy to produce and consume anonymous types means that a creative coder who happens to be intimately acquainted with this particular language power can easily get carried away and produce something utterly impossible to maintain, because trying to find the ultimate source of the type declaration you're working with several levels downstream requires that you scope into the function where the type was first created.

This leads me to your suggestion about linting. Yes, since I am speaking out against this proposal you can be sure I would look for a linter rule to prevent this from being used, but because it's a language feature, it's another syntax I have to learn and understand to be fluent in TypeScript which circles back to my original objection: that simply because we can doesn't mean we should. I love TypeScript's rapid evolution but we need to be careful to balance innovation and evolution with the needs of the ecosystem. Too much complexity, too much syntax can kill a language.

As for your final point that this would kill two birds with one stone: true, but only because you've created two monstrous birds in trying to do things that other languages disallow.

Compilers are smart. They're smarter than humans. That's why we need them.

But as we read code, we're compiling it in our heads. The complex use cases you're talking about solving require a level of type inference that while a compiler can do, a human being reading the code can't in most cases. You may be overestimating the average intelligence (or, as we get older, the average amount of caring) we will apply to a problem when reading code. In my view, this syntax proposal requires more horsepower than its added value merits, and that's why I think while it's a cool idea, it's just too much.

@KiaraGrouwstra
Copy link
Contributor

Does this really add learning curve if it just mirrors param defaults in the expression language?
In function composition I'm not sure there's really a nicer way to clarify types.

@RyanCavanaugh
Copy link
Member

Is there a difference between this and the recently-implemented default generic type parameters?

@Dessix
Copy link
Member

Dessix commented Apr 3, 2017

@RyanCavanaugh
This feature allows inference to determine types that fit the existing partial spec to allow creation of types and constraints that were otherwise infeasible or unnecessarily verbose. It is also more specific and strict than the current defaulting method, approaching the goals of type literals.

@kube
Copy link
Author

kube commented Apr 3, 2017

@RyanCavanaugh This feature would extend Type Inference to Generics.

Runtime Function

Today it's possible to infer a type of runtime function, without specifying default generic type:

const return = <T>(fn: () => T) => null as T

Then you can call the function without specifying the type, and T will be inferred:

const hello = () => 'World'
const helloReturn = return(hello) // null

type Hello = typeof helloReturn // 'World'

This is a trick used currently to get the return type of a function, but implies a function call at runtime. (Which will return null here, as the goal is just to do the type inference statically)

Type Declaration

This feature would simply extend this possibility to type declaration, and by this way remove unnecessary runtime function call and variable declaration.

Default Generic Type currently provides no inference:

type Return<T extends () => S, S = any> = S

const Hello = () => 'World'
type HelloReturn = Return<typeof Hello> // any

@Igorbek
Copy link
Contributor

Igorbek commented Jun 2, 2017

Interestingly, originally type parameters defaults were implemented to have described behavior:

  • When specifying type arguments, you are only required to specify type arguments for the required type parameters. Unspecified type parameters can be inferred.

Then, although it wasn't mentioned in the notes, after a design meeting #13607, @rbuckton changed the spec:

Following the discussion during the design meeting, I am making the following changes:

  • When specifying type arguments, you are only required to specify type arguments for the required type parameters. Unspecified type parameters can be inferred. Unspecified type parameters will resolve to their default types.
    For example:
declare function f<T, U = T>(a: T, b?: U): void;

f(1); // ok. Using inference: T is number, U is number.
f(1, "a"); // ok. Using inference: T is number, U is string.
f<number>(1, "a"); // error. Not using inference: T is number, U is number. 

While this is generally stricter, we can loosen this restriction at a later date if necessary.

@rbuckton @RyanCavanaugh @DanielRosenwasser can this restriction be considered for removing?

I, personally, faced with that when tried to type real-world widely used library: styled-components v2, see my comment in the PR.

@KiaraGrouwstra
Copy link
Contributor

KiaraGrouwstra commented Jun 3, 2017

On use-cases, this could allow declaring reusable variables within types; currently there is no good way to deal with factoring out duplicate computations within types that already need their generics for explicit input.

Edit: whoops, you actually can already do that with defaults.

@masaeedu
Copy link
Contributor

Is there a reason this needs to be restricted to optional type parameters?

@KiaraGrouwstra
Copy link
Contributor

@masaeedu: I think it was worded like that because under normal circumstances type parameters already have inference, though not for what the OP tried here. The generic in the example was made optional with the intent to separately capture it, while still wanting to let it get inferred rather than provide a default value in its declaration (as instead, he intends for it to be inferred from the previous generic).

I've flipped on this proposal though; it's a poor man's #6606.

@KiaraGrouwstra
Copy link
Contributor

Well, I take that back. Return types aside, this proposal might also serve to enable extracting constituents of unions, or function parameters. I'm not aware of other ways to do those so far.
An extended #5453 might go so far as to allow extracting parameters once supplied to a function, but this would be about extracting parameter types asked for by an (unapplied) function.

@KiaraGrouwstra
Copy link
Contributor

After further consideration, I'm under the impression other potential uses of this proposal, such as extracting parameter types of function types, could be tackled with #6606 as well.
So the challenge posed in this thread is how to explicitly pass some type, then capture its constituents.
Through functions that could be achieved; the parts to be passed explicitly would be passed as parameters, the constituents then captured through regular (non-defaulted!) generics.
For type Return<T extends () => S, S = any> = S, that might become something like type Return = <T extends () => S, S>(f: T) => S;.

My union idea might have been doomed either way though. For your entertainment:

declare function ArrayifyUnion<Union2 extends A | B, A, B>(v: Union2): [A, B];
let u: string | number = 123;
let x = ArrayifyUnion(u);
// ^ want [string, number], got [{}, {}]

I think there were actually still some generic erasure issues, but yeah I dunno, not very confident about this approach anyway.

@KiaraGrouwstra
Copy link
Contributor

Fixed by #21496.

Return types aside, this proposal might also serve to enable extracting constituents of unions, or function parameters.

Looks like these have been tackled with it so far.

@Raiondesu
Copy link

Raiondesu commented Jul 2, 2019

@tycho01, #21496 does not solve every use-case.

For example, what should we do, if we need to accept one generic parameter and infer another?
Once you pass one generic - all non-optional ones stop being inferred and become required for passing.

And if you just = something (make optional) the parameter you need to be inferred - it will never be.

The only solution for this as of now is to divide your generic function into noop HO-functions with generic-parameters only, which is a pretty ugly solution:

declare const unnecessaryHOF: <T>() => <U extends T>(input: U) => U /* imagine we do something important with the output here */;

const workingResult = unnecessaryHOF<{ bar: string }>()({ bar: 'asd', foo: 2 });

// Autocomplete and type inference work properly
workingResult.bar; // string
workingResult.foo; // number

See playground for a more comprehensive demo.

So even if the exact scenario that is described in the explanation of this issue can be solved with inference in conditional types, it doesn't mean that scaled production scenarios can be too.

@jamesmfriedman
Copy link

@Raiondesu I can confirm that your solution is a viable workaround. Also makes me sad, having to change the API of my project to properly support type inference.

@unional
Copy link
Contributor

unional commented Sep 26, 2019

I also use the same workaround in type-plus typeAssertion<T>() and typeOverrideIncompatible<T>() functions

@jamesmfriedman
Copy link

@unional can you provide an example of that for my own edification (and others)?

@unional
Copy link
Contributor

unional commented Sep 26, 2019

@jamesmfriedman oh, I mean the same workaround as @Raiondesu .

Essentially wrapping it in an extra function to separate the types that you want to specify and those you want to infer.

Using typeOverrideIncompatible() as an example:

import { ANotB } from './ANotB';

export function typeOverrideIncompatible<A extends object>() {
  return function <B extends object>(source: B, override: ANotB<A, B>): A {
    return {
      ...source,
      ...override,
    } as any
  }
}

@oleg-brizy
Copy link

declare function demo<T1, T2 = "">(p1: T1, p2: T2): void;

demo(1, 2);
demo<boolean>(true, 3);
// TS2345 ----------^ Unexpected
// Argument of type '3' is not assignable to parameter of type '""'

@nicky1038
Copy link

nicky1038 commented Aug 2, 2020

I got here from Google trying to find any information about the same problem as @Raiondesu had. Namely the impossibility of writing functions where some generic parameters are mandatory and some should be automatically infered by TypeScript itself.

It would be very useful if that possibility was added to TypeScript. Obviously, #21496 does not help in this case.

However, I should say #21496 solves the OP's use case (not perfectly, but anyway). The problem @Raiondesu and me stuck into is similar but still a bit different from that use case. It can confuse new readers (like me). Maybe it is worth to discuss the new problem in a sepatate issue? Moreover, it seems like #10571 is more specific for it.

@es-lynn
Copy link

es-lynn commented Aug 25, 2020

Same issue as @nicky1038 and @Raiondesu. I have 2 generics where A needs to be declared, and B can be inferred from the parameters.

@sirajalam049
Copy link

I tried from this blog on optional generic types and worked for me.

function fetchData <T = void> ( url: string): T {
        const response:T = fetch(url);
        return response 
}

@IgnusG
Copy link

IgnusG commented Jan 22, 2021

@jamesmfriedman oh, I mean the same workaround as @Raiondesu .

Essentially wrapping it in an extra function to separate the types that you want to specify and those you want to infer.

Using typeOverrideIncompatible() as an example:

import { ANotB } from './ANotB';

export function typeOverrideIncompatible<A extends object>() {
  return function <B extends object>(source: B, override: ANotB<A, B>): A {
    return {
      ...source,
      ...override,
    } as any
  }
}

I've used the same approach for a factory pattern. Separating the types into 2 generic declarations worked pretty well, although it did introduce the need to call the function in a bit of a weird way: FactoryBuilder<ObjectInterface>()(objectDefaults)

To set it up I've had do something along the lines of:

function FactoryBuilder<Target>(/* Our empty definition to separate the 2 generics */): <S extends Defaults<Target>>(
  defaults: S,
) => Factory<Target, S>

In my case it's important to be able to specify the interface for the object we wish to create = It cannot and should not be inferred.

To provide the defaults for it on the other hand, and get proper typing support (properties given defaults should be marked as optional in the returned factory), we can either do the above or we'd need to supply the type of the defaults which leads to more overhead (and no in-lining):

const defaults = {...};
const factory = FactoryBuilder<ObjectInterface, typeof defaults>(defaults);

Having the ability to infer optional generics would shorten this all to FactoryBuilder<ObjectInterface>(defaults);
I know its a tiny change (and might not look important) but the developer has to keep this in mind to call the builder with ()(defaults) instead of (defaults) which can lead to some confusion.

@akomm
Copy link

akomm commented Apr 28, 2021

The main intention is to infer the type and not being forced to specify the generic manually. For that, a default is used for the generic. Currently, specifying a default for a generic opt-out of inference. Currently the proposal is to add inference to initially picked default value. What about not adding the inference on the default value, which seems like a workaround already, but make generics only required IF it they can not be inferred? Then you don't need to specify the default.

@unional
Copy link
Contributor

unional commented Apr 28, 2021

FYI I'm able to do generic type inference this way:

https://github.com/unional/type-plus/blob/7a3cf9a984ed7972230251db5bdd03d9141904a8/src/math/GreaterThan.ts#L18-L30

I'm not sure if I should recommend this to everyone thou. 🌷

@Stevemoretz
Copy link

@jamesmfriedman oh, I mean the same workaround as @Raiondesu .
Essentially wrapping it in an extra function to separate the types that you want to specify and those you want to infer.
Using typeOverrideIncompatible() as an example:

import { ANotB } from './ANotB';

export function typeOverrideIncompatible<A extends object>() {
  return function <B extends object>(source: B, override: ANotB<A, B>): A {
    return {
      ...source,
      ...override,
    } as any
  }
}

I've used the same approach for a factory pattern. Separating the types into 2 generic declarations worked pretty well, although it did introduce the need to call the function in a bit of a weird way: FactoryBuilder<ObjectInterface>()(objectDefaults)

To set it up I've had do something along the lines of:

function FactoryBuilder<Target>(/* Our empty definition to separate the 2 generics */): <S extends Defaults<Target>>(
  defaults: S,
) => Factory<Target, S>

In my case it's important to be able to specify the interface for the object we wish to create = It cannot and should not be inferred.

To provide the defaults for it on the other hand, and get proper typing support (properties given defaults should be marked as optional in the returned factory), we can either do the above or we'd need to supply the type of the defaults which leads to more overhead (and no in-lining):

const defaults = {...};
const factory = FactoryBuilder<ObjectInterface, typeof defaults>(defaults);

Having the ability to infer optional generics would shorten this all to FactoryBuilder<ObjectInterface>(defaults);
I know its a tiny change (and might not look important) but the developer has to keep this in mind to call the builder with ()(defaults) instead of (defaults) which can lead to some confusion.

I didn't even read all of your text but you gave me the idea to make mine work with two nested functions thanks!

@jamesmfriedman
Copy link

@Stevemoretz oh I gotcha. Yeah, it's a bummer to have the extra () at the callsite. Just looks strange. Amazing how much effort I exhausted in order to be able to write myFunc(...args) instead of myFunc()(...args)

@Stevemoretz
Copy link

@Stevemoretz oh I gotcha. Yeah, it's a bummer to have the extra () at the callsite. Just looks strange. Amazing how much effort I exhausted in order to be able to write myFunc(...args) instead of myFunc()(...args)

That's true but I use it for a config creator only once in any apps so I'm good with it, but sure this should be implemented it's a pain in the ***.

@infacto
Copy link

infacto commented May 18, 2022

This would be a great feature. Just another simple example:

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

I want to set the return value type R which is unknown by default.

By default the generic types automatically infer from the arguments or fallback to 'unknown'. It behaves like optionals. Why do I need to set all generics (T and D) when I set only the first of them (R)? Just leave the remaining generics with the default behavior (infer from args or unknown).

I don't want to set T or D with an default value (like T = unknown). Because I want them resolved automatically from the arguments. But only set one of them which are not in arguments (R).

So what can we do? Either we implement a required flag to generics or a flag to mark generics as explizit optional to infer from args. Or just leave it as it is and allow that what we want. Because the generics are optional anyway.

We could do something like this:

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

Adding ? to mark it explizit as optional to keep the default behavior of infer from args or unknown.
But since the generics are not required anyway, I don't think that we need a syntax for that. It should just work as we want here with the current syntax. There is no support for required generics. But can be done with e.g. foo<T = void>. Therefore we just change the behavior as expected here. Any expected breaking changes? I just want the following to be working:

// Good (expected, but not working)
const result = await this.myService.showModal<string>(MyComponent, someData);

But currently I have to set all three generics types or override the infer default with a static types like unknown.

// Bad (pseudo code, not tested)
const result = await this.myService.showModal<string, MyComponent, Data>(MyComponent, someData);

// Or it forces me to define the method with generics like `<R, T = any, D = any>`.  :(
// Or I have to wrap the methods to define generics in another scope. It's just lame.
// Or I add an optional dummy argument for type infer. lel

Change "All or nothing" to "Nothing or Some". Just do it. Why not? What's the problem here? Please explain me in simple words with coloured hand puppets. It's over 5 years old great request.


Update: Found a related issue (duplicate?): #26242

@akomm
Copy link

akomm commented May 19, 2022

What if any non inferable generics are implicit unknown (strict) or implicit any (non-strict). Default type has as today to satisfy the constraint, so does the inferred type. Default type replaces "unknown" in case of non-inferability, though evtl. not if the unknown type is explicit at position from which the inference is possible.

@jcalz
Copy link
Contributor

jcalz commented Oct 25, 2022

I don't see this explicitly mentioned here, but if this is ever implemented I'd expect something like

type Foo<T = infer> = {x: T}

const f: Foo = {x: 1};
// const f: Foo<number>

where you use the infer keyword as the default type to ask for such inference.

@NfNitLoop
Copy link

Relatedly, TS5 introduced const type parameters. I was hoping I could do something like:

interface Foo<const T = string> { t: T }

const f: Foo = { t: 1 }

But no such luck. I get the error:
Screenshot 2023-04-20 at 2 26 53 PM

(I'd be happy with <T = infer> or <const T = someDefault>. Whatever lets me write this. 😊)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests