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 skipping some generics when calling a function with multiple generics #10571

Open
niieani opened this issue Aug 26, 2016 · 81 comments · May be fixed by #26349
Open

Allow skipping some generics when calling a function with multiple generics #10571

niieani opened this issue Aug 26, 2016 · 81 comments · May be fixed by #26349
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@niieani
Copy link

niieani commented Aug 26, 2016

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:

function case1<A, B, C>(a: A, b: B, c: C): A {}

example(1, '2', true);

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 {}:

function case2<A, B, C>(): A {}

example<number, string, boolean>();

Case 3 - the one that's interesting to this feature request - some can be inferred, some can't:

function case3<A, B, C>(b: string, c: boolean): 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);

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 or inferred type, so we could write:

example<number, auto, auto>('thing', bool);

Or maybe even, if we only want to specify those up to a certain point:

example<number>('thing', bool);

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

@mhegazy
Copy link
Contributor

mhegazy commented Aug 26, 2016

I would say covered by #2175

@basarat
Copy link
Contributor

basarat commented Aug 26, 2016

auto I would say * as auto might be a type name (unlikely but still). Also shorter 🌹

@niieani
Copy link
Author

niieani commented Aug 27, 2016

@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 ({}) when it cannot be automatically inferred by the compiler.

This issue deals with the possibility of omitting some type parameters for automatic inference, while specifying others, not with defining fallback defaults.
Hope that's clearer.

I also like @basarat's proposed * instead of auto.

@kitsonk
Copy link
Contributor

kitsonk commented Aug 27, 2016

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.

@niieani
Copy link
Author

niieani commented Aug 27, 2016

@kitsonk You would still have to introduce an undefined type for non-last arguments (in ES6+ this is how you would use the default on non-last arguments).
The proposed * / auto is just that -- without the ugly sounding undefined which is also a type, now that we have strict null checks.

@kitsonk
Copy link
Contributor

kitsonk commented Aug 27, 2016

No... you could just skip them, like array destructuring: < , , Bar>

@niieani
Copy link
Author

niieani commented Aug 27, 2016

@kitsonk sure, coma-style skipping is an option too. However in your original post you argued for "default arguments" semantics, not array destructuring semantics.
Ultimately I'm okay with either semantic, < , , Bar> or <*, *, Bar>.

@mhegazy
Copy link
Contributor

mhegazy commented Aug 29, 2016

I personally find this very hard to read. specially with long argument list, something like foo<, , ,A, , D>() was that right? or was it foo<, , ,A, D, >() .

#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.

@niieani
Copy link
Author

niieani commented Aug 29, 2016

@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 undefined - the empty space or *. You do make a point with coma skip being hard to read with long lists of arguments -- I'm going to back the * proposed by @basarat.

@kitsonk
Copy link
Contributor

kitsonk commented Aug 29, 2016

the problem is as the interface author you cannot always reliably put them at the end.

Can you provide an example, considering TypeScript allows overrides, where you feel this cannot be accomplished?

@mhegazy
Copy link
Contributor

mhegazy commented Aug 29, 2016

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.

@niieani
Copy link
Author

niieani commented Aug 29, 2016

@mhegazy that's one reason, but actually there's another one.
I came across this problem while writing type declarations for RethinkDB. The definitions are incredibly complex and I remember being unable to implement certain features exactly because of the fact that certain classes would have to use up to 4-8 generics (as a workaround, because of other TS limitations). Each generic type would be a pre-formed object based on the input T, so that we can keep track of the original(s) while transforming the object being passed through (notably the way RethinkDB's group() and ungroup() methods work).

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!
Output of every single method would require me to type and re-type those same generic types over and over again, while most of them could be inferred and only some need manual adjustment in the output.

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.

@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 Oct 24, 2016
@niieani
Copy link
Author

niieani commented Dec 23, 2016

Flowtype's equivalent is * - existential type. Read this for reference.

@pocesar
Copy link

pocesar commented Jan 3, 2017

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

@unional
Copy link
Contributor

unional commented Jan 18, 2017

Same here. The situation I faced is to type the ExtJS's Ext.extend() method:

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 MyAction and MyActionConfig just to make it easier to author new class:

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>({...})

@unional
Copy link
Contributor

unional commented Jan 19, 2017

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 superclass: new(...args: any[]) => any, but that's icing on the cake.

@niieani
Copy link
Author

niieani commented Feb 22, 2017

Related: #1213

@RyanCavanaugh RyanCavanaugh added Help Wanted You can do this Committed The team has roadmapped this issue and removed Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature labels Mar 22, 2017
@ghetolay
Copy link

ghetolay commented Mar 31, 2017

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 T is mandatory, we could totally infer R and avoid defining it aswell.

@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 a.type was not inferred and got the default string type instead of the string literal a.
But look likes a combination of default generic type and #14400 would do.

@temoncher
Copy link

@Ranguna
Isn't it already scheduled for November 2022? https://github.com/microsoft/TypeScript/wiki/Roadmap#49-november-2022
The issue is already closed with #46827

@Matchlighter
Copy link

@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

example('thing', true) satisfies ReturnType<typeof example<number, string, boolean>>

which is even more verbose than the workaround that exists.

satisfies is not a solution to this.

@Misaka-0x447f
Copy link

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:

const foo = <T extends unknown, I1 extends string, I2 extends string>(typeRef: T, arg1: I1, arg2: I2) =>  {...}

foo(true as boolean, { /*whatever*/ }, { /*it is*/ })

@mturley
Copy link

mturley commented May 1, 2023

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 Customer for T and letting K be inferred from the key values of the objects in my categories array on my args object:

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 getFieldValue: (customer: Customer) => ..., but (a) that shouldn't be necessary anyway and (b) there are things that I'm omitting here that rely on the inferred type and are a lot harder to fix than this.

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 T (the type of the objects we're describing in the table). Wrapping them all in one function requires passing in several lines of just type parameters when we should be able to just pass in <Customer> and infer the rest.

@jsejcksn
Copy link

jsejcksn commented May 1, 2023

@mturley We're all looking forward to a future with this proposed feature. FWIW — in your example K can be keyof T by default — based on the details you shared, you can create your desired API without this feature:

TS Playground

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: { /*...*/ }
});

