Home > Net >  Type check two dimensional vector values based on enum method arguments
Type check two dimensional vector values based on enum method arguments

Time:11-02

I have two enums and a key-value store. The key for the first level of the store is Enum1 and the key for the second level is Enum2 as seen below.

enum A { a, b }
enum B { c, d }

const MODEL: { [key in A]?: { [key in B]?: string } = {
  [A.a]: {
    [B.c]: "test"
  }
}

I want to create a method type signature that validates whether the combination of enums is valid.

I assume this would look something like below:

const myMethod = <E1 extends A, E2 extends B>(
  arg1: E1,
  arg2: E2,
  value: (typeof MODEL)[E1][E2] // This is where I have issues
// Error message: Type 'B' cannot be used to index type <big object type redacted>
)

The example above has been heavily simplified. The specific use case above may seem redundant/unrealistic but the core logic is what I need to clarify.

As a result, based on the above supplied MODEL variable, the following would be the expected outcomes:

myMethod(A.a, B.c, "test") // no error, as it exists in model
myMethod(A.a, B.d, "test") // error, combination does not exist in model
myMethod(A.a, B.c, "result") // error, value not exist in model

CodePudding user response:

The main problem here is that you annotated the type of MODEL to be a type whose properties and subproperties are all optional (that's what the ? mapping modifier does, after all).

It's essentially the same as

const MODEL: Partial<Record<A, Partial<Record<B, string>>>> = ...

So if you were to just start deeply indexing into it, the compiler will warn you that you might be dereferencing undefined:

MODEL[A.a][B.c] // error!
//~~~~~~~~ <-- Object is possibly 'undefined'

Note that the compiler has no way to know whether or not MODEL[A.a] is defined, because you annotated the type as one where the property may or may not be present. You remember what you put in there, but the compiler doesn't. If you want it to remember, don't annotate the type, you should let the compiler infer it.

And if you want the compiler to know the literal type of the strings (so it can distinguish "test" from "result"), then you should tell it to pay attention to this via a const assertion:

const MODEL = {
  [A.a]: {
    [B.c]: "test"
  }
} as const;

The inferred type of MODEL is now

/* const MODEL: {
  readonly 0: {
      readonly 0: "test";
  };
} */

Great, so now we need to give myMethod() a generic call signature that depends on what keys are actually at each level of the type of MODEL:

const myMethod = <
  E1 extends keyof typeof MODEL,
  E2 extends keyof typeof MODEL[E1]
>(
  arg1: E1,
  arg2: E2,
  value: (typeof MODEL)[E1][E2]
) => { }

That compiles with no error. And lets' call it:

myMethod(A.a, B.c, "test") // okay
myMethod(A.a, B.d, "test") // error
// ---------> ~~~ 
// Argument of type 'B.d' is not assignable to parameter of type 'B.c'
myMethod(A.a, B.c, "result") // error
// --------------> ~~~~~~~~
// Argument of type '"result"' is not assignable to parameter of type '"test"'

Looks good!

Playground link to code

  • Related