DEV Community

juliancollinson2
juliancollinson2

Posted on

Am I the only one who finds this frustrating about generics?

The problem

Here is my problem, when a class/function has multiple generic types that are not mandatory to specify it often happens that the code looks like this:

const instance = new GenericClass<any, any, number>();
Enter fullscreen mode Exit fullscreen mode

or

const result = genericFunction<string, any, any, number>();
Enter fullscreen mode Exit fullscreen mode

Basically what I am looking for is to avoid specifying all the generic types where I want to leave the default type.

My solution

I had studied this problem a lot, but I am not able to find a solution that doesn't become a problem too.

This is what I come up with after a sleepless night:

First I specify which properties have a generic type and create a utility type GetGeneric which returns the specified type or, in case the type has been omitted for that property, the default type.

type GenericProps = 'input' | 'output';
type AcceptedGenerics = { [Property in GenericProps]: any };

type GetGeneric<T extends Partial<AcceptedGenerics>, Prop extends keyof AcceptedGenerics, TDefault = any> = T extends Pick<AcceptedGenerics, Prop> ? T[Prop] : TDefault
Enter fullscreen mode Exit fullscreen mode

Then here is how I use this method for classes:

class Obj<T extends Partial<AcceptedGenerics>> {
    input: GetGeneric<T, 'input', string>
    output: GetGeneric<T, 'output', string>
}

const instance = new Obj<{ output: number }>();

/*
    instance = {
       input: string,
       output: number,
    }
*/
Enter fullscreen mode Exit fullscreen mode

And here the function version:

function Func<T extends Partial<AcceptedGenerics>>(
    input: GetGeneric<T, 'input'>
): GetGeneric<T, 'output', {}> {
  return { } as GetGeneric<T, 'output', {}>; 
}

const result = Func<{ input: number, output: { prop: string } }>(2);

/*
    Func<{ input: number, output: { prop: string } }>() = (input: number) => { prop: string }
*/
Enter fullscreen mode Exit fullscreen mode

I also noticed that in case the extended use of the GetGeneric utility type make the code look messy, it is possible to define all the generic type like so:

function Func<T extends Partial<AcceptedGenerics>, TInput = GetGeneric<T, 'input'>, TOutput = GetGeneric<T, 'output', {}>>(
    input: TInput 
): TOutput {
  return { } as TOutput; 
}
Enter fullscreen mode Exit fullscreen mode

But I am not sure if this solution is better or worst.

Conclusions

I am asking you what you think about this solution, if you have any suggestions and if you think this was a problem to solve in the first place.

Top comments (1)

Collapse
 
juliancollinson2 profile image
juliancollinson2 • Edited

A better way now I think is to use types.

for objects:

type EndPoint<T extends Partial<{ [Key in 'input' | 'output']: any }>> = T & Omit<{
  output: string;
  route: string;
  method: 'get' | 'post';
}, keyof T>;

type MyEndPoint = EndPoint<{ output: number }>;

const ep: MyEndPoint = {
  output: 3,
  route: '/get',
  method: 'get'
}
Enter fullscreen mode Exit fullscreen mode

for functions:

type CallEndPoint<T extends Partial<{ [Key in 'input' | 'output']: any }>> = 
  'input' extends keyof T ? (input: T['input'], method: 'get' | 'post') => 'output' extends keyof T ? T['output'] : void
  : (method: 'get' | 'post') => 'output' extends keyof T ? T['output'] : void

type CallMyEndPoint1 = CallEndPoint<{ input: number, output: { prop: string } }>
type CallMyEndPoint2 = CallEndPoint<{ input: number }>
type CallMyEndPoint3 = CallEndPoint<{ output: string }>

const call1: CallMyEndPoint1 = (input: number, method: 'get' | 'post') => {
  return { prop: 'it is working!' };
}

const call2: CallMyEndPoint2 = (input: number, method: 'get' | 'post') => {
  console.log('it is working!');
}

const call3: CallMyEndPoint3 = (method: 'get' | 'post') => {
  return 'it is working!';
}
Enter fullscreen mode Exit fullscreen mode