-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Comments
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 I'd see this proposal such as to enable that. |
I just realized, might generics 'arities' not be used to disambiguate calls for functions with over-loaded type signatures? i.e. if |
This would bring forward some nice possibilities in using inference to validate function parameters, and I'd very much like to see it happen 👍 |
Such a simple idea, but adds so much power to the language! Would love to see it happen! Related: |
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. |
@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? |
@Dessix which other languages have generic inference patterns that resemble this proposal? |
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. |
@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. |
Does this really add learning curve if it just mirrors param defaults in the expression language? |
Is there a difference between this and the recently-implemented default generic type parameters? |
@RyanCavanaugh |
@RyanCavanaugh This feature would extend Type Inference to Generics. Runtime FunctionToday 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 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 Type DeclarationThis 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 |
Interestingly, originally type parameters defaults were implemented to have described behavior:
Then, although it wasn't mentioned in the notes, after a design meeting #13607, @rbuckton changed the spec:
@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. |
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. |
Is there a reason this needs to be restricted to optional type parameters? |
@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. |
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. |
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. 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. |
Fixed by #21496.
Looks like these have been tackled with it so far. |
@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? And if you just 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. |
@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. |
I also use the same workaround in |
@unional can you provide an example of that for my own edification (and others)? |
@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 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
}
} |
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 '""' |
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. |
Same issue as @nicky1038 and @Raiondesu. I have 2 generics where |
I tried from this blog on optional generic types and worked for me.
|
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: 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 |
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. |
FYI I'm able to do generic type inference this way: I'm not sure if I should recommend this to everyone thou. 🌷 |
I didn't even read all of your text but you gave me the idea to make mine work with two nested functions thanks! |
@Stevemoretz oh I gotcha. Yeah, it's a bummer to have the extra |
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 ***. |
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 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 ( I don't want to set 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 // 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 |
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. |
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 |
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: (I'd be happy with |
#13487 added default generic types, but it's still not possible to infer a generic type:
Here
S
takes its default typeany
, butT
could permit inference of a subset type ofany
forS
:Here
HelloReturn
still hasany
type, and TypeScript could inferS
as the literal type'World'
, which is a subset ofany
.Use Case
Here's an example of a fully declarative workaround for #6606, using generic type inference:
Default Type
If return of
T
is not a subset ofS
, it should throw an error: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.
The text was updated successfully, but these errors were encountered: