typescript

Can I type a function return based on the items in an array parameter?


I’m working with an API pattern where calling a getItem function returns an item with a small number of properties.

{ id: 123, isBig: true }

I can also pass an array called properties into my getItem function to request additional properties.

getItem({ properties: ['color'])) should return { id: 123, isBig: true, color: 'red' }

My Item type has the additional properties as optional properties:

{ id: number, isBig: boolean, color?: 'string' )

I would like to be able to type my getItem function such that if I pass in { properties: ['color'] }, TypeScript can infer that the color property will be present on the return value.

My first thought was to use generics, something along the lines of this.

type APIFunction<TReturnType, TProperties extends (keyof TReturnType)[] =[]> = 
  (params: { properties: TProperties }) => // oops, what do we return here?

But this approach requires looping through the generic TProperties array and I don’t know that that’s possible. Is this something I can do in TypeScript?


Solution

  • In order for this to work, you'd need APIFunction<Item> to be a generic function; that is, the second type parameter (which corresponds roughly to your TProperties parameter) needs to move to the call signature, as follows:

    type APIFunction<R> = <K extends keyof R>
        (params: { properties: K[] }) => R & Required<Pick<R, K>>;
    

    So R is the expected basic return type of your function, and K is the union of keys passed in as the properties array. Then the return type is R intersected with Required<Pick<R, K>> using the Required and Pick utility types to say that the return type is R with all of the properties at keys K definitely present.

    Let's test it out:

    type Item = { id: number, isBig: boolean, color?: string };
    
    declare const getItem: APIFunction<Item>;
    const item1 = getItem({ properties: [] });
    item1.color.toUpperCase(); // error, possibly undefined
    
    const item2 = getItem({ properties: ['color'] });
    item2.color.toUpperCase(); // okay
    

    Looks good. In the first case, item1 is of type Item & Required<Pick<Item, never>> which is equivalent to Item and so the color property is possibly undefined. In the second case, item2 is of type Item & Required<Pick<Item, "color">> which is Item & {color: string}, equivalent to { id: number, isBig: boolean, color: string }, so the color property is definitely defined.

    Note that you'd probably need to implement getItem() separately from any other APIFunction<R> for other R types, since that implementation needs to know specifically that it's looking up Items from somewhere as opposed to some other types. But the implementation is out of scope for the question as asked.

    Playground link to code