Shaun Luttin
Shaun Luttin

Reputation: 141462

Define an array of objects with a distinct object key

How, if at all, can TypeScript define an array that constrains a specified object key to be distinct?

Here is a demo that indicates the requirements.

type Magician = {
  distinctKey: string;
  lastName: string;
};

// How, if at all, can we make line 19 be a compiler error?
type ObjectArrayWithDistinctKey<TItem, TKey> = TItem[]; 

const example: ObjectArrayWithDistinctKey<Magician, 'distinctKey'> = [
  {
    distinctKey: 'ammar1956',
    lastName: 'Ammar'
  },
  {
    distinctKey: 'vernon1894',
    lastName: 'Vernon'
  },
  {
    distinctKey: 'ammar1956', // error!
    lastName: 'Ammar'
  }
];

I appreciate that this is a job for something like a Map not an array.

Upvotes: 0

Views: 52

Answers (1)

Shanon Jackson
Shanon Jackson

Reputation: 6531

No this is a behavior that isn't possible in Arrays, the reason is that Arrays can only be of a single type "A", A's cannot compare themselves against other A's.

This is a behavior that can however be achieved with tuple, or variable length tuples.

EDIT: There are many ways to achieve this with tuples, i would say there is no best way it really depends on your the whole situation, that's why i didn't add a solution however this is atleast one way is to turn a Union MagicianA | MagicianB into permutations of tuples [MagicianA | MagicianB] | [MagicianB, MagicianA] this allows the ordering to not matter but to force distinct keys and length.

interface Magician<DistinctKey extends "ammar1956" | "vernon1894"> {
  distinctKey: DistinctKey;
  lastName: string;
};
type AmmarMagician = Magician<"ammar1956">;
type VernonMagician = Magician<"vernon1894">;

/* Easiest quality of life solution */
type Overwrite<T, S extends any> = { [P in keyof T]: S[P] };
type TupleUnshift<T extends any[], X> = T extends any ? ((x: X, ...t: T) => void) extends (...t: infer R) => void ? R : never : never;
type TuplePush<T extends any[], X> = T extends any ? Overwrite<TupleUnshift<T, any>, T & { [x: string]: X }> : never;

type UnionToTuple<U> = UnionToTupleRecursively<[], U>;

type UnionToTupleRecursively<T extends any[], U> = {
    1: T;
    0: UnionToTupleRecursively_<T, U, U>;
}[[U] extends [never] ? 1 : 0]

type UnionToTupleRecursively_<T extends any[], U, S> =
    S extends any ? UnionToTupleRecursively<TupleUnshift<T, S> | TuplePush<T, S>, Exclude<U, S>> : never;
/* End Easiest qulaity of life solution */


const example: UnionToTuple<AmmarMagician | VernonMagician> = [
  {
    distinctKey: 'ammar1956',
    lastName: 'Ammar'
  },
  {
    distinctKey: 'vernon1894',
    lastName: 'Vernon'
  },
]; // works


const example2: UnionToTuple<AmmarMagician | VernonMagician> = [
  {
    distinctKey: 'vernon1894',
    lastName: 'Vernon'
  },
    {
    distinctKey: 'ammar1956',
    lastName: 'Ammar'
  },
]; // works swapped



const example3: UnionToTuple<AmmarMagician | VernonMagician> = [
  {
    distinctKey: 'vernon1894',
    lastName: 'Vernon'
  },
    {
    distinctKey: 'ammar1956',
    lastName: 'Ammar'
  },
    {
    distinctKey: 'ammar1956',
    lastName: 'Ammar'
  },
]; // fails duplicates.

Or you can also do something like this for variable length....

type ValidKeys = "ammar1956" | "vernon1894" | "UnknownMagician"
interface Magician<DistinctKey extends ValidKeys> {
  distinctKey: DistinctKey;
  lastName: string;
};
type AmmarMagician = Magician<"ammar1956">;
type VernonMagician = Magician<"vernon1894">;
type UnknownMagician = Magician<"UnknownMagician">;

/* Easiest quality of life solution */
type MagiciansArray = [AmmarMagician, VernonMagician, ...UnknownMagician[]];
/* End Easiest qulaity of life solution */
const example: MagiciansArray = [
  {
    distinctKey: 'ammar1956',
    lastName: 'Ammar'
  },
  {
    distinctKey: 'vernon1894',
    lastName: 'Vernon'
  },
  {
      distinctKey: "UnknownMagician",
      lastName: "hello"
  }
]; // works

Upvotes: 1

Related Questions