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
3
  • Do you only have a problem with d.nested ? Commented Jan 9, 2021 at 19:30
  • No - there's 2 problems: 1. In selector: d => d.nested d has the type of unknown as opposed to TData. 2. The problem stated in the question. I should probably edit it for clarity - is that what you're driving at? Commented Jan 10, 2021 at 8:29
  • Edited and mentioned above Commented Jan 10, 2021 at 8:38

1 Answer 1

2
// 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:

Sign up to request clarification or add additional context in comments.

4 Comments

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: github.com/tannerlinsley/react-query/pull/…
Incidentally, I'd say that limited use of type assertions inside a function like this is fine - and may in fact be unavoidable
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.
@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 )

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.