Home > other >  Typesafe find on const array in Typescript
Typesafe find on const array in Typescript

Time:10-07

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

Playground

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

  • Related