Home > Enterprise >  Typescript - Guard against intersection elements
Typescript - Guard against intersection elements

Time:06-05

How can you make typescript warn on an element level if that array element is present in another array?

For example:

const canBeAorB = <A1 extends ("a" | "b")[]>(arr: A1) => arr

const arr0 = canBeAorB(["b", "c"]) // Type '"c"' is not assignable to type '"a" | "b"'.

This is what I want to achieve for TS to warn me on a element that it is invalid

But instead of a value I need to guard against all elements of other array, for example:

const blockedElements = ["a", "b"]

const myArr = ["b", "c"] // make TS yell that "b" is invalid because its a member of blockedElements 

I managed to get really close by doing this:

const blockedValues = ["a", "b"] as const;

type BlockedElement = typeof blockedValues[number];

const cannotHaveBlockedEls = <A1 extends Readonly<Exclude<A1[number], BlockedElement>[]>>(arr: A1) => arr

const arr3 = cannotHaveBlockedEls(["b", "c"] as const)

Which will yield:

Argument of type 'readonly ["b", "c"]' is not assignable to parameter of type 'readonly "c"[]'.
  Type '"b" | "c"' is not assignable to type '"c"'.
    Type '"b"' is not assignable to type '"c"'

But the error is displayed on top of the whole array argument instead of on top of "b", my real use case is a larger array where this would really make a difference.

Is it possible to do?

CodePudding user response:

The situation you're running into is that the const assertion in ["b", "c"] as const effectively makes the whole expression opaque to the cannotHaveBlockedEls() function you're passing it into. It's as if you wrote this:

const oops = ["b", "c"] as const;
const arr3 = cannotHaveBlockedEls(oops);
//                                ~~~~ <-- error

Presumably the only reason you wrote as const there in the first place is to force the compiler to see ["b", "c"] as an array of string literal types instead of just string[].

If so, there are other ways to do that. Here's one:

const cannotHaveBlockedEls = <T extends string>(arr: Exclude<T, BlockedElement>[]) => arr

The generic type parameter T is constrained to string, which gives the compiler a hint that you'd like it to be inferred as a string literal type (or a union of string literal types). Since T is now a string, we need arr to be of an array type whose element type is Exclude<T, BlockedElement>.

Of course this means that arr must be an array of strings or string literals. If you needed to accept other types, you'd have to widen the constraint on T.

Anyway, let's see it in action:

const arr3 = cannotHaveBlockedEls(["b", "c"]); // error!
// ------------------------------> ~~~
// Type '"b"' is not assignable to type '"c"'.

Looks good!

  • Related