Typescript is beautiful, and I love to write on typescript cause I can just write
enum Test {
some = 'some',
another = 'another',
}
const test: Record<Test, string> = {
some: 'string'
}
then typescript asks me to type every key value from Test
as the key of my test object
Property 'another' is missing in type '{ some: string; }' but required in type 'Record<Test, string>'
But if I want to write an opposite operation (like parse/stringify), I want to be sure that I don't miss some Test as values of Record
enum Test {
some = 'some',
another = 'another',
}
const test: Record<string, Test> = {
some: Test.some
}
But typescript assume that it is correct.
I would like to throw some error by typescript.
Is there any way in typescript to check every Record value of Enum?
TypeScript doesn't really have the idea of "exhaustive property types", but you could write a generic type along with a helper function to validate that a given value behaves as you like. For example:
const exhaustive = <V,>() => <T extends Record<keyof T, V>>(
t: { [K in keyof T]: [V] extends [T[keyof T]] ? T[K] : Exclude<V, T[keyof T]> }
) => t;
This is a curried function where you manually specify the value type V
you'd like to "exhaust", and it outputs another generic function that does the check:
enum Test {
some = 'some',
another = 'another',
}
const exhaustiveTest = exhaustive<Test>()
(The reason you need to use two functions like that is because TypeScript doesn't let you manually specify a generic type argument while inferring another one; there is an open feature request for this at microsoft/TypeScript#26242 and the various workarounds including currying are discussed in Typescript: infer type of generic after optional first generic)
When you call exhaustiveTest()
it will return its input, but a compiler error will occur if you are missing any of the values in Test
:
const test = exhaustiveTest({
abc: Test.some,
def: Test.another
}); // okay
const badTest = exhaustiveTest({
abc: Test.some // error! Type 'Test.some' is not assignable to type 'Test.another'
})
Hooray, that's what you wanted!
The way it works is that the call to exhaustiveTest()
infers the type argument T
to be the type passed in as the function argument t
, but it checks it against the mapped type { [K in keyof T]: [Test] extends [T[keyof T]] ? T[K] : Exclude<Test, T[keyof T]> }
. Each property is the conditional type [Test] extends [T[keyof T]] ? T[K] : Exclude<Test, T[keyof T]>
. That means: if every value of type Test
can be assigned to one of the property types of T
, then each property of T
is just fine as itself (the indexed access type T[K]
); otherwise, it should be Exclude<Test, T[keyof T]>
using the Exclude<T, U>
utility type to filter out all the properties of T
from Test
to get just the missed values, so that the error message will mention these missing values.
As a concrete case, if T
is {abc: Test.some; def: Test.another}
, then T[keyof T]
is Test.some | Test.another
, which is the same as Test
, and so the mapped type is just {abc: Test.some; def: Test.another}
, and it type checks. But if T
is just {abc: Test.some}
, then T[keyof T]
is just Test.some
, which is missing thing from Test
. And so the mapped type is {abc: Test.another}
, and it fails to type check, complaining about the missing thing.