Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript how to enforce record key to be string when extended

Tags:

typescript

I'm trying to create a function that will accept an object as a map and that a further nested function will utilize it's keys(I'll need the value as well in the future):

  function exec(a: string) {
    return a;
  }
  
  function one<T extends Record<string, any>>(a: T) {
    return <K extends keyof T>(b: K) => {
    exec(b);// TS2345: Argument of type 'string | number | symbol' is not assignable to parameter of type 'string'.   Type 'number' is not assignable to type 'string'.
   };  
 }

but I get this error TS2345: Argument of type 'string | number | symbol' is not assignable to parameter of type 'string'.   Type 'number' is not assignable to type 'string'. since object keys could be number | Symbol as well and not strictly string. Can it be done without exec(b.toString())?

like image 799
gadi tzkhori Avatar asked Sep 16 '25 22:09

gadi tzkhori


1 Answers

As you noted, a string index signature does not prevent there from being non-string keys (it only means that any string keys that exist need to have properties compatible with the index signature, which for Record<string, any> is exceedingly lax). And the same thing holds for generic type parameters with constraints like T extends U; even if U doesn't have a particular property key, it doesn't mean T can't have that key. Essentially, object types in TypeScript are open and not exact (as requested in microsoft/TypeScript#12936), and in most circumstances the compiler will not prohibit excess properties.

This means you can call one like this:

const sym = Symbol();
const a = { str: 1, [sym]: 2 }
const fn = one(a); // okay
fn("str"); // okay
fn(sym); // okay

which is why the implementation of one complains that b might not be a string.


If you want to prevent that, you'll need to constrain K to be not just keyof T, but just the subtype of keyof T also assignable to string. The easiest way to express that is to just intersect string with keyof T:

function one<T extends Record<string, any>>(a: T) {
    return <K extends string & keyof T>(b: K) => {
        exec(b); // okay
    };
}

Now the compiler knows that b must be keyof T as well as a string. And now you get the desired error if you try to call the function returned from one with a non-string input:

const sym = Symbol();
const a = { str: 1, [sym]: 2 }
const fn = one(a); // okay
fn("str"); // okay
fn(sym); // error

Playground link to code

like image 52
jcalz Avatar answered Sep 19 '25 12:09

jcalz