Home > other >  Typescript type that checks if a given key if passed to as a key to an object resolves to an array
Typescript type that checks if a given key if passed to as a key to an object resolves to an array

Time:04-13


This is kind of a theoretical question but I was wondering if we can build an input type that checks if a given enum key if passed to as a key to an object resolves to an array. I guess this is better explained with an example:
enum FormKeys {
  a = "a",
  b = "b",
}

interface IInitalFormValues {
  [FormKeys.a]: number
  [FormKeys.b]: string[]
}

const initialFormValues: IInitalFormValues = {
  [FormKeys.a]: 123,
  [FormKeys.b]: ["a"],
}

function onChange(input: FormKeys) {
  /*
    Error here since if we pass FormKeys.a then assigning an array to
    type number is an error
  */
  initialFormValues[input] = []
}

onChange(FormKeys.b)

What we can obviously do here is to change input: FormKeys to input: FormKeys.b but this solution does not scale well, is there a way to do it more generically?
Thanks

CodePudding user response:

You can pick out the keys that are for array types by doing this (I picked this up here):

type ArrayKeys<T> = {
    [Key in keyof T]: T[Key] extends any[] ? Key : never;
}[keyof T];

(That's a bit tricky, full explanation below.) ArrayKeys<IInitialFormValues> would be just FormKeys.b, for instance, since that's the only property with an array type. If you had more, ArrayKeys<IInitialFormValues> would be a union of them.

Then your input parameter's type is ArrayKeys<IInitialFormValues>:

function onChange(input: ArrayKeys<IInitialFormValues>) {
    initialFormValues[input] = [];
}

That way, this works:

onChange(FormKeys.b);   // Works as desired

but this fails:

onChange(FormKeys.a);   // Fails as desired

Playground example


How that ArrayKeys works:

type ArrayKeys<T> = {
    [Key in keyof T]: T[Key] extends any[] ? Key : never;
}[keyof T];

There are two stages to that:

  1. The {/*...*/} part maps each property of T into a new mapped type where the type of the property is the key itself or never. Suppose we had:

    type Example = {
        a: string[];
        b: number[];
        c: string;
    };
    

    Just the first part of ArrayKeys creates this anonymous type:

    {
        a: "a";
        b: "b";
        c: never;
    }
    
  2. Then the [keyof T] part at the end creates a union of the types of those properties, which in theory would be "a" | "b" | never, but never is always dropped from union types, so we end up with "a" | "b" — the keys of Example for properties with array types.

You can go a step further and build a type consisting of only the parts of Example that match that by using Pick<Example, ArrayKeys<Example>>, but you don't need that for this specific purpose.

  • Related