4

I have the following TS code:

type FunctionMap = Record<
  string,
  (...params: any) => any
>

function needsARecordOfFunctions(functions: FunctionMap) {
  /* ... */
}

needsARecordOfFunctions({ myFunc: 'foobar' }); // Type 'string' is not assignable to type '(...params: any) => any'.
needsARecordOfFunctions(); // Expected 1 arguments, but got 0.
needsARecordOfFunctions({ myFunc: () => {} }); // ✅

// This passes but I want it to fail
needsARecordOfFunctions({});

My question is, how can I get needsARecordOfFunctions({}) to fail with a type error in the above code? I would like to define a record type that, well, has at least one record defined in it.

Playground

chipit24
  • 6,509
  • 7
  • 47
  • 67
  • Your problem stems from the `{}` being treated not as an empty object in many contexts but as a very wide "anything goes" type. Ensuring that empty objects are disallowed in parameters is tricky. First, you need to make the compiler infer the *exact* type of the argument passed in with a generic type parameter – Oleg Valter is with Ukraine Jun 11 '21 at 16:14
  • Usually, you can then employ the observation that `keyof {}` is `never`, but your case is complicated because of the index signature brought in by the `Record` utility type. You need a utility for removing an index first. See here: https://stackoverflow.com/q/51465182/11407695 – Oleg Valter is with Ukraine Jun 11 '21 at 16:16
  • then you can check whether `keyof` of the resulting generic type parameter is `never` or not like this: https://tsplay.dev/Wzyb3m. I will draft an answer later that explains the steps in detail – Oleg Valter is with Ukraine Jun 11 '21 at 16:21

1 Answers1

1

First, you need to get rid of the index signature as index signature contributes to the shape of the type:

keyof { [x:string]: any }; // string | number

Credit for a generic utility type goes to Mihail, I only adapted it to TS 4.1+. The gist of it is that if string is a subtype of keyof T, it means that the type has a string index signature (check against number gets us numeric signature):

type RemoveIndex<T> = {
  [ P in keyof T as string extends P ? never : number extends P ? never : P ] : T[P]
};

Next, you need to make the compiler infer the type of the argument passed to the parameter to process. Therefore you need a generic function signature, in your case with a single parameter constrained to FunctionMap.

Finally, you have to ensure the inferred type passes the "no empty objects" constraint. This can be achieved by applying an observation that keyof {} is never (see this Q&A for details). Therefore a conditional keyof RemoveIndex<T> extends never ? never : T ensures that such empty object types are not assignable (see this Q&A for a similar example):

function needsARecordOfFunctions<T extends FunctionMap>(functions: keyof RemoveIndex<T> extends never ? never : T) { /* ... */ }

Bringing all of this together:

needsARecordOfFunctions({ myFunc: 'foobar' }); // Type 'string' is not assignable to type 'never'
needsARecordOfFunctions(); // Expected 1 arguments, but got 0.
needsARecordOfFunctions({ myFunc: () => {}, func2: () => 42 }); // ✅
needsARecordOfFunctions({}); // error, expected

Playground