Search code examples
typescripttypescript-genericstype-inferencetypescript-types

How to infer generic T in return type through a type guard and implicit inference in lower levels?


type Entity<T> = {payload: T};

interface IBaseDataType {name?: string};
interface IDataTypeV1 extends IBaseDataType {id: number};
interface IDataTypeV2 extends IBaseDataType {id: string};

const dataV1: IDataTypeV1[] = [{id: 1, name: '1'}, {id: 2, name: '2'}, {id: 3, name: '3'}, {id: 4, name: '4'}]
const dataV2: IDataTypeV2[] = [{id: '1', name: '1'}, {id: '2', name: '2'}, {id: '3', name: '3'}, {id: '4', name: '4'}]

const getDbItemV1 = (id: number): Entity<IDataTypeV1> => {
  return {
    payload: {
      id,
      name: dataV1.find(d => d.id === id)?.name
    }
  };
}

const getDbItemV2 = (id: string): Entity<IDataTypeV2> => {
  return {
    payload: {
      id,
      name: dataV2.find(d => d.id === id)?.name
    }
  };
}

const isString = (val: unknown): val is string => typeof val === 'string';

type DataType<T> = T extends string ? IDataTypeV2 : IDataTypeV1;

const getDbItem = <T extends string | number>(id: T): Entity<DataType<T>> 
    => isString(id) ? getDbItemV2(id) : getDbItemV1(id); // ?

       // Type 'Entity<IDataTypeV1> | Entity<IDataTypeV2>' is not assignable to type 'Entity<DataType<T>>'.
       // Type 'Entity<IDataTypeV1>' is not assignable to type 'Entity<DataType<T>>'.
       // Type 'IDataTypeV1' is not assignable to type 'DataType<T>'


const getItem = (id: number) => getDbItem(id);

console.log(getItem(3)); // this works and getItem(3) infers to  Entity<IDataTypeV1> and V1 is associated with number param for id, so it seems okay, but the return line in getDbItem throws type error

TypeScript playground link for better visual cue what's happening: initial playground link

EDIT: Thanks for the reply, can you check this playground again (last line): updated playground

EDIT: What if the id is not number | string, but there are separate numberId: number | null and stringId: string | null. How to make the typing properly?

Potential answer to my question (still not strictly inferred ReturnType): Playground

type BaseTypeMap<T extends object, U extends object> = {
  string: { type: string; return: T };
  number: { type: number; return: U };
};
type TypeMap = BaseTypeMap<IDataTypeV2, IDataTypeV1>;

const getDbItem = <T extends keyof TypeMap>(
  numberId: Extract<TypeMap[T]['type'], number> | null,
  stringId: Extract<TypeMap[T]['type'], string> | null
): Entity<TypeMap[T]['return']> =>
  isString(stringId) ? getDbItemV2(stringId) : getDbItemV1(numberId as number);

// Entity<TypeMap[T]['return']> infers number | string | null instead of number (when numberId is not null) or string (when stringId is not null) - that's my goal but I guess impossible

Solution

  • The solution was to use assertion for the return types of the 2 calls

    // const getDbItem = <T extends string | number>(id: T): Entity<DataType<T>> => isString(id) ? getDbItemV2(id) : getDbItemV1(id); // Error: Type 'IDataTypeV1' is not assignable to type 'DataType<T>'.
    
    const getDbItem = <T extends string | number>(id: T): Entity<DataType<T>> 
           => isString(id) 
              ? getDbItemV2(id) as Entity<DataType<T>> 
              : getDbItemV1(id) as Entity<DataType<T>>;
    
    getDbItem(3).payload.id // id is infered as number
    getDbItem('3').payload.id // id is infered as string
    

    Playground

    I'm still open to a potential answer which would eliminate the need of as assertions, but at this point I don't believe such thing is possible with TypeScript.