I am trying to make a function generic. But i can't figure out why it says [E] Property 'displayName' does not exist on type 'T[{ [k in keyof T]: T[k] extends Base ? k : never; }[keyof T]]'
.
interface Base {
id: string;
displayName: string;
}
interface Station extends Base {
district: Base;
city: Base;
}
type Item<T> = { [ k in keyof T ]: T[k] extends Base ? k : never }[keyof T] | 'self';
type Result = { displayName: string };
const f = <T extends Base>(items: Item<T>[], obj: T): Result[] => {
return items
.map(item => {
if (item === 'self') {
return { displayName: obj.displayName };
}
return {
displayName: obj[item].displayName, // displayName does not exist
};
});
};
const s: Station = {
id: '1234',
displayName: 'Station 1',
district: { displayName: 'Test', id: '1' },
city: { displayName: 'Testcity', id: '1' },
};
const r = f(['city', 'self'], s); // expected: [ { displayName: 'TestCity' }, { displayName: 'Station 1' } ]
CodePudding user response:
Here you have a solution with explanation in comments
interface Base {
id: string;
displayName: string;
}
/**
* Treat it as a simple callback for Array.prototype.map
*/
type MapPredicate<
Key,
Obj extends Base,
/**
* Represents 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 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 last argument (tuple of keys)
* If elements extends either key of first argument of applyBase function or "self"
* - it is considered as allowed key, 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
}
/**
* Logic is simple, we need to infer literaly 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']);
As you might have noticed, I have splitted second argument into two parts to make it easier for TypeScript to infer them.
If you are interested in argument inference, you can check my enter link description here
CodePudding user response:
You should cast obj[item]
to expected type.
Try:
return {
displayName: (obj[item] as unknown as Base).displayName,
};
CodePudding user response:
The type of T
in function f
refers to Base
. Therefore obj
is of type Base
, and you cannot use any keys to index it other than id
or displayName
.
It seems like what you actually need is for obj
to be a type which describes Station
, rather than Base
:
interface Base {
id: string;
displayName: string;
}
type ExtendedBase<K extends string> = [K] extends keyof [Base] ? never : {
[key in K]: Base
} & Base
type Result = { displayName: string };
const f = <T extends string>(items: (T | 'self')[], obj: ExtendedBase<T>): Result[] => {
return items
.map(item => {
if (item === 'self') {
return { displayName: obj.displayName };
}
return {
displayName: obj[item].displayName, //ok
};
});
};
const s = {
id: '1234',
displayName: 'Station 1',
district: { displayName: 'Test', id: '1' },
city: { displayName: 'Testcity', id: '1' },
};
const r = f(['city', 'self'], s); // Result[]
I have omitted the Station
type simply because it isn't doing any work in this example.