Issue
I have the following enum:
enum GREETINGS { HELLO = 'Hello', HI = 'Hi' }
I want to create a type for a Set data structure which would force the user to include all of the enum values within it, like below:
type MyGreetings = Set<GREETINGS>
The expected outcome should cause the following line to be highlighted with a warning stating that not all required values are included within the result:
const result: MyGreetings = new Set([GREETINGS.HELLO]);
Solution
One problem is that TypeScript considers Set<GREETINGS.HELLO>
to be assignable to / compatible with Set<GREETINGS>
; in theory that should mean that if you want Set<GREETINGS>
to be assignable to MyGreetings
, then by transitivity of assignability, Set<GREETINGS.HELLO>
will also be assignable to MyGreetings
, and you're stopped before you've begun. But:
The fact that Set<GREETINGS.HELLO>
is assignable to Set<GREETINGS>
is actually using an intentional hole in the type system: method parameter bivariance. Method parameter bivariance is very useful, but it is not type safe:
const justHello = new Set<GREETINGS.HELLO>([GREETINGS.HELLO]);
const stillJustHello: Set<GREETINGS> = justHello;
stillJustHello.add(GREETINGS.HI) // <-- no error, oops
By "widening" justHello
to stillJustHello
, the compiler has forgotten that it should not accept any member but GREETINGS.HELLO
.
So one way to get closer to what you want is to use the --strictFunctionType
compiler option (which is part of the --strict
suite of compiler options and is recommended in general) and rewrite at least one Set
method signature using a function property type instead of a method signature. Let's look at add()
and see the difference:
type Method = { add(x: GREETINGS): void } // method syntax
const m: Method = new Set<GREETINGS.HELLO>(); // okay
type FuncProp = { add: (x: GREETINGS) => void } // function syntax
const f: FuncProp = new Set<GREETINGS.HELLO>(); // error
So let's try something like:
type MyGreetings = Set<GREETINGS> & { add: (g: GREETINGS) => MyGreetings };
If we use that, you get the desired result:
const result: MyGreetings = new Set([GREETINGS.HELLO]); // error!
// Type 'GREETINGS' is not assignable to type 'GREETINGS.HELLO'
const okay: MyGreetings = new Set([GREETINGS.HELLO, GREETINGS.HI]); // okay
Hooray!
Well, kind of. The compiler really cannot know exactly which values have been passed into a Set
. It can infer that new Set(...)
is of type Set<X>
for some X
, but there are ways to manually specify a wider X
or get the compiler to infer a wider X
. And then all the progress from above is lost.
For example, since a Set<GREETINGS>
is allowed to contain just a subset of GREETINGS
, someone can do the following manual specification:
const oops: MyGreetings = new Set<GREETINGS>([GREETINGS.HELLO]) // no error
Or someone could get the compiler to infer a union this way:
const alsoOops: MyGreetings = new Set([
Math.random() < 0.5 ? GREETINGS.HELLO : GREETINGS.HI
]); // no error
Neither of those can be an error; the value on the right hand side of the equals sign is a Set<GREETINGS>
. And you need Set<GREETINGS>
to be assignable to MyGreetings
, so the assignments are also not errors. There's not much that can be done about this.
If in practice you don't think someone will create a set in these or other pathological ways, then maybe you can use MyGreetings
. Otherwise, if you really need to guarantee something, you should consider changing your design in some way. But this is probably out of scope for the question, so I'll stop there.
Playground link to code (tested and working for typescript version 3.5.1 and higher)
Answered By - jcalz Answer Checked By - Gilberto Lyons (PHPFixing Admin)
0 Comments:
Post a Comment
Note: Only a member of this blog may post a comment.