I want to define an interface with generic type that have to accept an object having its keys as "root fields name" and the value as an array of objects that defines some sub-fields having the key as the name of the sub-field and the type as the type of the field value. Something like this:
interface Inputs{
emails: { email: string, active: boolean, i: number }[]
}
const obj:Inputs = {emails: [ {email: "...", active: true, i: 100} ]}
The interface who receive this as a generic type have a "name" property that will receive the (keyof) name of the sub-field ( ex. active ) and a function with a parameter that have to receive the type of the sub-field defined in the name property.
Something like this
[
{
name: "active",
component: ({ value, values }) => {
console.log(value, values);
return <>Component</>;
}
}
]
In this example, value must have "boolean" as only accepted type since the active key in the object has a boolean type.
I managed to get almost everything I wanted to do. The only problem is that instead of receiving the exact type of the subfield, the parameter of the function gets a union of all types of the object.
So in the previous example, since "email" is a string type, value should be string type, instead the receiving type is string | number | boolean ( all the available types in the object ).
I don't know if I have been able to explain myself well, but I have prepared a sandbox to do it better
https://codesandbox.io/s/boring-fast-pmmhxx?file=/src/App.tsx
interface Options<
T extends { [key: string]: unknown }[],
Key extends keyof T[number]
> {
values: T;
value: Key;
}
interface InputDef<
T extends { [key: string]: any }[],
Key extends keyof T[number]
> {
name: Key;
component: (props: Options<T, T[number][Key]>) => React.ReactNode;
}
interface Props<T extends { [key: string]: [] }, Key extends keyof T> {
name: Key;
inputs: InputDef<T[Key], keyof T[Key][number]>[];
callback: (values: T) => void;
}
interface Inputs {
firstName: string;
lastName: string;
emails: { email: string; active: boolean; other: number }[];
}
const GenComponent = <T extends { [key: string]: any }, Key extends keyof T>({
name,
inputs
}: Props<T, Key>) => {
console.log(inputs);
return (
<div>
{name} {JSON.stringify(inputs)}
</div>
);
};
interface MainComponentProps {
callback: TestCallback<Inputs>;
}
const MainComponent: React.FC<MainComponentProps> = ({ callback }) => {
return (
<>
<GenComponent
callback={callback}
name="emails"
inputs={[
{
name: "active",
component: ({ value, values }) => {
console.log(value, values);
return <>Component</>;
}
}
]}
/>
</>
);
};
type TestCallback<Data> = (values: Data) => void;
function test<Data>(values: Data): void {
console.log(values);
}
export default function App() {
return (
<div className="App">
<MainComponent callback={test} />
</div>
);
}
On line 57, since the name in the object is "active" the type of value should be "boolean" instead of "string | number | boolean". How can I achieve this?
Thanks!
I'm going to simplify your example to show where the problem is and how to fix it. First, you have a generic
KeyValFunc<T, K>type that takes an object typeTand one if its key typesK, and hold both that key and a function that accepts a value whose type matches the object type's property for that key:So for this interface
You can write a value of type
KeyValFunc<Foo, "x">whosekeyis of type"x"and whosevalFuncis of type(val: number) => void:That's all well and good, but now you want an array of
KeyValFunc<T, K>for some givenTbut where you don't care what the specificKis, and in factKcan be different for each array element. That is, you want a heterogenous array type. Your idea is to write that asKeyValFunc<T, keyof T>[]:But, unfortunately, this doesn't work:
Why doesn't the compiler realize that the
xkey goes with thenumbervalue? Why isvaltyped asstring | number | boolean?The issue is that
KeyValFunc<T, keyof T>is not the element type you want it to be. Let's examine it forFoo:Oh, that's not very informative. Let's define an identity mapped type so we can use it on
KeyValFuncto see each property.So that's the problem. By using
keyof TforK, each array element can have any property key ofFoo(keyof Foo), and the parameter tovalFunccan be any property value ofFoo(Foo[keyof Foo]). That's not what we want.Instead of plugging
keyof Tin asKinKeyValFunc<T, K>, what we really want to do is distributeKeyValFunc<T, K>across unions inK. That is, for eachKin thekeyof Tunion, we want to evaluateKeyValFunc<T, K>, and then combine them together in a new union.Here's one way to write that:
This is a mapped type which is immediately indexed into with all its keys, to produce the union property we want. Let's verify that it does what we want for
Foo:Yes, that's better. Now
Testis itself a union of three types, each of which isKeyValFuncfor a particular key ofFoo. And so forKeyValFuncArray<T>, we want to useSomeKeyValueFunc<T>instead ofKeyValueFunc<T, keyof T>:And suddenly things work as expected:
Playground link to code