Reputation: 91
I spent quite some time on this and I would appreciate some help. I want a component (a function) to accept an array of objects while also validating the properties of the objects.
Interfaces and data:
interface ObjectWithId {
id: string | number;
}
interface TableMeta<T extends ObjectWithId, K extends PropertyKey = keyof T> {
data: T[];
searchKey: K;
onClick?: (item: T) => void;
}
interface Vegetable {
id: number,
label: string,
}
interface Fruit {
id: number,
name: string,
}
const vegetableMeta: TableMeta<Vegetable> = {
data: [],
searchKey: 'label', // this only allows 'label' or 'id' 👍
}
const fruitMeta: TableMeta<Fruit> = {
data: [],
searchKey: 'name', // this only allows 'name' or 'id' 👍
onClick: (item) => {item.id} // ✔️ has correct item type <---------------
}
const metas = [vegetableMeta, fruitMeta];
A component (a function for simplicity):
const metaParser = (metas: TableMeta<{id: number | string}, PropertyKey>[]) => {
const id = metas[0].data[0].id; // should be `number | string`
}
metaParser(metas); // ❌ Type 'ObjectWithId' is not assignable to type 'Vegetable'
The shape of objects in the array is unknown in advance
Any ideas on how to make it work?
Upvotes: 4
Views: 7954
Reputation: 91
One way to make it work is to expose the type of the callback and make it more generic when dealing with a collection of objects.
interface ObjectWithId {
id: string | number;
}
interface TableMeta<
T extends ObjectWithId,
K extends PropertyKey = keyof T,
F extends (item: never) => void = (item: T) => void, // <--- expose the type
> {
data: T[];
searchKey: K;
onClick?: F;
}
interface Vegetable {
id: number,
label: string,
}
interface Fruit {
id: number,
name: string,
}
const vegetableMeta: TableMeta<Vegetable> = {
data: [],
searchKey: 'label',
}
const fruitMeta: TableMeta<Fruit> = {
data: [],
searchKey: 'name',
onClick: (item) => {item.id}
}
const metas = [vegetableMeta, fruitMeta];
make it more generic ---⌄
const metaParser = (metas: TableMeta<ObjectWithId, PropertyKey, (item: never) => void>[]) => {
const id = metas[0].data[0].id;
}
metaParser(metas);
Upvotes: 1
Reputation: 33091
THIRD UPDATE
type Base = { id: number | string } & { [p: string]: unknown }
interface Data<T extends Base> {
data: Array<T>;
}
interface SearchKey<T extends Base> {
searchKey: Exclude<keyof T, 'id'>;
}
type TableMeta<T extends Base> = Data<T> & SearchKey<T>
/**
* AFAIK, this is the required overhed to be able correctly infer the types
*/
const builder = <T extends Base, K extends Exclude<keyof T, 'id'>>(searchKey: K, data: Array<T>): TableMeta<T> => ({ data, searchKey });
const metas = [
builder('name', [{ id: 2, name: 2 }]),
builder('key', [{ id: '2', key: 'John Doe' }]),
// builder('hello', [{ id: 3, age: 42 }]) //expected error
];
const metaParser = <T extends TableMeta<Base>>(metas: Array<T>) => {
const result = metas // T[]
const result2 = metas[0].searchKey// string | number
const result3 = metas[0].data[0].id// string | number
return metas
}
const result = metaParser(metas); // ok
Here you can find more info about typing callbacks. I'm not sure if it possible to implement this without helper builder function.
You can consider this answer, but still you have to use extra function, even two
Upvotes: 0