Reputation: 2707
Trying to strongly type a data formatting function. What function does is simple,
Input:
const eleOfArray = {
a: 1,
b: "string",
c: [1, 2, 3]
}
Output:
const eleOfArray = {
a: {
value: 1,
component: 1
},
b: {
value: "string",
component: "string"
},
c: {
value: [1, 2, 3],
component: [1, 2, 3]
}
}
function:
export function tableDataFormatter<T>(d: T) {
const formatedData: ITableData<T> = {}
Object.keys(d).forEach((key: string) => {
// tslint:disable-next-line:ban-ts-ignore
// @ts-ignore
formatedData[key] = {
// tslint:disable-next-line:ban-ts-ignore
// @ts-ignore
value: d[key as keyof T],
component: d[key as keyof T]
}
})
return formatedData
}
Interface:
interface ITableData<T> {
readonly [key: keyof T]: {
readonly component: React.ReactNode
readonly value: T[keyof T]
}
}
The problem I'm having with this code is when I use tableDataFormatter
it shows value
is always string | number
.
Usage:
public formatData<IBenefit> (data: ReadonlyArray<IBenefit>): ReadonlyArray<ITableData<IBenefit>> {
return super.formatData(data).map((d: ITableData<IBenefit>) => ({
...d,
stores: {
...d.stores,
component: <Addon addonValues={d.stores.value} />
// d.stores.value should be an Array but it's being shown as ReactText/string
}
}))
}
So I have to suppress the err
cause the function is working as intended and I'm clearly assigning value
as readonly value: T[keyof T]
Upvotes: 1
Views: 5125
Reputation: 328608
Your ITableData<T>
interface is not valid TypeScript. Specifically, you are trying to use an index signature constrained to keyof T
, but the only allowable index signature types are string
and number
. Instead you should consider using a mapped type instead of an interface. It will give you the mapping you want:
type ITableData<T> = {
readonly [K in keyof T]: {
readonly component: React.ReactNode
readonly value: T[K]
}
}
Note how the nested value
property is of type T[K]
and not T[keyof T]
. T[keyof T]
is going to be the union of all value types of T
and the mapping from each key to each value is lost. But T[K]
means that for each key K
, the nested value
property is of the same type as the original property of T
indexed by K
. This is the way to avoid the string | number
problem.
Then for tableDataFormatter()
I'd change some of the annotations and assertions like follows:
// strip off the readonly modifier for the top level properties
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
// explicitly declare that the function returns ITableData<T>
export function tableDataFormatter<T extends Record<keyof T, React.ReactNode>>(
d: T
): ITableData<T> {
// assert that the return value will be ITableData<T> but make it
// mutable so we can assign to it in the function without error
const formatedData = {} as Mutable<ITableData<T>>;
// assert that Object.keys(d) returns an array of keyof T types
// this is not generally safe but is the same as your "as keyof T"
// assertions and only needs to be done once.
(Object.keys(d) as Array<keyof T>).forEach(key => {
formatedData[key] = {
value: d[key],
component: d[key]
}
})
return formatedData
}
Hopefully that works the way you expect now. Good luck!
Upvotes: 3