Typescript: Allow skipping some generics when calling a function with multiple generics

Created on 27 Aug 2016  Â·  50Comments  Â·  Source: microsoft/TypeScript

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

In Discussion Suggestion

Most helpful comment

I can't tell you how many times I've needed this feature and had to do something super ugly to dodge the non-existence of it (e.g., creating a wrapper function that has a subset of the generic types).

All 50 comments

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

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

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

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

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

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.

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

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?

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

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

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

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.

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.

Simpler sample for the issue.
This is very common pattern in C++ to specify only first parameter and deduce others.
I believe TypeScript should allow this too:

function convert<To, From>(from: From): To {
    return from as any as To; // doesn't really matter
}

var x = convert(1); // OK ?!?  To = {}  , I want a compilation error here.
var x = convert<number>(1); // Error
var x = convert<number, number>(1); // Correct

The main issue is automatic deducing '{}' without any error which is inappropriate for me at all:

I would implement this using default params syntax like:

const prop =
  <Obj extends {[K in P]?: {}}, P extends string = P>(prop: P):

But not sure if there are really need cases when you want to skip params in the middle,

Just to provide another use case that affects many typescript + jasmine users, this would allow to correctly type jasmine.createSpyObj

It creates an object that can impersonate the type you are trying to mock, but also has a bunch of spies, for properties of that object that you list on its second argument.

It needs two generic parameters, the type being "mocked" and some keys of that type. A proper typing of that function would be:

createSpyObj<Obj, MethodName extends keyof Obj>(
    name: string, methodNames: MethodName[]):  Obj & { [K in Methods]: jasmine.Spy; };

The second function argument is already the list of keys being mocked, so that can be inferred. However, the type that you are mocking can't because it doesn't show up in the parameter list. Without skipping the generic arguments that can be inferred, one would need to use it like this:

jasmine.createSpyObj<MyClass, 'method1'|'method2'>('name', ['method1', 'method2']);

It is annoying to have to write the list of keys twice and keep them in sync. Instead, it would be nice if the second generic would be inferred automatically, and one could write:

jasmine.createSpyObj<MyClass>('name', ['method1', 'method2']);

That can't be accomplished with default generics, because in absence of a generic parameter, the compiler favors the default over anything that could be inferred. You want the second generic parameter to be inferred from the second function parameter, not to default to something.

Alternatively, the behavior of default generics could be modified to trigger only when the type can't be inferred, rather than when the type is not listed. That might break existing code, though.

The following "workaround" is more of a curiosity than a practical workaround, but the fact that function types carry type parameters allows for a way to infer some parameters and not others of a call to a "curried" API call, such as this one that constructs a pair:

const pair: <X>(x: X) => <Y>(y: Y) => [X, Y] =
    <X>(x: X) => <Y>(y: Y) => [x, y];

const p1 = pair<number>(1)<string>('asdf');
const p2 = pair(1)<string>('asdf');
const p3 = pair<number>(1)('asdf');

Pretty interesting that this works.

Riffing on the above suggestion, I've had several occasions where it made sense to write a builder or currying wrapper in order to leverage inference of complex, hard-to-specify types.

auto would be incredibly helpful in these situations.

There are two situations I've encountered where mixing explicit and implicit type parameters would be a huge improvement:

1: Mix-ins

A mix-in is a function that takes a class as a parameter and returns an extension of that class. It's a bit like monkey-patching, except you create a new prototype chain rather than modifying an existing one.

Here's a simple mix-in:

export interface Echoable<T> {
  echo(): T;
}

export interface HasValue<T> {
  _value: T;
}

export function withEcho<T, S extends Constructor<HasValue<T>>>(superclass: S): S & Constructor<Echoable<T>> {
  return class extends superclass implements Echoable<T> {
      echo(): T {
        return this._value;
      );
    }
  };
}

class BaseExample {
  _value = 5;
}

class EchoExample extends withEcho(BaseExample) {}

When TypeScript tries to infer the type of T, it chooses {}, which is usually incorrect - you need to specify T to get your program to compile. However, S merely exists to passthrough the shape of the base class. Not only should it be inferable, but precisely specifying it isn't always possible. This is what I've had to resort to to get the types to be correct on a chain of mix-ins:

