Home > Software design >  Why does interface extends Record<string, ...> allow numeric keys?
Why does interface extends Record<string, ...> allow numeric keys?

Time:04-12

I am trying to find a relatively generic way to type POST bodies and the responses I get back in conjunction with their API routes (in a nextjs app).

For this I want the compiler to force me to add a body type and a return type to all the API routes, which I achieved with the following interface:

export interface PostTypeMapping extends Record<string, {body: unknown, return: unknown}> {
  "/api/taskCompletions": {body: PostCompletionBody, return: void},
  "/api/task": {body: PostTaskBody, return: void},
}

So far so good. I can use this type in my API route like so:

 async (req, res: NextApiResponse<PostTypeMapping["api/task"]["return"]>) => {
  //...
}

But when I try to write a wrapper that automatically infers the POST body and the return type from the URL, I get an error in the line with await fetch(url,:

Argument of type 'keyof PostTypeMapping' is not assignable to parameter of type 'RequestInfo'. Type 'number' is not assignable to type 'RequestInfo'

export async function fetchPost<T extends keyof PostTypeMapping>(url: T, body: PostTypeMapping[T]["body"]): Promise<PostTypeMapping[T]["return"]> {
  try {
    const res = await fetch(url, { // <- The error above occurs here
      method: "POST",
      body: JSON.stringify(body),
      headers: {
        "Content-Type": "application/json",
      },
    });
    if(res.status === 201 || res.status === 204){
      return;
    }
    return res.json();
  } catch (err: any){
    return {message: "Fehler: "   err.message};
  }
}

Why can url, which is typed as keyof PostTypeMapping, be a number?

I investigated further and for the most part, the extends Record<string, {body: unknown, return: unknown}> does seem to do what I want (force me to add a body and return type to all the entries of the interface), but allows numeric keys as well as strings. Why? Both cases that are not allowed are good.

export interface PostTypeMapping extends Record<string, {body: unknown, return: unknown}> {
  "/api/taskCompletions": {body: PostCompletionBody, return: void},
  "/api/task": {body: PostTaskBody, return: void},
  1: {body: void, return: void}, // why is this legal? 1 is not a string
  2: "asd" // not allowed -> Property '2' of type '"asd"' is not assignable to 'string' index type '{ body: unknown; return: unknown; }'
  "asd": "asd" // not allowed -> Property '"asd"' of type '"asd"' is not assignable to 'string' index type '{ body: unknown; return: unknown; }
}

typescript playground

EDIT:

A simplified reproduction of the problem can be found at https://tsplay.dev/Nr5X2w thanks to T.J. Crowder

CodePudding user response:

See microsoft/TypeScript#48269 for an authoritative answer to this question.

Numeric keys have always been allowed for string index signatures, because non-symbol keys in JavaScript are always coerced to strings first. So the "number" keys should really be more like "numeric strings", but TypeScript allows you to think of them as numbers to support indexing into arrays with numbers.

Prior to TypeScript 2.9, keyof {[k: string]: any} would have just been string. But TypeScript 2.9 introduced support for number and symbol properties with keyof. Part of this change is that keyof X where X has a string index signature now includes number. So keyof {[k: string]: any} is string | number. This is working as intended.

But for mapped types like Record, the compiler does not immediately augment the keys this way. Apparently it is important that Record<K, V> be properly contravariant in K (according to the comment in ms/TS#48269 anyway).

But Record<string, any> is, after all, equivalent to {[k: string]: any}, and therefore we have an inconsistency. TypeScript doesn't take consistency as its most important design goal; indeed, it is a non-goal of TypeScript to have a provably correct type system. Productivity is, in some sense, more important than correctness. If fixing an inconsistency would make TypeScript very annoying to use for a lot of people, then it's better to leave the inconsistency. And this is apparently one of those situations; according to the same comment, the inconsistency here can't be eliminated (presumably without destroying some oft-used part of the language, such as numeric keys for arrays), so it stays.

Oh well!

  • Related