Reputation: 141462
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
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