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?
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 Item
s from somewhere as opposed to some other types. But the implementation is out of scope for the question as asked.