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!