Home > front end >  Returning the strict item-type of an array-property by a given key and an object
Returning the strict item-type of an array-property by a given key and an object

Time:12-29

I am trying to provide a helper/wrapper function (get_item), which should return a single item of an objects property, but somehow I am unable to get the correct typings done.

// without typings
function get_item(foo, key, index) {
    const array = foo[key];
    // check if the index exists
    if (index in array === false) throw new Error("Item "   index   " not found for "   key);
    return array[index];
}
// usage example 
const item = get_item({bar:[0,1,2]}, 'bar', 0);

My attempts seem to fail, as soon as I try to infer the ArrayItemType of array.

Simply returning the type of array by the given key is no problem, as is shown here.

But on top of that, I know, that I will only have properties of type Array in my foo-object and want to return the type of a single item in the array I selected via the key, but that fails, as typescript seems to forget its relation to the key property when accessing the array with the index operator.

Typescript 4.5.4 Playground (same code as below)

/**
 * I want to get the item at position 'index' 
 * from the properties with name 'key'
 * of the structure 'Foo'
 */
function get_item<
    KEY extends keyof Foo,  // "zoo" | "bar"
>(foo: Foo, key: KEY, index:number)
    : ArrayItemType<Foo[KEY]> // determines the type of a single element for the property 'key' beeing number|string
{
    const array: Foo[KEY] = foo[key];   // this can be Array<number> or Array<string> depending on 'key'
    // check if the index exists
    if (index in array === false) throw new Error("Item "   index   " not found for "   key);
    const item = array[index];          // at this point the type seems to have collapsed to number|string,
                                        // discarding the fact that we know the exact type of 'array' for a given 'key'
    return item;    // Error
                    // Type 'string | number' is not assignable to type 'ArrayItemType<Foo[KEY]>'. 
                    // Type 'string' is not assignable to type 'ArrayItemType<Foo[KEY]>'.
}

type ArrayItemType<ARRAY> = ARRAY extends Array<infer ITEM> ? ITEM : never;

/**
 * Some interface with a few properties of type Array<any>
 */
interface Foo {
    bar : Array<number>;
    zoo : Array<string>;
}

const foo : Foo = {
    bar: [0,1],
    zoo: ["a", "b"],
};
// determines correct types here, this is the way i want it
const number_item :number = get_item(foo, 'bar', 1);
const string_item :string = get_item(foo, 'zoo', 1);

Sure adding as ArrayItemType<Foo[KEY]> does solve the issue, but I would like to keep those type castings to a minimum especially when I don't understand why it should not work (which makes me think, that I misinterpret my code somewhere).

So, am I missing something?
Should this somehow not be possible?
Or is the problem with KEY extends keyof Foo again, allowing more values than I am aware?

CodePudding user response:

You can do it this way:

TS Playground

/** Throws if value is undefined */
function getItem <
  K extends PropertyKey,
  T extends Record<K, readonly unknown[]>,
  I extends number,
>(obj: T, key: K, index: I): T[K][I] extends undefined ? never : T[K][I] {
  const arr = obj[key];
  if (!Array.isArray(arr)) throw new Error(`Property "${key}" not found`);
  const value = arr[index];
  if (typeof value === 'undefined') throw new Error(`Value not found at index ${index}`);
  return value;
}

getItem({bar: [2, 4, 6]}, 'bar', 0); // number
getItem({bar: [2, 4, 6] as const}, 'bar', 0); // 2

interface Foo {
  bar: number[];
  zoo: string[];
}

const foo: Foo = {
  bar: [0, 1],
  zoo: ['a', 'b'],
};

getItem(foo, 'bar', 1); // number
getItem(foo, 'zoo', 1); // string
getItem({hello: ['world'] as const}, 'hello', 1); // never, will throw

However, beware of the nature of index signatures and arrays in TypeScript:

getItem(foo, 'zoo', 2); // string but actually never because it will throw
  • Related