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:
/** 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