How can I check that an object has at least one record?

Viewed 90

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

1 Answers

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

Related