Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

typescript Record accepts array, why?

Can anyone explain why this compiles in typescript?
I tried some googling and looking it up in the typescript documentation but didn't find the answer.

type RecType = Record<string, any>
const arr: RecType = [1, 2, "three"] //or new Array(1, 2, 3)

console.log(arr)  // [1, 2, "three"] 
console.log(Array.isArray(arr)) // true
console.log(Object.keys(arr)) // ["0", "1", "2"] 

here's a typescript playground link with the code

like image 648
samz Avatar asked Jul 16 '26 00:07

samz


2 Answers

You might be surprised, that it accepts more than just the array and object. All of the following are valid assignments:

const a: Record<string, any> = []; 
const b: Record<string, any> = {}; 
const c: Record<string, any> = () => {};
const d: Record<string, any> = new Map();
const e: Record<string, any> = new Date();
const f: Record<string, any> = new Boolean(); 
const g: Record<string, any> = new String();
const h: Record<string, any> = class Klass{};

When you call a constructor with new, then the instanceof Object will return true:

function instanceOfObject(arg: Record<string, any>) {
    return arg instanceof Object;
}

// all true
console.log(
    instanceOfObject([]),
    instanceOfObject({}),
    instanceOfObject(() => {}),
    instanceOfObject(new Map()),
    instanceOfObject(new Date()),
    instanceOfObject(new Boolean()),
    instanceOfObject(new String()),
    instanceOfObject(class Klass{}),
)

So, the Record here is only a nickname for { [P in string]: any; } which means something like "having a string index signature". Effectively our instanceOfObject function accepts any objects/instances.

Remember that TypeScript is structurally typed, so it checks the objects interface and looks for the signature. Here the Object interface has the string signature.

Perhaps a better name for the Record would be Indexed, so as to not confuse it with a dictionary which is common in other languages, but not available in JavaScript.

For example, we know that array can be indexed by a numeric index. So we can assign:

const arr: Record<number, unknown> = [1,2,3]; // OK

But you might be again surprised:

const arr: Record<number, unknown> = "whoa"; // OK

Yes, even a string literal "can be assigned to Record". This works because string is indexable by number. Also note, that literal types are autoboxed in JavaScript, and not initialized with new, so the instanceof fails.

console.log("str" instanceof String); // false
console.log(new String("str") instanceof String); // true

That's why you cannot pass the "whoa" literal to the previous function:

// Argument of type 'string' is not assignable to parameter of type  
// 'Record<string, any>'
instanceOfObject("whoa"); // ERR

That's why we use the typeof operator like in typeof "str" === "string" for literal values, and also we avoid using the String() constructor and related classes.

So when you want to accept only object literals, use the Record<string, unknown> which does not permit the object instances from above, and not even the array, as it does not have the string index signature:

// Type 'number[]' is not assignable to type 'Record<string, unknown>'.
//  Index signature for type 'string' is missing in type 'number[]'
const obj: Record<string, unknown> = [1];

Checkout TS Playground

like image 198
Mist Avatar answered Jul 18 '26 18:07

Mist


After resting a little, coming back to the problem and looking a little deeper I think I understand it.

this is how Record is defined in typescript (from it's source code)

type Record<K extends keyof any, T> = {
    [P in K]: T;
};

an array can be assigned to this because

  • keyof operator will return the array's indices as the keys (similar to how Object.keys does).
  • the accessor [P in K]: T is valid for array because array["0"] is a valid way to access index 0 of the array (and so is array[0]

I hope I got it right, feel free to make corrections.

like image 40
samz Avatar answered Jul 18 '26 16:07

samz