Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Narrowing conditional type in typescript without discriminant

Tags:

typescript

I've got a simple api call function that fetches data and maps the result conditionally based on the arguments flags.

As a fetch result I'm getting a conditional type or a union type, but I'm not able to use type guards here because these two types don't share a common discriminated property and I'm supposed to discriminate them based on external type. How can I tell typescript that these two types depend on another type? Is this the case to add another layer of indirection?

// prerequisites
declare function apiService(...a: any[]): any;
declare function mapExternalFoo(x: IExternalFoo): IFoo;
declare function mapExternalBar(x: IExternalBar): IBar;
interface IFoo { a: any; }
interface IBar { b: any; }
interface IExternalFoo { c: any; }
interface IExternalBar { d: any; }

interface IProps<T extends boolean> {
  isExtended?: T;
}

// I want it to look like this, but it fails with a compile error

function fetchData<T extends boolean>(params: IProps<T>): Promise<T extends true ? IFoo : IBar> {
  return apiService('/user/data', params).then(
    (data: T extends true ? IExternalFoo[] : IExternalBar[]) =>
      // Cannot invoke an expression whose type lacks a call signature. 
      params.isExtended ? data.map(mapExternalFoo) : data.map(mapExternalBar)
  );
}

I was able to get this to work with type casting and to get the correct return types with function calls but it's cumbersome and I feel that's not the right way to do it

function fetchData(params: IProps<true>): Promise<IFoo[]>;
function fetchData(params: IProps<false>): Promise<IBar[]>;
function fetchData<T extends boolean>(params: IProps<T>) {
  return apiService('/user/data', params)
    .then(
      (data: IExternalFoo[] | IExternalBar[]) =>
        params.isExtended
          ? (data as IExternalFoo[]).map(mapExternalFoo)
          : (data as IExternalBar[]).map(mapExternalBar)
    );
}

Adding a type alias for the conditional type helps to get rid of the lack of call signature error, but I'm getting mismatched types error instead

type INarrow<T extends boolean> = T extends true ? IExternalFoo[] : IExternalBar[];
like image 545
infctr Avatar asked Sep 20 '18 16:09

infctr


1 Answers

TypeScript doesn't really distribute its control-flow analysis over union types, and deals even less gracefully with conditional types. At times like this I wish that you could tell the compiler to manually walk through some set of possible narrowings of an expression, and succeed if and only if all such narrowings are seen to be type safe. Alas, this doesn't exist, and it's difficult to say if this pain point will get better in the foreseeable future.

Given that unfortunate situation, the most straightforward way to proceed is probably using type assertions inside function implementations (like your data as IExternalFoo[]), plus overloads on the function signature (you only need one overload signature, with conditional types... and one implementation signature with unions). This is almost exactly what you've done, so I don't have much advice for you other than "that's what I've done too in situations like this". Conditional types are much more useful for function callers than they are for function implementers.

So my only recommendation is that you can keep your conditional type in the overload signature:

function fetchData<T extends boolean>(params: IProps<T>): Promise<T extends true ? IFoo[] : IBar[]>;
function fetchData(params: IProps<boolean>): Promise<IFoo[] | IBar[]> {
  return apiService('/user/data', params)
    .then(
      (data: IExternalFoo[] | IExternalBar[]) =>
        params.isExtended
          ? (data as IExternalFoo[]).map(mapExternalFoo)
          : (data as IExternalBar[]).map(mapExternalBar)
    );
  }

const promseIFooArray = fetchData({isExtended: true});
const promseIBarArray = fetchData({isExtended: false});
const promiseEitherArray = fetchData({isExtended: Math.random()<0.5});

Oh well, hope that helps. Good luck!

like image 128
jcalz Avatar answered Oct 23 '22 17:10

jcalz