I have a simple function that can accept a single object of type T or an array of objects of type T[]. It will then do its thing and return a result matching the type passed in (i.e. if an array is passed an array of results are returned, and if a single item is passed, a single result is returned).
The transpiled JS functions exactly as expected, but the type system keeps insisting on nesting the array inside the generic instead and I'm not sure why or how to ensure it resolves to what I want.
Example Function
type OneOrMany<T> = T | T[]
type Item = Record<string, any>
type CapitalizedProps<T extends Item> = {
[K in keyof T as Capitalize<K & string>]: T[K]
}
function toCapitalizedProps<T extends Item>(item: T): CapitalizedProps<T>
function toCapitalizedProps<T extends Item>(items: T[]): CapitalizedProps<T>[]
function toCapitalizedProps<T extends Item>(
itemOrItems: OneOrMany<T>,
): OneOrMany<CapitalizedProps<T>> {
if (Array.isArray(itemOrItems)) {
return itemOrItems.map((item) =>
toCapitalizedProps(item),
) as CapitalizedProps<T>[]
}
const result = { ...itemOrItems }
for (const key in result) {
result[(key[0].toUpperCase() + key.slice(1)) as keyof T] = result[key]
delete result[key]
}
return result as unknown as CapitalizedProps<T>
}
Function overloads use the first match.
And it turns out that an array matches the
Itemconstraint ofRecord<string, any>, because an array has string properties that all have values assignable toany.So when you pass an array, the first overload is used, which infer
Tas an array type.If you swap the order, then the overload that needs to be an array will be checked first and used if it is an array. If not, it will go to the next one.
See playground