Check out this solution with detailed explanations in the comments
interface Base {
id: string;
displayName: string;
}
/**
* This is a callback for Array.prototype.map
*/
type MapPredicate<
Key,
Obj extends Base,
/**
* Represents the first argument of applyBase function
*/
Source extends Record<string, Base>
> =
/**
* If Key is "self"
*/
Key extends 'self'
/**
* Return obj.displayName
*/
? { displayName: Obj['displayName'] }
/**
* If Key extends keys of the first argument of applyBase
*/
: Key extends keyof Source
? Source[Key] extends Base
/**
* return obj[item].displayName (see js implementation)
*/
? { displayName: Source[Key]['displayName'] }
: never
: never
/**
* Map through a tuple and apply MapPredicate to each element,
* just like it is done in runtime representation
*/
type Mapped<
Arr extends Array<any>,
Obj extends Base,
Source extends Record<string, Base>
> = {
[Key in keyof Arr]: MapPredicate<Arr[Key], Obj, Source>
}
const builder = (obj: { displayName: string }) =>
({ displayName: obj.displayName })
/**
* Simple validation of the last argument (tuple of keys)
* If elements extend either key of the first argument of applyBase function or "self"
* - they are considered as allowed keys, Otherwise - forbidden
*/
type Validation<Obj, Tuple extends unknown[]> = {
[Key in keyof Tuple]: Tuple[Key] extends keyof Obj
? Tuple[Key]
: Tuple[Key] extends 'self'
? Tuple[Key]
: never
}
/**
* The logic is straightforward, we need to infer each provided argument.
*/
function convert<
BaseId extends string,
BaseName extends string,
BaseObj extends { id: BaseId, displayName: BaseName }
>(base: BaseObj): <
NestedId extends string,
NestedName extends string,
Keys extends PropertyKey,
Extension extends Record<Keys, { id: NestedId, displayName: NestedName }>,
Items extends Array<Keys>
>(obj: Extension, items: Validation<Extension, [...Items]>) => Mapped<[...Items], BaseObj, Extension>
function convert<
BaseObj extends { id: string, displayName: string }
>(base: BaseObj) {
return <
Extension extends Record<PropertyKey, Base>,
Items extends Array<PropertyKey>
>(obj: Extension, items: Validation<Extension, [...Items]>) =>
items
.map(item =>
item === 'self'
? builder(base)
: builder(obj[item])
)
}
const applyBase = convert(
{
id: '1234',
displayName: 'Station 1',
})
// const result: [{
// displayName: "Testcity";
// }, {
// displayName: "Station 1";
// }]
const result = applyBase(
{
district: { displayName: 'Test', id: '1' },
city: { displayName: 'Testcity', id: '1' },
}, ['city', 'self']);
Interactive Playground Link
If you're interested in argument inference, feel free to visit my article here