Home > Net >  How to make sure that the specified key of an object extends Array?
How to make sure that the specified key of an object extends Array?

Time:07-30

How do I make sure that the specified keyof a type extends an array?

For example I have this type:

type TestType = {
    arrayKey: Array<number>,
    notArrayKey: number
}

And I have this function that will access the specified key of the type:

const getKey = <T>(object: T, key: keyof T): Array<any> => {
    return object[key]
}

How do I make sure that the only valid input of key are arrayKey as it extends Array and not "notArrayKey" which does not extends Array?

CodePudding user response:

Behemoth's answer will provide good IntelliSense suggestions for the key if you choose to use the parameter order of (object, key).

More simply, if you reverse the parameters, you can use generic constraints to ensure that the object argument conforms to a type having an array value at the key argument provided:

TS Playground

function getValueInObj <
  Key extends string,
  Obj extends Record<Key, readonly any[]>,
>(key: Key, object: Obj): Obj[Key] {
  return object[key];
}

const obj = {
  arrayKey: [1, 2, 3],
  nonArrayKey: '123',
};

getValueInObj('arrayKey', obj); // OK

getValueInObj('nonArrayKey', obj); /*
                             ~~~
Argument of type '{ arrayKey: number[]; nonArrayKey: string; }' is not assignable to parameter of type 'Record<"nonArrayKey", readonly any[]>'.
  Types of property 'nonArrayKey' are incompatible.
    Type 'string' is not assignable to type 'readonly any[]'.(2345) */

Expanding on the other answer:

The type mapping can be performed in the generic constraint in order to provide a type parameter name to use in the return type.

In the example below, when the obj value is provided as the first argument to the function, the generic type Obj becomes the type of that object, and the type parameter Key becomes a union of all of its properties which have value types that extend readonly any[] (which means an immutable array with elements of any type). This union ends up looking like 'arrayKey' | 'anotherArrayKey', so the value provided to key has to be one of those strings, or a compiler error diagnostic will be emitted.

TS Playground

function getValueInObj <
  Obj extends Record<PropertyKey, any>,
  Key extends keyof {
    [
      K in keyof Obj as Obj[K] extends readonly any[]
        ? K
        : never
    ]: unknown; // The "unknown" type used here as the value doesn't matter
  },            // because it goes unused: it could be any type
>(object: Obj, key: Key): Obj[Key] {
  return object[key];
}

const obj = {
  arrayKey: [1, 2, 3],
  anotherArrayKey: ['a', 'b', 'c'],
  nonArrayKey: '123',
};

getValueInObj(obj, 'arrayKey'); // number[]

getValueInObj(obj, 'nonArrayKey'); /*
                   ~~~~~~~~~~~~~
Argument of type '"nonArrayKey"' is not assignable to parameter of type '"arrayKey" | "anotherArrayKey"'.(2345) */

CodePudding user response:

You could filter the keys of TestType by their value with a Mapped Type. Basically this checks whether the value of a key extends Array<number> (T[K] extends Array<number>). An example for both keys of TestType:

  1. Does TestType.arrayKey extend Array<number>? --> Yes, valid key
  2. Does TestType.notArrayKey extend Array<number>? --> No, not a valid key because it has the type number

type TestType = {
  arrayKey: Array<number>;
  notArrayKey: number;
};

const test = {
  arrayKey: [0],
  notArrayKey: 0
};

const getKey = <T extends object>(
  object: T,
  key: {
    [K in keyof T]: T[K] extends Array<number> ? K : never;
  }[keyof T]
): T[keyof T] => {
  return object[key];
};

getKey<TestType>(test, "arrayKey"); // works
getKey<TestType>(test, "notArrayKey");
                     // ~~~~~~~~~~~~ --> Argument of type
// '"notArrayKey"' is not assignable to parameter of type '"arrayKey"'

I'm not sure though how to define the return type of function as Array<number>. Probably it's not possible because it cannot be guaranteed that T actually has a property of type Array<number>. But I'd love to be proved wrong!

  • Related