Uri Klar
Uri Klar

Reputation: 3988

Enforce object to contain all keys of an enum and still have type inference for it's values

I have an object and I want to enforce it to contain all keys of an Enum, and I also want the type of it’s values to be inferred. So if I do this:

enum RequiredKeys {
    A = 'a',
    B = 'b'
}
const objectThatShouldContainAllRequiredKeys = {
    [RequiredKeys.A]: (id: string) => {}
};
// Argument of type '123' is not assignable to parameter of type 'string'
// Which is great, that's exactly what I want. 
objectThatShouldContainAllRequiredKeys[RequiredKeys.A](123);

But now, I tried enforcing the object keys and every solution I try breaks the type inference. For example:

enum RequiredKeys {
    A = 'a',
    B = 'b'
}
// Property 'b' is missing in type '{ a: (id: string) => void; }' but required in type 'Record<RequiredKeys, Function>'.
// Which is great, that's exactly what I want
const objectThatShouldContainAllRequiredKeys: Record<RequiredKeys, Function> = {
    [RequiredKeys.A]: (id: string) => {}
};
// No error here, which is less great...
objectThatShouldContainAllRequiredKeys[RequiredKeys.A](123);

Any idea how I can enjoy both worlds? Have the object enforce all keys from the enum and infer the object values? Thanks!!

Upvotes: 3

Views: 1896

Answers (3)

Rom Haviv
Rom Haviv

Reputation: 188

@Aleksey L. 's solution works great!

Though, tried to make a generic approach.. I wonder if anyone can come up with a better way to do it:

function createWithRequiredKeys<TKeys extends keyof any>() {
  return <TObject extends Record<TKeys, unknown>>(obj: TObject): TObject => obj;
}
const withRequiredKeys = createWithRequiredKeys<RequiredKeys>()({
  [RequiredKeys.A]: (id: string) => {},
  [RequiredKeys.B]: "foo",
});

edit:

running this function after the object declaration might do the trick as well:

function validateRequiredKeys<TKeys extends keyof any>(
  obj: Record<TKeys, unknown>
): void {
  return;
}

const obj = {...}
validateRequiredKeys<RequiredKeys>(obj); //this will fail

Upvotes: 0

Fast solution: (SECOND UPDATE)

enum RequiredKeys {
  A = 'a',
  B = 'b'
}

type Mapped = {
  [RequiredKeys.A]: (id: string) => any,
  [RequiredKeys.B]: (id: number) => any
}

// Property 'b' is missing in type '{ a: (id: string) => void; }' but required in type 'Record<RequiredKeys, Function>'.
// Which is great, that's exactly what I want
const objectThatShouldContainAllRequiredKeys: Mapped = {
  [RequiredKeys.A]: (id: string) => { },
  [RequiredKeys.B]: (id: number) => { }
}
// No error here, which is less great...
const result = objectThatShouldContainAllRequiredKeys[RequiredKeys.A]('ok'); // ok
const result2 = objectThatShouldContainAllRequiredKeys[RequiredKeys.B](1); // ok

const result3 = objectThatShouldContainAllRequiredKeys[RequiredKeys.B]('1'); // error
const result4 = objectThatShouldContainAllRequiredKeys[RequiredKeys.A](2); // error

If you want more generic solution, you can take a look on this answer

Please keep in mind, that in case of more generic solution, you should create a type map for enum and function types

Upvotes: 0

Aleksey L.
Aleksey L.

Reputation: 37928

You can create identity function with type parameter constrained to have required keys, so typescript will validate the passed object keys and will infer its values' types:

const createWithRequiredKeys = <T extends Record<RequiredKeys, unknown>>(obj: T) => obj;

const withRequiredKeys = createWithRequiredKeys({
    [RequiredKeys.A]: (id: string) => {},
    [RequiredKeys.B]: 'foo',
}); 

// withRequiredKeys is { a: (id: string) => void; b: string; }

withRequiredKeys[RequiredKeys.A](123); // 'number' is not assignable to parameter of type 'string'

Playground

Upvotes: 5

Related Questions