1

Update: it looks like for the behaviour desired, TypeScript requires existential generic types - and as if TS 4.1 it doesn't have them. Thanks the helpful answer. I think to solve the typing react-query useQueries there is still a way forward whereby we use unknown when selector is supplied. I'll try and make that work and see where it goes.

Consider the following:

interface Data<TData = unknown, TSelected = unknown> {
    data: TData;
    selector?: (data: TData) => TSelected
}

function makeArrayAsConstItemsForDataTypesOnly<
    TItem extends readonly Data[]
>(arr: [...TItem]): { [K in keyof TItem]: { item: Extract<TItem[K], Data>["data"] } } {
    return arr.map(item => {
        return item.selector 
            ? { item: item.selector(item.data) }
            : { item: item.data }
    }) as any;
}

const returnedData = makeArrayAsConstItemsForDataTypesOnly([
    { data: { nested: 'thing' }, selector: d => d.nested },
    { data: 1 },
    { data: 'two' }])

returnedData takes the type:

const returnedData: [{
    item: {
        nested: string;
    };
}, {
    item: number;
}, {
    item: string;
}]

A selector may or may not be supplied with each element. If supplied, it maps over the supplied data type and transforms the returned data.

Given the above example, then ideally the returned type would be:

const returnedData: [{
    item: string;
}, {
    item: number;
}, {
    item: string;
}]

Alas it isn't, also, in selector: d => d.nested, d takes the type unknown as opposed to the type TData. So we aren't getting the type inference flowing through as hoped.

Pseudo-code for the return type would look like this:

  • for each entry of the array:
    • get the data property
    • if the array entry contains a selector then return { item: entry.selector(entry.data) }
    • else return { item: entry.data }

Is it possible to express this via the type system in TypeScript? See playground here.

So there's two problems here:

  • selector flowing through TData as the input
  • the return type of the overall function
John Reilly
  • 5,791
  • 5
  • 38
  • 63

1 Answers1

1
// credits goes to https://stackoverflow.com/questions/50374908/transform-union-type-to-intersection-type/50375286#50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
  k: infer I
) => void
  ? I
  : never;

//credits goes to https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468114901
type UnionToOvlds<U> = UnionToIntersection<
  U extends any ? (f: U) => void : never
>;

//credits goes to https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468114901
type PopUnion<U> = UnionToOvlds<U> extends (a: infer A) => void ? A : never;

//credits goes tohttps://stackoverflow.com/questions/53953814/typescript-check-if-a-type-is-a-union#comment-94748994
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;

type UnionToArray<T, A extends unknown[] = []> = IsUnion<T> extends true
  ? UnionToArray<Exclude<T, PopUnion<T>>, [PopUnion<T>, ...A]>
  : [T, ...A];

type Values<T> = T[keyof T]
type MapPredicate<T> = { item: Values<T> };

// http://catchts.com/tuples
type MapArray<
  Arr extends ReadonlyArray<unknown>,
  Result extends unknown[] = []
  > = Arr extends []
  ? Result
  : Arr extends [infer H]
  ? [...Result, MapPredicate<H>]
  : Arr extends readonly [infer H, ...infer Tail]
  ? MapArray<Tail, [...Result, MapPredicate<H>]>
  : never;

type Test1 = MapArray<[{nested:42},{a:'hello'}]>[0] // { item: 42; }

interface Data<TData = any, TSelected = any> {
  data: TData;
  selector?: (data: TData) => TSelected
}

const builder = <T, R>(data: T, selector?: (data: T) => R): Data<T, R> => ({
  data,
  selector
})

type Mapper<T extends Data> = T['selector'] extends (...args: any[]) => any ? ReturnType<T['selector']> : T['data']

const first = builder({ nested: 'thing' }, d => d.nested);
const second = builder({ a: 42 });

type First = typeof first
type Second = typeof second

type Result = Mapper<First>

const arr = [first, second];

function makeArrayAsConstItemsForDataTypesOnly<T extends Data>(data: Array<T>) {
  const result = data.map((item) => {
    return item.selector
      ? { item: item.selector(item.data) }
      : { item: item.data }
  })

  /**
   * I don't know how to avoid type casting here
   * I tried different approaches, but none of them
   * helped
   */
  return result as MapArray<UnionToArray<Mapper<T>>>
}

const test = makeArrayAsConstItemsForDataTypesOnly(arr)

type ResultArray = typeof test;

type FirstElement = ResultArray[0] // { item: string }
type SecondElement = ResultArray[1] // { item: number }

I know, using type casting is not the best solution, but I was unable to infer generics in better way.

This answer might help you to build data structures with callback in a better type safe way

These links might help you to understand what's goin on here:

  • Thanks for sharing your answer @captain-yossarian. Unfortunately in my case having the `builder` function (which I think is only to provide a type) in the mix won't work for my scenario. For context, I'm trying to strongly type the `useQueries` hook of react-query and as such I'm unlikely to be able to change the API significantly. You can see context on this PR: https://github.com/tannerlinsley/react-query/pull/1527#issuecomment-756125257 – John Reilly Jan 10 '21 at 15:24
  • Incidentally, I'd say that limited use of type assertions inside a function like this is fine - and may in fact be unavoidable – John Reilly Jan 10 '21 at 15:27
  • Okay I've read the linked question and I think I'm asking for something that the language doesn't yet support - essentially it requires existential generic types. That is what `builder` was fulfilling. That's never going to work for the react-query use case. I think I need to pivot the question. I'll add an edit above. – John Reilly Jan 10 '21 at 15:45
  • 1
    @JohnReilly, yes, `builder` function is only for typings. It is an overhead, but I believe V8 will inline it. Unfortunatelly it is not that easy to infer generic of callback argument in above case. I tried to use other data structures, but without significant success. Btw, question is very good ) – captain-yossarian from Ukraine Jan 10 '21 at 16:32