export function withMotionOperators<T, S extends Constructor<Observable<T>>>(superclass: S): S
    & Constructor<ObservableWithMotionOperators<T>> {
  const result = withFoundationalMotionOperators<T, S>(superclass);
  const result1 = withDedupe<T, typeof result>(result);
  const result2 = withLog<T, typeof result1>(result1);
  const result3 = withMerge<T, typeof result2>(result2);
  const result4 = withTimestamp<T, typeof result3>(result3);
  const result5 = withStartWith<T, typeof result4>(result4);
  const result6 = withRewrite<T, typeof result5>(result5);
  const result7 = withRewriteTo<T, typeof result6>(result6);
  const result8 = withRewriteRange<T, typeof result7>(result7);
  const result9 = withPluck<T, typeof result8>(result8);
  const result10 = withAddedBy<T, typeof result9>(result9);
  const result11 = withSubtractedBy<T, typeof result10>(result10);
  const result12 = withMultipliedBy<T, typeof result11>(result11);
  const result13 = withDividedBy<T, typeof result12>(result12);
  const result14 = withDistanceFrom<T, typeof result13>(result13);
  const result15 = withUpperBound<T, typeof result14>(result14);
  const result16 = withLowerBound<T, typeof result15>(result15);
  const result17 = withThresholdRange<T, typeof result16>(result16);
  const result18 = withThreshold<T, typeof result17>(result17);
  const result19 = withIsAnyOf<T, typeof result18>(result18);
  const result20 = withAppendUnit<T, typeof result19>(result19);
  const result21 = withInverted<T, typeof result20>(result20);
  const result22 = withDelayBy<T, typeof result21>(result21);
  const result23 = withIgnoreUntil<T, typeof result22>(result22);
  const result24 = withVelocity<T, typeof result23>(result23);

  return result24;
}

As you might imagine, I don't look forward to maintaining that code, but without partial inference, I couldn't find a better way to return the precise type of the resulting class.

2: Generics that can be computed from one another

Consider a method that receives input T, looks it up in Map<T, U>, and emits U.

rewrite<U>(dict: Map<T, U>): Observable<U> {
  return this.map(
    (value: T) => dict.get(value)
  );
}

Imagine you want to make it possible to change the value emitted, so now the values in dict can be either static or observables:

rewrite<U, R extends U | Observable<U>>(dict: Map<T, R>): Observable<U> {
  const latestValues = new Map();

  // ...subscribe to every stream in dict and update latestValues when one emits

  return this.map(
    (value: T) => latestValues.get(value)
  );
}

// and using it:
const number$ = boolean$.rewrite<number>(new Map([[ true, 1 ], [ false, 0 ]]));

Because types can't be selectively inferred, the ability to pass either U or Observable<U> leaks into every call-site, even if they don't use the new feature:

// This is identical logic, but the caller needs to understand and specify a more
// complex type signature to support a feature they aren't even using
const number$ = boolean$.rewrite<number, number>(new Map([[ true, 1 ], [ false, 0 ]]));

Because R is defined in terms of U, it ought to be inferable; thus, rewrite would only need one generic parameter to be explicitly specified:

const number$ = boolean$.rewrite<number>(new Map([[ true, 1 ], [ false, 0 ]]));

TypeScript just needs to treat explicit type arguments the same as the types of value arguments, so that everything participates in inference on an equal footing.

E.g. we can awkwardly rearrange most function types and their application sites so that all our explicit type arguments turn into a dummy value argument. Then inference works fine:

declare const case3: <A, B, C>(b: B, c: C, dummy?: { a?: A, b?: B, c?: C }) => A

const a: number = case3('thing', true, undefined as { a: number }); // everything's good here

I don't think we really want syntax like auto to explicitly specify "infer here", we just need the semantics of the straightforward syntax case3<number>('thing', true) to be equivalent to the awkward desugaring above. Perhaps it would be nice to have a little syntax for specifying type arguments by name so the position of the type parameters becomes irrelevant: case3<A=number>('thing', true).

