I'm wondering if this would be something possible to create a Type-safe find()
on a const array :
const MY_ARRAY = [
{ id: "a", name: "AAA" },
{ id: "b", name: "BBB" },
{ id: "c", name: "CCC" },
] as const;
type MyArrayId = (typeof MY_ARRAY[number])["id"]
function find<ID extends MyArrayId>(id: ID): MyTypesafeArrayFind<ID> {
return MY_ARRAY.find(entry => entry.id === id);
}
// TODO: here lies the problem :)
type MyTypesafeArrayFind<ID extends MyArrayId> = unknown
const typesafeResult = find("a");
// I'd expect `typeof typesafeResult` to be { id: "a", name: "AAA" }
// Note: I am *not* interested into something like:
// { id: "a"|"b"|"c", name: "AAA"|"BBB"|"CCC" }
see this playground
Do you think this would be something doable for const arrays ?
As a precision, and even if this is not type-safely expressed in my example, the id
field in my array will be guaranteed to be unique (we can consider that first entry that matches search criteria will match)
Thanks in advance for anyone involved.
CodePudding user response:
Here you have easy to understand solution:
const MY_ARRAY = [
{ id: "a", name: "AAA" },
{ id: "b", name: "BBB" },
{ id: "c", name: "CCC" },
] as const;
type MyArray = typeof MY_ARRAY
type MyArrayId = MyArray[number]["id"]
function find<ID extends MyArrayId>(id: ID): MyTypesafeArrayFind<ID>
function find<ID extends MyArrayId>(id: ID) {
return MY_ARRAY.find(entry => entry.id === id);
}
type MyTypesafeArrayFind<Id extends MyArrayId> = Extract<MyArray[number], { id: Id }>
// const typesafeResult: {
// readonly id: "a";
// readonly name: "AAA";
// }
const typesafeResult = find("a")
const fn = (str: string) => {
find(str) // error
}
However it has it's own cons. You are not allowed to use find
inside higher order function. You are allowed to use only existing 'ids'.
If you want to make it super safe, and make your co-workers angry, you can consider this solution:
const MY_ARRAY = [
{ id: "a", name: "AAA" },
{ id: "b", name: "BBB" },
{ id: "c", name: "CCC" },
] as const;
type MyArray = typeof MY_ARRAY
type MyArrayId = MyArray[number]["id"]
/**
* Returns true if provided generic is literal string type
*/
type IsLiteralString<Str extends string> = string extends Str ? false : true
const withTuple = <
Elem extends { id: string },
List extends Elem[]
>(tuple: readonly [...List]) => {
function curry<Id extends List[number]['id']>(id: Id): Extract<List[number], { id: Id }>
function curry<Id extends string>(id: IsLiteralString<Id> extends true ? Id extends List[number]['id'] ? Id : never : Id): List[number] | undefined
function curry(id: string) {
return tuple.find(entry => entry.id === id);
}
return curry
}
const find = withTuple(MY_ARRAY)
// {
// readonly id: "a";
// readonly name: "AAA";
// }
const typesafeResult = find("a")
let id = 'any id'
const typesafeResult2 = find(id,) // Element | undefined, expected and safe
const typesafeResult23 = find('aa',) // expected error, it is useles to use id which is not exists in our list
IsLiteralString
- checks whether provided generic is a literal string type or it is just string
, non infered type.
withTuple
- expects your list for inference and returns desired function.
curry
- function which is declared inside of withTuple
is function with overloadings.
First function overload function curry<Id extends List[number]['id']>(id: Id): Extract<List[number], { id: Id }>
infers id
as an existing id
in our array and infers return type.
Second function overload
function curry<Id extends string>(id: IsLiteralString<Id> extends true ? Id extends List[number]['id'] ? Id : never : Id): List[number] | undefined
Allows you to provide non ifered regular string
type for higher order function. Also provided id
is literal and it does not exists in your array, TS will forbid you to use it. I call it "typescript negation". You can find more information about this approach in my article and in this answer