Home > OS >  Typescript Generics return type based on argument
Typescript Generics return type based on argument

Time:11-22

How to do dynamic typescript return type based on an argument is undefined or not. Please help.

Also added a Typescript Playground Snippet for reference

I have come up with this, but is not working as expected.

V extends keyof T ? ToObject<T> : ToObject<T[keyof T]>

The code:

export interface ToObject<T> {
  [k: string]: T;
}

export const toObject = <T, V = (keyof T)>(
  list: T[],
  key: keyof T,
  valueKey?: V
): V extends keyof T ? ToObject<T> : ToObject<T[keyof T]> => valueKey === undefined
    ? list.reduce((all, cur) => ({
      ...all,
      [cur[key] as unknown as string]: cur
    }), {} as ToObject<T>)
    : list.reduce((all, cur) => ({
      ...all,
      [cur[key] as unknown as string]: cur[valueKey as unknown as keyof T]
    }), {} as ToObject<T[keyof T]>);

Usage :

interface Data{
  a: string;
  b: string;
  c: string;
}
const list: Data[] = [
  {a:'a', b: 'b', c: 'c'},
  {a:'1', b: '2', c: '3'}
];

// use 1
toObject<Data>(list, 'a');
// response
{
  {'a': {a:'a', b: 'b', c:'c'}},
  {'1': {a:'1', b:'2', c: '2'}},
}

// Use 2
toObject<Data>(list, 'a', 'b');
//response
{ {'a': 'b'}, {'1': '2'} }

ISSUE:

// use 1 ERROR -> Cant do this
const dataMap: ToObject<Data> = toObject<Data>(list, 'a');
// Type 'ToObject<Data> | ToObject<string>' is not assignable to type 'ToObject<Data>'.
// Type 'ToObject<string>' is not assignable to type 'ToObject<Data>'.
// Type 'string' is not assignable to type 'Data'.(2322)

// Use 2 ERROR -> Cant do this
const dataKeyMap: ToObject<string> = toObject<Data>(list, 'a', 'b');
// Type 'ToObject<Data> | ToObject<string>' is not assignable to type 'ToObject<string>'.
// Type 'ToObject<Data>' is not assignable to type 'ToObject<string>'.
// Type 'Data' is not assignable to type 'string'.(2322)

CodePudding user response:

As written, TS does not have enough information differentiate between toObject's two return types.

If we add function overloads, we can tie a return type to a function signature. If valueKey is passed, toObject returns ToObject<T[keyof T]>, if not, then the function returns ToObject<T>.

Revised Playground

export function toObject <T, V = (keyof T)>(list: T[], key: keyof T): ToObject<T>
export function toObject <T, V = (keyof T)>(list: T[], key: keyof T, valueKey: V): ToObject<T[keyof T]>
export function toObject <T, V = (keyof T)>(list: T[], key: keyof T, valueKey?: V): ToObject<T> | ToObject<T[keyof T]> {
  return valueKey === undefined
    ? list.reduce((all, cur) => ({
      ...all,
      [cur[key] as unknown as string]: cur
    }), {} as ToObject<T>)
    : list.reduce((all, cur) => ({
      ...all,
      [cur[key] as unknown as string]: cur[valueKey as unknown as keyof T]
    }), {} as ToObject<T[keyof T]>);
}  

In addition to making the TS compiler happy, the overloads also inform human users how they should call the function.

  • Related