0

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.

1 Answer 1

1
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

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you davidhu, good idea, this is pretty close; it does trigger a compile error for f({ a: "foo", b: "bar" }, ["a", "b", "c"]); but ideally it needs to also error on f({ a: "foo", b: "bar", c: "baz" }, ["a", "b"]);. I am trying to extend your solution to make that happen.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.