Search code examples
typescripttypescript-typingstypescript-generics

Properly restricting Record in generic function


I'm trying to declare a generic TypeScript function that accepts a Record<number, boolean> and returns the same value. I need the input type to be a generic type parameter, that is reflected in the output. Unfortunately, calling the function, I don't get an error, when passing something other than a number (string) as the record key:

const func = <T extends Record<number, boolean>>(input: T): T => input;

const x = func({
    1: true,
    test: false // <-- doesn't show error
})

It works if I inline the type Record<number, boolean>, but then I'm not able to use it in my return type:

const func2 = (input: Record<number, boolean>): Record<number, boolean> => input;

const y = func2({
    1: true,
    test: false // <-- shows error
});
// ^? y: Record<number, boolean> - and not { 1: true }

How can I constrain my types, so I get errors for the input, but also preserve the output type?

Disclaimer: this is a simplified example. In the real code, more generic parameters exist and T is used in multiple places, one of them being a more complicated return type based on T.


Solution

  • Object types in TypeScript are open/extendible and not sealed/closed/"exact". If you have a type constraint like T extends U, it is quite possible for T to have more keys in it than are mentioned in U. There is a longstanding open issue at microsoft/TypeScript#12936 requesting "exact" types, so that presumably T extends Exact<U> would prohibit T from having any extra keys in it, but for now it's not part of the language.

    So instead of T extends Record<number, boolean>, which requires that all numeric keys have boolean values, but which says nothing whatsoever about non-numeric keys, you could make a recursive constraint like this:

     const func = 
       <const T extends { [K in keyof T]: K extends number ? boolean : never }>(
         input: T
       ): T => input;
    

    Essentially T is constrained to a version of itself where all numeric keys have property values of type boolean, while all other keys have property values of the impossible never type. This will result in an error where you want it:

    const y = func({ 1: true, test: false }); // error!
    // ---------------------> ~~~~
    // Type 'boolean' is not assignable to type 'never'.(2322)
    

    Oh and since you apparently care about the literal type of the boolean properties, you can make T a const type parameter as shown above, which (if you have no errors to mess things up), will preserve such information:

    const x = func({ 1: true, 2: false });
    /* const x: {
        readonly 1: true;
        readonly 2: false;
    } */  
    

    Playground link to code