@masaeedu nice trick with the type witness undefined as .... Unfortunately it doesn't work with --strict:

error TS2352: Type 'undefined' cannot be converted to type 'string'.

One could first cast to any then to the proper type but that'll get pretty verbose for the caller.. Then we could put an explicit value of that type (like "" for string), but it's really quite poor and could be baffling for someone reading the code, assuming that value is in fact an input to the function. Any better idea while we don't have the real proper solution?

@emmanueltouzery You can do {} as ... if you don't like undefined as any as .... To be honest I'm not recommending you actually start using this in code, I'm just trying to illustrate that the proper semantics are already available in TypeScript, it just doesn't use them for type arguments proper. I've opened #20122 to request that plain type inference be changed so that it works the same as the hack above.

@masaeedu thank you! Works great. Actually intend to use this in real code with {} as... It's not ideal but way better than explicitely giving 4 type parameters when only 1 is needed, and type witnesses are a rather spread practice in other languages (for instance haskell). And when writing a library it's optional for users, as the type witness parameter is optional anyway -- they can still list all the types if they prefer that.

Of course when the proper support comes to typescript I'll port first thing.

@emmanueltouzery Ok, if you are going to use this, there's some subtle things to watch out for. For one thing, the encoding I've posted above is less restrictive then explicit type arguments when redundantly specified:

type Animal = { move(): void }
type Dog = Animal & { woof(): void }
declare const animal: Animal

declare const fn: <T>(x: T, witness?: { t?: T }) => void

fn<Dog>(animal)              // This is an error (T is explicitly set to Dog)
fn(animal, {} as { t: Dog }) // But this is not (T is unified to Animal)

If you want it to become exactly as restrictive, you need to enforce contravariance using a slightly more awkward encoding:

type Witness<T> = { [P in keyof T]?: (a: T[P]) => void }

// ...
declare const fn: <T>(x: T, witness?: Witness<{ t: T }>) => void

fn(animal, {} as Witness<{ t: Dog }>) // Now you get the same type error here

Note that this depends on function contravariance (--strictFunctionTypes), so you need TypeScript >= 2.6 for this.

Here's my practical example where partial inference of template params would be very nice...

Reference this playground link where I experimented with creating a visitor pattern implementation for string literal types (also works with string enums): https://goo.gl/sPhnng
(enable all strict options for best results)

For this discussion, focus on (one of) the signature for "visitString":

function visitString<R, S extends string>(value: S, visitor: StringVisitor<R, S>): R;

Currently, either both R and S must be explicitly specified when calling visitString, or both must be inferred.

It's much more helpful to specify R and let S be inferred, which allows me to clearly communicate my intent (to both the compiler and other developers that read my code) for what type the visitor should return, then the compiler helps enforce that I implement all the visitor's methods correctly to return the desired type:

const enum TestEnum {
    A = "A Value",
    B = "B Value",
    C = "C Value"
}

function getEnumValue(): TestEnum {
    return TestEnum.C;
}

// 'S' type param should be inferred from function param type
const booleanResult = visitString<boolean>(getEnumValue(), {
    [TestEnum.A]: () => {
        alert("A");
        return true;
    },

    [TestEnum.B]: () => {
        alert("B");
        return false;
    },

    [TestEnum.C]: () => {
        alert("C");
        // helpful compiler error should quickly direct me to the fact that
        // I messed up here by not returning a boolean value.
    }
});

(edited to update playground link and minor syntax change)

For my particular use case (the "string visitor" I was trying to create in my previous comment), I came up with a work-around. I explain it fully in this comment: https://github.com/Microsoft/TypeScript/issues/20643#issuecomment-352328395

@mhegazy
I don't think there's a way to fix this without breaking backwards compatability or having a bunch of caveats (like, we only do inference for missing type parameters when there is only a single signature and no overload resolution), because we use type parameter arity to inform overload resolution, for example:

const f: {
    <T>(): string,
    <T, U>(): number,
} = () => void 0;

const y = f<any>();
const z = f<any, any>();

