2

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!

Lwyrn
  • 1,821
  • 1
  • 16
  • 27
  • I suspect this issue is covered by the answers to the questions that [this question](https://stackoverflow.com/questions/72247786/object-with-one-keys-value-dependent-on-another-keys-value) was marked a duplicate of. Hopefully that will be useful! – T.J. Crowder May 21 '22 at 13:16
  • 1
    Does [this approach](https://tsplay.dev/w2a7rW) meet your needs? If not, what am I missing? If so, might I suggest you change your question to make the example simpler? Perhaps like [this](https://tsplay.dev/NBkGxm) so that we're not dealing with react and arrays of things that don't directly relate to the issue? Let me know. – jcalz May 22 '22 at 01:31

1 Answers1

6

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 type T and one if its key types K, and hold both that key and a function that accepts a value whose type matches the object type's property for that key:

interface KeyValFunc<T, K extends keyof T> {
    key: K,
    valFunc: (val: T[K]) => void
}

So for this interface

interface Foo {
    x: number,
    y: string,
    z: boolean
}

You can write a value of type KeyValFunc<Foo, "x"> whose key is of type "x" and whose valFunc is of type (val: number) => void:

const obj: KeyValFunc<Foo, "x"> = { key: "x", valFunc: val => val.toFixed() }; // okay

That's all well and good, but now you want an array of KeyValFunc<T, K> for some given T but where you don't care what the specific K is, and in fact K can be different for each array element. That is, you want a heterogenous array type. Your idea is to write that as KeyValFunc<T, keyof T>[]:

type KeyValFuncArray<T> = KeyValFunc<T, keyof T>[];

But, unfortunately, this doesn't work:

const arr: KeyValFuncArray<Foo> = [
    { key: "x", valFunc: val => val.toFixed() } // error!
    // ---------------------------> ~~~~~~~
    // Property 'toFixed' does not exist on type 'string | number | boolean'.
]

Why doesn't the compiler realize that the x key goes with the number value? Why is val typed as string | number | boolean?


The issue is that KeyValFunc<T, keyof T> is not the element type you want it to be. Let's examine it for Foo:

type Test = KeyValFunc<Foo, keyof Foo>;
/* type Test = KeyValFunc<Foo, keyof Foo> */;

Oh, that's not very informative. Let's define an identity mapped type so we can use it on KeyValFunc to see each property.

type Id<T> = { [K in keyof T]: T[K] };

type Test = Id<KeyValFunc<Foo, keyof Foo>>;
/* type Test = {
     key: keyof Foo;
     valFunc: (val: string | number | boolean) => void;
} */

So that's the problem. By using keyof T for K, each array element can have any property key of Foo (keyof Foo), and the parameter to valFunc can be any property value of Foo (Foo[keyof Foo]). That's not what we want.


Instead of plugging keyof T in as K in KeyValFunc<T, K>, what we really want to do is distribute KeyValFunc<T, K> across unions in K. That is, for each K in the keyof T union, we want to evaluate KeyValFunc<T, K>, and then combine them together in a new union.

Here's one way to write that:

type SomeKeyValueFunc<T> = { [K in keyof T]-?: KeyValFunc<T, K> }[keyof T]

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:

type Test = SomeKeyValueFunc<Foo>;
//type Test = KeyValFunc<Foo, "x"> | KeyValFunc<Foo, "y"> | KeyValFunc<Foo, "z">

Yes, that's better. Now Test is itself a union of three types, each of which is KeyValFunc for a particular key of Foo. And so for KeyValFuncArray<T>, we want to use SomeKeyValueFunc<T> instead of KeyValueFunc<T, keyof T>:

type KeyValFuncArray<T> = SomeKeyValueFunc<T>[];

And suddenly things work as expected:

const arr: KeyValFuncArray<Foo> = [
    { key: "x", valFunc: val => val.toFixed() } // okay
]

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • I'm really sorry to revive this question but you really helped me understand this concept and I'm trying to extend it by allowing the `key` to be a function that can access the keys of the data. The key is easy because I can say: `type KeyFn = (data: TData) => TData[K];` and it works I'm able to access the keys from the data in a function in the `key` property, the problem is that then `valFunc` looses reference to what the value actually is. I get `val implicitly is any type` – Gustavo Sanchez Oct 27 '22 at 17:07
  • I can't really address followup questions in the comment section of answers to old questions. If you have a question and need help, you might want to make your own question post about it. Good luck! – jcalz Oct 27 '22 at 17:35
  • That makes sense! Sorry, I have made a new question with the topic. https://stackoverflow.com/questions/74226876/get-type-of-a-function-return-using-generic-types – Gustavo Sanchez Oct 27 '22 at 18:45