-
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
Allow skipping some generics when calling a function with multiple generics #10571
Comments
I would say covered by #2175 |
|
@mhegazy I don't think #2175 covers this. In fact, both propositions complement each other quite nicely. The proposed "Default generic type variables" extends the "all or nothing" notion of generic usage and deals with the way the class/function producer specifies them, not the way the class/function consumer uses them. In usage, you are still left with either omitting all generic parameters or specifying all explicitly. The only thing #2175 changes is the fallback type ( This issue deals with the possibility of omitting some type parameters for automatic inference, while specifying others, not with defining fallback defaults. I also like @basarat's proposed |
We already have a paradigm for passing arguments to functions, including default arguments in ES6+ in TypeScript/JavaScript. Why invent a new semantic? Why would generics just not follow the same semantics. |
@kitsonk You would still have to introduce an |
No... you could just skip them, like array destructuring: |
@kitsonk sure, coma-style skipping is an option too. However in your original post you argued for "default arguments" semantics, not array destructuring semantics. |
I personally find this very hard to read. specially with long argument list, something like #2175 puts this on the declaration. you have to decide as an interface author which type parameters are optional, and what are the defaults, and you put them at the end. also note that generic type arguments is modeled after function arguments. it is illegal to call a function with missing arguments, or with less parameters than the signature requires. |
@mhegazy the problem is as the interface author you cannot always reliably put them at the end. Sometimes you might need to force the use of the last argument, while the penultimate is inferred. That's why we need to be able to choose which are to be inferred - as the consumer. Indeed it is illegal to call with missing arguments, that's why we're proposing an "infer" argument - equivalent of |
Can you provide an example, considering TypeScript allows overrides, where you feel this cannot be accomplished? |
I would expect @niieani wants to keep the type parameter in the same order as the regular parameters. so in this sense it is not always possible to move them around if you do not control the actual function signatures. |
@mhegazy that's one reason, but actually there's another one. The end-user only needs to be able to consume the methods by passing one or two generic arguments at most, not all of them -- the point is I don't want to burden the user from having to re-type all the generics that are an implementation detail. But ultimately non-last arguments are not a major problem for the end-user, it's my problem as the type-definition/library creator, as not being able to type only the specific one or two type parameters creates a maintenance nightmare! I don't remember the exact code example right now as I was working on the typings around February, but if I start working on it again, I'll post one here. |
Flowtype's equivalent is |
skipping commas is something JS already has (inside destructuring and sparse arrays) would be nice to have as types. recently got struck by this problem with Redux Actions, it's really really really really hard to implement functional middleware typings when the resulting function is so deeply nested and you have to have 4-5 generics in the type declaration and must declare all of them manually if you decide to ever define any of them |
Same here. The situation I faced is to type the interface ExtendedClass<Config, Class, Override> extends Function {
new (config: Config): Class & Override;
superclass: Class;
}
declare class Ext {
static extend<Config, Class, Override>(superclass: new(...args: any[])
=> Class, overrides: Override): ExtendedClass<Config, Class, Override>;
}
// optimal usage
interface MyActionConfig { ... }
const MyAction = Ext.extend<Ext.ActionConfig & MyActionConfig>(Ext.Action, { ... })
// actual usage
interface MyActionConfig { ... }
interface MyActionOverride { ... }
const myActionOverride: MyActionOverride = { ... }
const MyAction = Ext.extend<
Ext.ActionConfig & MyActionConfig,
Ext.Action,
Ext.MyActionOverride>(Ext.Action, myActionOverride)
const myAction = new MyAction({ ... }) // { ... } is Ext.ActionConfig & MyActionConfig Currently, I have to do a trade-off by giving up the ability to connect interface ExtendedClass<Class, Override> extends Function {
new <Config>(config: Config): Class & Override;
superclass: Class;
}
declare class Ext {
static extend<Class, Override>(superclass: new(...args: any[])
=> Class, overrides: Override): ExtendedClass<Class, Override>;
}
interface MyActionConfig { ... }
const MyAction = Ext.extend(Ext.Action, { ... })
// Trade off: user of `MyAction` need to do this every time.
const myAction = new MyAction<Ext.ActionConfig & MyActionConfig>({...}) |
Please ignore my last post. I'm able to simplify it. Here is what I got: interface ExtendClass<Class> extends Function {
superclass: Class;
}
declare class Ext {
static extend<Class>(superclass: new(...args: any[])
=> any, overrides: Partial<Class>): Class & Ext.ExtendClass<Class>;
}
// usage
export interface MyAction extends Ext.Action {
// You must define the constructor so that your class can be instantiated by:
// `const action = new MyAction(...)`
new (config: MyAction.Config): MyAction;
// your custom properties and methods
}
export const MyAction = extend<MyAction>(Ext.Action, {
// properties and methos exists in `MyAction`
})
export namespace MyAction {
export type Config = Partial<Ext.ActionConfig> & {
// Additional properties
}
} The only thing is that I can't restrict the |
Related: #1213 |
Here is another use case : function actionBuilder<T, R extends string>(type: R | '') {
return function(payload: T) {
return {
type: type,
payload
};
};
}
//espected usage
const a = actionBuilder<number>('Action');
//Instead of
const a = actionBuilder<number, 'Action'>('Action');
// a would be of type
number => { type: 'Action', payload: number }; So while defining @mhegazy I tried with default generic : function actionBuilder<T, R extends string = string>(type: R | '') {
return function(arg: T) {
return {
type: type,
payload: arg
};
};
}
const a = actionBuilder<number>('a')(3); Here |
@Ranguna |
@btoo You've missed the point of this issue. I'll illustrate with Case 3 from the original post: function case3<A, B, C>(b: B, c: C): A {}
// incorrect type of A - left unspecified:
example('thing');
// correct, but unnecessarily verbose - we already know that 'thing' is a string and true is a bool
example<number, string, boolean>('thing', true); We'd like to write one of example<number>('thing', true);
example<number, _, _>('thing', true);
example<number, *, *>('thing', true);
example<number, infer, infer>('thing', true);
example<number, auto, auto>('thing', true);
example<A: number>('thing', true); You're suggesting we write
which is even more verbose than the workaround that exists.
|
Still waiting for this feature and for now this could be a solution if you only have one required type parameter and lots of inferred type parameters:
|
As there's been silence here for 6 months, I'll add my +1. This feature would simplify things greatly for a library feature I'm working on. I need to pass one required type parameter which can't be inferred, but I have 3 (so far) other type parameters which can easily be inferred from arguments. This is extra annoying in a case like mine where the types are more complex. Here is a simplified example, setting up a React hook for filtering items in a table: interface FilterCategory<T, K> {
key: K;
getFieldValue: (item: T) => string;
someMoreOptions?: { /* things that need to reference the T and K types */ }
};
const useFilterControls = <T, K>(args: { categories: FilterCategory<T, K>, someMoreArgs: { /*...*/ } }) => { /*...*/ }; Here is the desired usage, passing in const controls = useFilterControls<Customer>({
categories: [
{ key: 'name', getFieldValue: (customer) => customer.name },
{ key: 'address', getFieldValue: (customer) => formatAddress(customer.address) },
{ key: 'phone', getFieldValue: (customer) => customer.phone.preference === 'mobile' ? customer.phone.mobile : customer.phone.home },
// Imagine many more categories here
],
someMoreArgs: { /*...*/ }
}); But that doesn't work, it requires either both T and K be passed in or neither. I have to do this: const controls = useFilterControls<Customer, 'name' | 'address' | 'phone'>({
categories: [
{ key: 'name', getFieldValue: (customer) => customer.name },
{ key: 'address', getFieldValue: (customer) => formatAddress(customer.address) },
{ key: 'phone', getFieldValue: (customer) => customer.phone.preference === 'mobile' ? customer.phone.mobile : customer.phone.home },
// Imagine many more categories here
],
someMoreArgs: { /*...*/ }
}); I know for this example I could omit both type params and use This is very simplified -- some of our tables have 10-15 filter categories, that string union would have to repeat all 10-15 keys. We also have other functions related to sorting, pagination, selection, expandable rows, and other table state stuff that all have their own sets of 2-4 generic type parameters and rely on the same single |
@mturley We're all looking forward to a future with this proposed feature. FWIW — in your example type FilterCategory<T, K extends keyof T> = {
key: K;
getFieldValue: (item: T) => string;
someMoreOptions?: { /* things that need to reference the T and K types */ }
};
function useFilterControls<T, K extends keyof T = keyof T>(
args: {
categories: FilterCategory<T, K>[];
someMoreArgs: { /*...*/ };
},
) { /*...*/ };
// Here are some stand-ins for the missing types in your example:
type Address = unknown;
declare function formatAddress(address: Address): string;
type Customer = {
address: Address;
name: string;
phone: Record<"preference" | "mobile" | "home", string>;
};
const controls = useFilterControls<Customer>({
categories: [
{ key: "name", getFieldValue: (customer) => customer.name },
{ key: "address", getFieldValue: (customer) => formatAddress(customer.address) },
{ key: "phone", getFieldValue: (customer) => customer.phone.preference === "mobile" ? customer.phone.mobile : customer.phone.home },
// Imagine many more categories here
],
someMoreArgs: { /*...*/ }
}); |
Thanks! Yeah, the example I wrote up is bad, there's not a 1:1 mapping of filter keys to the keys in T and there are some more wonky types in our full implementation that don't match up nicely like that. But I appreciate the advice and there are definitely some things we can simplify along those lines. |
woah! i got ratio'd super hard on this thread. well i can't say I'm not disappointed, but I'm not so much disappointed in getting ratio'd as much as I am in the commenters/lurkers in this thread for favoring their own emotional attachment over meaningful engagement essentially ignoring the entirety of #10571 (comment), @Matchlighter and, by proxy, his 👍ers want to
|
@btoo I can't say for others, but I am subscribed and emoted threads like that, because I just can't use workaround, (I use it a lot in some cases, and I am very satisfied by satisfy... But not for this case) And as external observer who can't really estimate this feat. difficulty I just emote/post to thread with hope to make this feature a bit hotter on backlog... And about workaround itself, this example just simplified fragment, but real code can be terrible itself and far more terrible with workarounds like this... And sorry for my English and may be too emotional response. |
@btoo you are correct that I and others here hadn't realized using
My takeaways here:
I apologize if my comment makes things worse rather than better, I acknowledge that it is also coming from an emotional place. I just wish things like this didn't always devolve into hurt feelings. |
Thanks @mturley - that is largely accurate from my side. I am guilty of that emotional response - I'm subscribed to several issues and many (some related to this, some not) received similar replies from @btoo regarding @btoo thank you for re-emphasizing the use of |
@Matchlighter To be fair, the first time I read @btoo 's response, I also did not notice that the use of |
FWIW, I tried the |
You could pass example<never, number>(_, num) |
There is a solution that is quite verbose on the declaration, but less verbose than current options on the usage: using an type mapper object type: type Args<A, B, C> = {
a?: A
b?: B
c?: C
}
function example<TArgs extends Args<A, B, C>, A = any, B = any, C = any>(b: TArgs['b'], c: TArgs['c']): TArgs['a'] {
return null as any
}
example('thing', true) // returns any
example<{ a: number }>('thing', true) // returns number
example<{ b: number }>('thing', true) // fails for incompatible argument 'b'
example<{ a: number; c: number }>('thing', true) // fails for incompatible argument 'c', but returns number With such solution, you can also be stricter on each type as such: // Constrained example
type Args2<Receives extends number, Returns extends string> = {
receives?: Receives
returns?: Returns
}
function constrained<
TArgs extends Args2<Receives, Returns>,
Receives extends number = number,
Returns extends string = string
>(
param: TArgs['receives'] extends number ? TArgs['receives'] : number
): TArgs['returns'] extends string ? TArgs['returns'] : string {
return null as any
}
constrained() // fails for missing argument
constrained('a string') // fails for incompatible argument
constrained(1) // works with default types
constrained<{ receives: 10 }>(1) // fails for incompatible argument
constrained<{ receives: 10 }>(10) // works with specific typed argument
constrained(1) // returns string
constrained<{ returns: 'specific' }>(1) // returns 'specific' value Hope this helps someone while a native solution isn't in place. Check the playground |
According to [zustand TypeScript Guide](https://docs.pmnd.rs/zustand/guides/typescript) and [zustand Github TypeScript Usage](https://github.com/pmndrs/zustand?tab=readme-ov-file#typescript-usage) instead of create(...), you have to write create<T>()(...) so Typescript can infer the type of the state correctly. This is a workaround for [microsoft/TypeScript#10571](microsoft/TypeScript#10571)
Right now in TypeScript it's all or nothing when calling generic methods. You can either skip typing all the generics altogether and they will be inferred from the context (if possible), or you have to manually (re)define all of them. But the reality isn't black and white, there are also shades of gray, where we can infer types for some of the generic parameters, but not others. Currently those have to be unnecessarily verbose by forcing the programmer to explicitly restate them.
Take a look at these 3 cases:
Case 1 - everything can be inferred - no need to call the method with
<>
definition:Compiler knows that:
A is number
B is string
C is boolean
Case 2 - nothing can be inferred, so we need to state what A, B and C should be, otherwise they'll default to
{}
:Case 3 - the one that's interesting to this feature request - some can be inferred, some can't:
Now, typing
string, boolean
in the above example isn't a big deal, but with complex scenarios, say with a method using 5 generics, where you can infer 4 of them, retyping them all seems overly verbose and prone to error.It would be great if we could have some way to skip re-typing the types that can automatically be inferred. Something like a special
auto
orinferred
type, so we could write:Or maybe even, if we only want to specify those up to a certain point:
The above "short-hand" notation could perhaps be different to account for function overloads with different number of generics.
Having such a feature would solve newcomers encountering problems such as this one: http://stackoverflow.com/questions/38687965/typescript-generics-argument-type-inference/38688143
The text was updated successfully, but these errors were encountered: