Search code examples
typescript

correct way to represent any object in Typescript


I want to create a function to avoid this:

Object.keys(object)
  .map((key) => key as keyof typeof object) // appease ts
  .some((key) => ... // do something with object[key] without ts complaining about it...),

all these options seems to work fine:

function keys<Key extends string | symbol | number, O extends Record<Key, unknown>>
  (object: O) { // OK

// function keys<O extends Object>(object: O) { // OK

// function keys<O extends {}>(object: O) { // OK

// function keys<O extends object>(object: O) { // OK
  return Object.keys(object) as Array<keyof O>
}

In this article I found the following explanation:

  • The TypeScript object type represents any value that is not a primitive value.
  • The Object type, however, describes functionality that is available on all objects.
  • The empty type {} refers to an object that has no property on its own.

It seems to me like the first option (O extends Record<Key, unknown>) is more accurate, but on the other hand Object.keys is types like this in Typescript: (method) ObjectConstructor.keys(o: {}): string[]

So I'd like to know the differences and which one is better for representing any object


Solution

  • You really shouldn't try to make a generic version of Object.keys. There's a good reason Object.keys returns string[] and not Array<keyof typeof whatever>: the keys of an object may be altered at runtime, and because TS is structurally typed there can always be extra keys in the object beyond it's specified type, and those "extra" keys could point to anything.

    It is not type-safe to do what you are doing, and it is disingenuous to callers of your keys function to pretend it is. Somebody might call your function, iterate through the array, pull object properties that per the type definition should be values of a certain type, and surprise! get blown up runtime. Probably in production, because some random guy named Murphy hates you.

    But in terms of your actual question, check out this content from reddit:

    The difference between {}, object and Record<string, unknown> explained

    Value is assignable to {} object Record<string, unknown>
    "string" Yes No No
    true Yes No No
    42 Yes No No
    42n Yes No No
    Symbol() Yes No No
    null No No No
    undefined No No No
    () => {} Yes Yes No
    [1, 2] Yes Yes No
    [] Yes Yes No
    {foo: "bar"} Yes Yes Yes
    {} Yes Yes Yes

    In essence:

    You can assign anything to {}, except for null and undefined.

    You can assign anything that's not a primitive to object. Primitives are strings, booleans, numbers, big integers, symbols, null and undefined.

    You can only assign non-primitive non-function non-array objects to Record<string, unknown>.

    Here's a TypeScript Playground demonstrating the table above.


    In conclusion, trying to type out tables in markdown sucks. At least when I do it in HTML the LSP autocompletes it for me. TIL.