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 · 71 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 · 71 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.

@Stevemoretz
Copy link

Stevemoretz commented May 20, 2021

#2175
saved my ass

@erfanasbari
Copy link

erfanasbari commented Jun 28, 2021

It's been 5 years and still this important feature is missing. 😪

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Jul 8, 2021

Wait until you find out what's missing from C! 😅

@achan-godaddy
Copy link

achan-godaddy commented Sep 29, 2021

is a solution to change the syntax so that we can support named generics instead of ordered generics. Just like a function could take named parameters function someFunction<{Input, Response, Error}>({input}: {input?: Input}): Promise<Response> then we'd call it with someFunction<{Response: string}>() if we didn't want to provide all the type params? Maybe this breaks type definitions but it seems like named generics or something like them would be more helpful than positional someFunction<Input,Response,Error> generics.

@ChibiBlasphem
Copy link

ChibiBlasphem commented Oct 11, 2021

I have this example were I need to infer the second generic type passed to useFlagship (as it's bothersome for the user to create intermediary variable) to be able to properly type the output of the function. At the moment i'm obligated to set a default value to Param (usually UseFlagshipParams<T>) generic type to return a type which contains properties we are sure to not have due to the params passed to the function :

interface Flags {
  study_plan_setup_button_text: 'original' | 'variation1';
  feature_driving_enabled: boolean;
}

type ToUnion<T extends Record<keyof T, unknown>> = T[keyof T]
type FlagRequest<T extends Partial<Record<keyof T, unknown>>> = ToUnion<{
  [Key in keyof T]: { key: Key, defaultValue: T[Key], activate?: boolean }
}>

interface UseFlagshipParams<T extends Record<keyof T, unknown>> {
  modifications: {
    requested: FlagRequest<T>[]
  }
}

interface UseFlagshipOutput<T extends Record<keyof T, unknown>, Params extends UseFlagshipParams<T>> {
  modifications: {
    [Key in Params['modifications']['requested'][number]['key']]: T[Key]
  },
}

function useFlagship<T extends Record<keyof T, unknown>, Params extends UseFlagshipParams<T>>(params: Params): UseFlagshipOutput<T, Params> {
  // Some code here
  return { modifications: {} } as any;
}

const { modifications } = useFlagship<Flags>({
  modifications: {
    requested: [
      {
        key: 'feature_driving_enabled',
        defaultValue: false
      },
      {
        key: 'study_plan_setup_button_text',
        defaultValue: 'original'
      }
    ]
  }
});

Testable on typescript playground here

@yss14
Copy link

yss14 commented Apr 10, 2022

@achan-godaddy I really like your idea about named generics. I think this would solve many problems in libraries which heavily depend on generics like react-query. If a named generic has a default assigned to it it would be optional, if not it would be mandatory to specify it.

@nichtsam
Copy link

nichtsam commented Apr 15, 2022

Here is another use case:

This is a function factory for react-query hook.
Takes a string key which can provide type to be inferred as GQueryKey,
and a data-fetching function which can provide type to be inferred as GResponseData.

GAcceptedParams can be specified when generating hooks, so when hooks are being used, the user of it can be provided with possible params that would have effect on the data-fetching function.

import { AxiosError } from 'axios';
import { QueryFunction, QueryKey, useQuery, UseQueryOptions } from 'react-query';

// TODO: make other generic auto-infer when GAcceptedParams is specified. (Now it's all or nothing, specifying one with turn off auto-inference)

type TUseQueryOptions<
    GResponseData,
    GError = unknown,
    GSelectedData = GResponseData,
    GQueryKey extends QueryKey = QueryKey,
> = Omit<UseQueryOptions<GResponseData, GError, GSelectedData, GQueryKey>, 'queryKey' | 'queryFn'>;

export const generateQueryHook =
    <GAcceptedParams, GResponseData, GOriginQueryKey extends string = string>(
        originQueryKey: GOriginQueryKey,
        queryFunction: QueryFunction<
            GResponseData,
            [first: GOriginQueryKey, second?: GAcceptedParams]
        >,
    ) =>
    <GSelectedData = GResponseData>(arg?: {
        params?: Record<string, unknown> & GAcceptedParams;
        options?: TUseQueryOptions<
            GResponseData,
            AxiosError,
            GSelectedData,
            [first: GOriginQueryKey, second?: Record<string, unknown> & GAcceptedParams]
        >;
    }) =>
        useQuery<
            GResponseData,
            AxiosError,
            GSelectedData,
            [first: GOriginQueryKey, second?: Record<string, unknown> & GAcceptedParams]
        >([originQueryKey, arg?.params], queryFunction, arg?.options);

Generally, GResponseData and GOriginQueryKey can be inferred, and it's also used to shard type across the function.
However, GAcceptedParams is optional, and must be specified manually, it's for further usage's type support.

Currently, when I want to specify GAcceptedParams, I will need to redundantly add the type for GResponseData, which should be auto-inferalbe.

@Eliav2
Copy link

Eliav2 commented Jun 16, 2022

Another year passed without enough attention to this very important feature(especially for library maintainers!)
it's not even on the typescript 4.8 iteration plan

@sky0014
Copy link

sky0014 commented Jul 5, 2022

Why is this feature still ignored, we really need it!

I even can't do a simple type mapping ...

interface TestType {
  something: string;
  other: number;
}

function func<T, K extends keyof T = keyof T>(key: K, arg: T[K]) {  // Here didn't work
  // do something
}

// func<TestType>( should auto complete with 'something' or 'other', and arg should be the right type

@Iuriiiii
Copy link

Iuriiiii commented Sep 6, 2022

function test<A, B>(param1: A, param2: B): B {
    return param2;
}
/* Wanted two type arguments, 1 passed */
/* infer, auto, * or ? doesn't works to infer the generic type */
const value13ShouldBeNumber = test<string>('hello', 13) // value13ShouldBeNumber: expected: number, gets: unknown

Third week learning and using TS and i need this feature... 😅

@btoo
Copy link

btoo commented Sep 6, 2022

the upcoming typescript version may have a fix for this

  • @Iuriiiii Allow skipping some generics when calling a function with multiple generics #10571 (comment) (playground link)
    function test<A, B>(param1: A, param2: B): B {
        return param2;
    }
    /* Wanted two type arguments, 1 passed */
    /* infer, auto, * or ? doesn't works to infer the generic type */
    
    /* use `satisfies` and any type wider than the type(s) you want to infer, e.g. `any`, `unknown`, `{}`, etc. */
    
    /* in fact, the narrowest possible type of the value `13` is, well, `13` */
    const value13ShouldBe13 = // const value13ShouldBe13: 13
        // function test<string, 13>(param1: string, param2: 13): 13
        test('hello', 13) satisfies ReturnType<typeof test<string, any>>;
    
    /* if you want to use `number`, cast it as such */
    const value13ShouldBeNumber = // const value13ShouldBeNumber: number
        // function test<string, number>(param1: string, param2: number): number
        test('hello', 13 as number) satisfies ReturnType<typeof test<string, any>>;
  • @jamesopstad Allow skipping some generics when calling a function with multiple generics #10571 (comment) (playground link)
    function createEvent<TData, TType extends string>(type: TType) {
      return function event(data: TData) {
        return { type, data };
      };
    }
    
    // const setAge: (data: number) => { type: "setAge"; data: number; }
    const setAge =
      // function createEvent<number, "setAge">(type: "setAge"): (data: number) => { type: "setAge"; data: number; }
      createEvent('setAge') satisfies ReturnType<typeof createEvent<number, any>>
  • @niieani Allow skipping some generics when calling a function with multiple generics #10571 (comment) (playground link)
    declare function example<A, B, C>(b: B, c: C): A
    
    // function example<number, string, boolean>(b: string, c: boolean): number
    example('thing', true) satisfies ReturnType<typeof example<number, any, any>>

@Iuriiiii
Copy link

Iuriiiii commented Sep 6, 2022

I really hope I don't have to use that syntax to write my types, the only problem is that TS doesn't infer the generics, just implement something like:

declare function test<A, B | infer>(param1: A, param2: B): B;

@Ranguna
Copy link

Ranguna commented Sep 6, 2022

@btoo

the upcoming typescript version may have a fix for this

"upcoming"?

Hah! This PR has been open for 6 years now.
It's definitely going to be in a future release, whether it's going to be the upcoming one or on version 200.5.3, we'll have to wait and see.

@temoncher
Copy link

temoncher commented Sep 8, 2022

@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

Matchlighter commented Sep 8, 2022

@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

Misaka-0x447f commented Nov 24, 2022

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*/ })

@rudy-xhd
Copy link

rudy-xhd commented Nov 26, 2022

Still waiting for this feature. So sad.

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
Development

Successfully merging a pull request may close this issue.