we give differing types for both calls when they only differed by type argument count (where if we inferred for "missing" parameters, they might be the same - the correct behavior isn't obvious to me).

Are we okay with either breaking overload resolution or only doing this for signatures which need no overload resolution? Both options have pretty big caveats.

Or option 3 (as mentioned in the OP), there's a specific syntactic opt-in, like const a = f<any, /*syntactic marker here*/>(); (it can't just be an omitted expression since then trailing commas are a bit ambiguous - though that's also true for argument lists, so maybe that's OK) that says "I would like inference for this parameter even though I provided other ones". Though this would miss half the use-cases mentioned in this thread to do with refactoring and expecting things to be inferred.

@weswigham Option 4: There could be something like --disableInferredGenerics.

@weswigham Can we allow inferring type parameter only on optional generic type like https://github.com/Microsoft/TypeScript/issues/14400 suggests?

For non optional generic parameter, I think it is better for users to write explicit marker. This also echoes this comment. https://github.com/Microsoft/TypeScript/pull/21316#issuecomment-359449739

I guess its also worth looking at what @rust-lang does. They allow inference for generic type parameters by using the underscore _ in place of a generic variable. You fill out some generic parameters and the compiler will deduce the rest (if it can).

// Vec<T> is a resizable array in Rust, T will be deduced to Rust's equivalent of `number`
[1, 2, 3].into_iter().collect::<Vec<_>>();

This is especially useful for unnameable / extremely large types, as they turn up when using mixins (you get really large types when using mixin functions so you can't really write them down).

@NeoLegends
* should be used instead of _

IMO the underscore conveys the meaning of existential types (a la „Please deduce existing type here“) better than the star, because the star looks like „any type can fit here“ (which is not true). But lets leave the bikeshedding for later when the semantics of the feature have been thought through.

Maybe github needs a 🚲 reaction.

For anyone watching this: I have a candidate implementation for this available in #22368 leveraging the infer T types that were added alongside conditional types, and would love to know how it matches up against your needs/expectations.

well, seems, I'll need to wait TypeScript 3.0.
I have encounter such a situation,
because I want to promisify some callback style function. so I write some code like this:

export function promisify<P extends wx.BaseOptions, R>(
    fun: (options: P) => any
  ): ((options: P) => Promise<R>) {
    function newFun(options: P): Promise<R> {
      return new Promise(function(resolve, reject) {
        options.success = res => {
          resolve(res);
        };
        options.fail = () => {
          reject();
        };
        fun(options);
      });
    }
    return newFun;
  }

  export const login = promisify<wx.LoginOptions, wx.LoginResponse>(wx.login);

And aboviously the first generic type wx.LoginOptions can be inferred from wx.login function.

I think seeing the last implementation has given us some pause on pursuing this. Additionally, we've been exploring other potential features that work with generics & inference (e.g. associated types) and we'd like to make sure everything plays nicely as well.

That's brutal. This feature along with lazy evaluation of generics are the two biggest missing features in Typescript IMO.

@DanielRosenwasser It's worth noting that Flow is working on implementing a similar concept (after they've deprecated * which worked slightly differently). They're using underscore (_) instead of (*), see their commit here: https://github.com/facebook/flow/commit/fb23b93750b8b72307c889dce8f6830f6fc0a30e

hello.
i have same case that i want to skip first optional generic type but i can not do this. can you help me?
i have this types:

type TDefaultViewStyleKeys ={
  containerStyle: any
}
type TTextStyleKeys = {
  textStyle: any
}

export type TViewStyles<T> = {
  [key in keyof T]?: StyleProp<ViewStyle>
};
export type TTextStyle<T> = {
  [key in keyof T]?: StyleProp<TextStyle>
}

export type TStyles<ViewStyleKeys=TDefaultViewStyleKeys, TextStyleKeys = TTextStyleKeys> = TViewStyles<ViewStyleKeys> & TTextStyle<TextStyleKeys>

and i want use a type that first arg of TStyles must be skiped:

type TTextStyleKeys = {
  labelStyle: any,
  textInputStyle:any
}
type TStyleInput = TStyles < ,TTextStyleKeys>

but compiler get an error on , .
how do i solve this case?

Given the OP's example, could you do just:

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

