damix911
damix911

Reputation: 4453

How can I enforce a constraint between two type parameters in TypeScript?

I have a function that takes a dictionary and an array of strings.

f({ a: "foo", b: "bar" }, ["a", "b"]);

I want to enforce that the values in the array are the keys of the dictionary. I.e, the call above is legitimate, but the two calls below must fail at compile time. The first one because it has an extra key (in the dictionary), the second one because it has an extra value (in the array).

f({ a: "foo", b: "bar", c: "baz" }, ["a", "b"]);
f({ a: "foo", b: "bar" }, ["a", "b", "c"]);

I don't know if it helps, but I was able to write a test that checks that the keys and the values are exactly the same.

type Valid<
    D extends Record<string, string>,
    A extends Array<string>
> = A[number] extends keyof D ? keyof D extends A[number] ? any : never : never;

This seems to work; Valid<D, A> is any when the keys and the values are the same, and is never when there is an extra key or an extra value.

How can I use Valid<D, A> to make the compilation fail? I tried stuff like this:

function f<
    D extends Record<string, string>,
    A extends Array<string> & Valid<D, A>
>(d: D, a: A): void
{
    console.log(d, a);
}

but so far nothing has worked. The definition above for instance causes a compile time error even when the keys and values match, which is of course not what I want.

Update

Thanks to @davidhu I have half of what I need; @davidhu suggested this, which errors in one of the two cases.

function f<
    D extends Record<string, string>,
    A extends Array<keyof D> 
>(d: D, a: A): void
{
    console.log(d, a);
}

// Errors because of the "c" in the array.
f({ a: "foo", b: "bar" }, ["a", "b", "c"]);

To cover the other half of it, I came up with this.

export type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;

function f<A extends readonly string[]>(d: UnionToIntersection<{ [K in A[number]]: string }>, a: A): void
{
    console.log(d, a);
}

// Errors because of the `c: "baz"` in the dictionary.
f({ a: "foo", b: "bar", c: "baz" }, ["a", "b"] as const);

Hopefully there is a way to combine the two things together.

Upvotes: 0

Views: 40

Answers (1)

davidhu
davidhu

Reputation: 10472

function f<
    D extends Record<string, string>,
    A extends Array<keyof D> 
>(d: D, a: A): void
{
    console.log(d, a);
}

f({ a: "foo", b: "bar", c: "baz" }, ["a", "b"]);
f({ a: "foo", b: "bar" }, ["a", "b", "c"]);

You just need to say A is an array of keys of D and it shows a compile error

Ts playground

Upvotes: 1

Related Questions