2

I would like to define a type where an Array can include one of a list of specific strings, for example:

const foo = "foo";
const bar = "bar';
const baz = "baz':

// acceptable
[foo, bar]

// acceptable 
[foo, bar, baz]

// unacceptable
[foo, foo]

// unacceptable
[foo, bar, bar]

How do I do this?

jumbopap
  • 3,969
  • 5
  • 27
  • 47

1 Answers1

4

I absolutely love this kind of question, even when the answer is of the form "you can do backflips and get close to a solution but it might not be worth it". Which this answer is. Let's do some backflips!

First, try to represent a constraint NoRepeats<T> where if T has all distinct property types, then the output will be T, but if even two keys have the same property type, the output will be never:

type NoRepeats<T> = true extends (
  (keyof T) extends (infer K) ? K extends any ? (
    T[K] extends T[Exclude<keyof T, K>] ? true : never
  ) : never : never
) ? "No Repeats Please" : T

It uses distributive conditional types to pull keyof T apart into individual keys K, and then compares T[K] with T[Exclude<keyof T, K>] where Exclude<keyof T, K> means "all the other keys except K". If they match, then the whole thing will be "No Repeats Please" (kind of a poor-man's error message in lieu of invalid types). If none of them match, then the answer is T.

Let's see how it behaves:

interface X {a: string, b: number, c: boolean}; // no repeats
declare const x: NoRepeats<X>; // x is of type X

interface Y {a: string, b: number, c: string}; // a and c are string
declare const y: NoRepeats<Y>; // y is of type "No Repeats Please"

Now, if you're passing in array types, you don't want to inspect every possible property for duplicates, since arrays have methods like forEach and map and properties like length. And furthermore you are really trying to check only tuples, not general arrays. That's because all the compiler knows about an array is the union of all its element types, and it has no way of determining if there are duplicates. So you are only inspecting the numeric-like keys of a tuple ("0", "1", "2", up to but not including the length of the tuple).

So we need to pull all the keys off that are common to all array types... which I will call "stripping" the tuple:

type StripTuple<T extends any[]> = Pick<T, Exclude<keyof T, keyof any[]>>

And make sure that we only check tuples and not generic arrays:

type NoRepeatTuple<T extends any[]> = 
  number extends T['length'] ? "Must Be Tuple" : NoRepeats<StripTuple<T>>

So, let's try this on tuples and arrays:

declare const z: NoRepeatTuple<[string, number, boolean]> // {0: string, 1: number, 2:boolean}
declare const a: NoRepeatTuple<[string, number, string]> // "No Repeats Please"
declare const b: NoRepeatTuple<string[]> // "Must Be Tuple"

So we're getting closer. Now there is no way to declare that a type T is a NoRepeatTuple<T> because that is a circular constraint. But you can do a similar trick as one you can use to approximate exact types, by making a function that will only accept tuples without repeats:

function requireNoRepeats<V extends "foo" | "bar" | "baz", T extends Array<V>>(
  t: T & NoRepeatTuple<T>
) { }

The parameter t being typed as T & NoRepeatTuple<T> will cause the compiler to infer the type of t passed in as T, and then check it with NoRepeatTuple<T>. Let's see if we can try it:

const foo = "foo";
const bar = "bar";
const baz = "baz";
requireNoRepeats([foo, bar]) // error, must be tuple

Whoops! Oh, the problem is that the value [foo, bar] is inferred to be an array and not a tuple. That is an issue in TypeScript and I don't think it will be immediately fixed.

So we have to convince the compiler to interpret something as a tuple. One way I've used before is to write a function that infers tuple types. When TS3.0 lands shortly you can write it succinctly using tuple rest/spread as

export type Lit = string | number | boolean | undefined | null | void | {};
export const tuple = <T extends Lit[]>(...args: T) => args;

Until then you can user a more verbose function.

Okay finally, let's try it:

requireNoRepeats(tuple(foo, bar)) // okay
requireNoRepeats(tuple(foo, bar, baz)) // okay
requireNoRepeats(tuple(foo, bar, bar)) // error, no repeats please
requireNoRepeats(tuple(foo, foo)) // error, no repeats please

So, that all works. Yay! Yay? As you can see, there are lots of caveats and type juggling there. Not something I'd feel particularly great about using in anyone's code but my own, at least not without plenty of testing.

Anyway, hope that gives you some ideas. Good luck!

jcalz
  • 264,269
  • 27
  • 359
  • 360