...and not have an auto/_/* placeholder? Even if that is more restrictive, it solves a lot of use-cases.

This is a common use-case for me - converting one type to another. The source type can be inferred, but the destination cannot.

// utils file
export function mapEnum<D extends string, S extends string>(value: S, object: {[K in S]: D}): D  {
    return object[value]
}

// file that imports mapEnum
export function upgrade(element: TextHeading): Heading {
    const a = element.attributes
    return {
        // ... omitted other keys
                //
                // Heading.size expects "h1" | "h2"
                // TextHeading.attributes.headingSize has "big-heading" | "small-heading"
                //
                // mapEnum converts:
                // "big-heading" -> "h1"
                // "small-heading" -> "h2"
                //
                // throw: Expected 2 type arguments, but got 1.
        size: mapEnum<Heading["size"]>(a.headingSize, {
            "big-heading": "h1",
            "small-heading": "h2",
        }),
    }
}

...which requires me to change it to:

mapEnum<Heading["size"], TextHeading["attributes"]["headingSize"]>(a.headingSize, ...)
// OR
mapEnum<Heading["size"], typeof a.headingSize>(a.headingSize, ...)

...or make a compromise:

export function mapEnum<V extends string>(value: string, object: {[k: string]: V})  {
    return object[value]
}

...which makes the return "h1" | "h2" but make the keys string instead of "big-heading" | "small-heading" - so if you typo the source keys the compiler won't pick up the mistake.

I have tried something like this:

export function mapEnum<V extends string, O extends {[K in V]: O[V]}>(value: V, object: O): O[V]  {
    return object[value]
}

But O[V] is string. Unless I do this:

mapEnum(a.headingSize, {
    "big-heading": "h1" as "h1", // "h1" is assumed to be `string` without cast
    "small-heading": "h2" as "h2",
})

I also run into rubenlg's case with testing frameworks as well, with spyOn, as well as other unit testing methods. The use-case is very similar - the source type can be inferred, but the destination cannot. So you either have to specify both, or none - making you choose between verbose typing or less restrictive typing.

Is there any update for this?

@lukescott

I think what you suggested would be too much of a break from the current way it works (i.e. you must specify all generics or none, no middle ground).

But as a solution I loved the suggestion about using the = infer syntax as a default value. I think the resulting syntax would be super clean.

export function mapEnum<D extends string, S extends string = infer>(value: S, object: {[K in S]: D}): D  {
    return object[value]
}

Additionally this could provide some protection if the value _could not_ be inferred (rather than the generic defaulting to any which is probably never what you want).

export function mapEnum<D extends string, S extends string = infer>(value: S | null, object: {[K in S]: D}): D  {
    return object[value]
}

mapEnum<number>(null, {}); // TS error "cannot infer generic S" or similar.

Another thought I'd had was an _always_ inferred generic using infer S syntax, that would actually stop you setting it manually. I might be trying to push this too far now, but I think it'd round out this set of functionality nicely.

// Either this syntax.
export function mapEnum<D extends string, infer S extends string>(value: S, object: {[K in S]: D}): D  {
    return object[value]
}

// Or this syntax (I can't decide).
export function mapEnum<D extends string>(value: infer S extends string, object: {[K in S]: D}): D  {
    return object[value]
}

mapEnum<number, "b">("a", {}); // TS error "generic S cannot be set manually, it must be inferred"

I consider the second form is a better option as it makes all inferred types available in the generic <> declaration. Otherwise, you don't know what the compiler has inferred.

export function mapEnum<D extends string, infer S extends string>(value: S, object: {[K in S]: D}): D  {
    return object[value]
}

I can't tell you how many times I've needed this feature and had to do something super ugly to dodge the non-existence of it (e.g., creating a wrapper function that has a subset of the generic types).

Was this page helpful?
0 / 5 - 0 ratings

Related issues

manekinekko picture manekinekko  Â·  3Comments

remojansen picture remojansen  Â·  3Comments

MartynasZilinskas picture MartynasZilinskas  Â·  3Comments

Roam-Cooper picture Roam-Cooper  Â·  3Comments

bgrieder picture bgrieder  Â·  3Comments