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())?
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With