Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to force interface to "implement" keys of enum in typescript 3.0?

Suppose I have some enum E { A = "a", B = "b"}. I would like to force some interfaces or types(for the sake of readability I'll mention only interfaces) to have all the keys of E. However, I want to specify the type of each field separately. Therefore, { [P in E]: any } or even { [P in E]: T } aren't proper solutions.

For instance, the code may contain two interfaces implementing E:

  • E { A = "a", B = "b"}
  • Interface ISomething { a: string, b: number}
  • Interface ISomethingElse { a: boolean, b: string}

As E extends during development it might become:

  • E { A = "a", B = "b", C="c"}
  • Interface ISomething { a: string, b: number, c: OtherType}
  • Interface ISomethingElse { a: boolean, b: string, c: DiffferntType}

And a few hours later:

  • E { A = "a", C="c", D="d"}
  • Interface ISomething { a: string, c: ChosenType, d: CarefullyChosenType}
  • Interface ISomethingElse { a: boolean, c: DiffferntType, d: VeryDifferentType}

And so on and so forth. Hence, from https://www.typescriptlang.org/docs/handbook/advanced-types.html it looks like it's not supported yet. Is there any typescript hack I am missing?

like image 399
poli Avatar asked Aug 13 '18 20:08

poli


1 Answers

I guess you're committed to writing out both the enum and the interface, and then hoping TypeScript will warn you the interface is missing keys from the enum (or maybe if it has extra keys)?

Let's say you have

enum E { A = "a", B = "b", C="c"};
interface ISomething { a: string, b: number, c: OtherType};

You can use conditional types to make TypeScript figure out if any constituents of E are missing from the keys of ISomething:

type KeysMissingFromISomething = Exclude<E, keyof ISomething>;

This type should be never if you don't have any keys missing from ISomething. Otherwise, it will be one of the values of E like E.C.

You can also make the compiler figure out if ISomething has any keys which are not constituents of E, also using conditional types... although this is more involved because you can't quite manipulate enums programmatically in expected ways. Here it is:

type ExtraKeysInISomething = { 
  [K in keyof ISomething]: Extract<E, K> extends never ? K : never 
}[keyof ISomething];

Again, this will be never if you don't have extra keys. Then, you can force a compile-time error if either one of these are not never, by using generic constraints along with default type parameters:

type VerifyISomething<
  Missing extends never = KeysMissingFromISomething, 
  Extra extends never = ExtraKeysInISomething
> = 0;

The type VerifyISomething itself is not interesting (it is always 0), but the generic parameters Missing and Extra will give you errors if their respective default values are not never.

Let's try it out:

enum E { A = "a", B = "b", C = "c" }
interface ISomething { a: string, b: number, c: OtherType }
type VerifyISomething<
  Missing extends never = KeysMissingFromISomething,
  Extra extends never = ExtraKeysInISomething
  > = 0; // no error

and

enum E { A = "a", B = "b", C = "c" }
interface ISomething { a: string, b: number } // oops, missing c
type VerifyISomething<
  Missing extends never = KeysMissingFromISomething, // error!
  Extra extends never = ExtraKeysInISomething
  > = 0; // E.C does not satisfy the constraint

and

enum E { A = "a", B = "b", C = "c" }
interface ISomething { a: string, b: number, c: OtherType, d: 1} // oops, extra d
type VerifyISomething<
  Missing extends never = KeysMissingFromISomething,
  Extra extends never = ExtraKeysInISomething // error!
  > = 0; // type 'd' does not satisfy the constraint

So all that works... but it's not pretty.


A different hacky way is to use a dummy class whose sole purpose is to scold you if you don't add the right properties:

enum E { A = "a", B = "b" , C = "c"};
class CSomething implements Record<E, unknown> {
  a!: string;
  b!: number;
  c!: boolean;
}
interface ISomething extends CSomething {}

If you leave out one of properties, you get an error:

class CSomething implements Record<E, unknown> { // error!
  a!: string;
  b!: number;
}
// Class 'CSomething' incorrectly implements interface 'Record<E, unknown>'.
// Property 'c' is missing in type 'CSomething'.

It doesn't warn you about extra properties, although maybe you don't care?


Anyway, hope one of those works for you. Good luck.

like image 144
jcalz Avatar answered Oct 07 '22 03:10

jcalz