@mturley
Copy link

mturley commented May 1, 2023

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.

@btoo
Copy link

btoo commented May 2, 2023

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

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);

why they're still unhappy. giving them the benefit of the doubt that they didn't already know this code was littered with issues, we can say for a moment they eventually arrived at an actually workable reproduction, which might've looked something like

this (playground link).
declare function example<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);
at this point, they could have correlated this to
my directly addressing this
which contradicts
their claim that I'm suggesting they do something else

You're suggesting we write

example('thing', true) satisfies ReturnType<typeof example<number, string, boolean>>

but instead, I mostly just saw more tired complaining about how they've been waiting forever and the typescript maintainers still haven't given them exactly what they're asking for yet

to spell it out, my interpretation of this feature request is to have a way that:

  • allows us to provide to a generic function (or ideally also a type) any number of type arguments less than the entire list
  • does the above without leaving any runtime footprint

well

example('thing', true) satisfies ReturnType<typeof example<number, any, any>>

does exactly this if you just use any instead of

_, *, infer, or auto

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);
and while there already exists a workaround as described in https://github.com//issues/10571#issuecomment-326761563, that workaround leaves a runtime footprint whereas mine does not

anyway I hope I'm wasting my breath here defending the use of satisfies and instantiation expressions because a typescript maintainer opened #54047 for adding this feature recently

@findoff
Copy link

findoff commented May 2, 2023

@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 every time, when I need write tricky/costly workaround, I think - "Why I write this ... instead of simple and clean code? It's really technical restriction? Or may be just feat. that doesn't counted as essential by maintainer because too small community reaction?"

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...
We live not in a fairytale, and this feature with and or something can be easily used by people who almost don't know TS, but satisfies with ReturnType just blowup his mind, and this case cause us use an easy and untyped code instead of easy and well-typed.

And sorry for my English and may be too emotional response.

@mturley
Copy link

mturley commented May 2, 2023

@btoo you are correct that I and others here hadn't realized using satisfies with any in place of the inferred generics works for some of these cases, and I do appreciate you reiterating that. I'll use that workaround for now. But a couple of points:

  1. I think folks interpreted your suggestion not as a temporary workaround but as a solution that makes this feature request unnecessary. This may have been an unfair assumption, but it was unclear IMO.
  2. The use of explicit any is considered by many to be an antipattern, and while I understand that in this case it isn't problematic (it doesn't result in any actual variables receiving the any type) it introduces a maintenance problem (I now need to pay attention to which anys are acceptable and which are not, instead of avoiding it entirely). Let alone the syntax being unnecessarily verbose. The simplified syntax in this proposal definitely has merit.
  3. I find it difficult to take your accusation of emotional attachment seriously when you seem to exhibit it yourself. I agree that some other comments in this thread are rude, but yours is among them. Statements like saying someone's example code is "littered with issues" when it was clearly a rough illustration of the issue (and it succeeded in doing so) are not helpful, and strange to see when your message also has formatting issues and contains a malformed URL. (Yes I do see the irony in me pointing this out). We don't need to expect every line of a message here to be perfect.

My takeaways here:

  • The feature request is still a valid one, and it is understandable that people are annoyed it has been ignored for so long.
  • The satisfies workaround is valid for some cases but not all, and both of you are right: it shouldn't be dismissed, but it shouldn't invalidate the request for the full feature.
  • Both sides of this argument seem to me to be at fault. Let's all maybe calm down a little and be happy that Perform partial inference with partially filled type parameter lists #54047 finally exists.

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.

@Matchlighter
Copy link

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 satisfies, so it felt like a "when you have a hammer, everything looks like a nail" situation and was being used to dismiss this issue instead of as a "here's something that may help while this issue remains open".

@btoo thank you for re-emphasizing the use of any in your snippets - that usage was lost on me (I interpreted them more as placeholders than as literal anys), so you're right that satisfies can be a valid (albeit verbose) workaround here.

@gpkc
Copy link

gpkc commented May 2, 2023

@Matchlighter To be fair, the first time I read @btoo 's response, I also did not notice that the use of any would solve this issue. It doesn't feel like an intuitive use of the any keyword at first.

@mturley
Copy link

mturley commented May 2, 2023

FWIW, I tried the satisfies solution and some things are still failing to be inferred (it appears to use default values for type params that have a default? not sure if that's what's really going on). It's difficult to create an isolated example to reproduce this. I'll be curious if the new implementation fixes it or has the same behavior.

@chr-ge
Copy link

chr-ge commented Oct 6, 2023

You could pass never or any to the first argument

example<never, number>(_, num)

@lucasconstantino
Copy link

lucasconstantino commented Dec 31, 2023

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

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