简体   繁体   中英

Can typescript check every Record value of Enum?

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]> } { [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]> [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} {abc: Test.some; def: Test.another} , then T[keyof T] is Test.some | Test.another Test.some | Test.another , which is the same as Test , and so the mapped type is just {abc: Test.some; def: Test.another} {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.

Playground link to code

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM