DEV Community

tmhao2005
tmhao2005

Posted on • Edited on

Typescript override typing for `Object.keys`

If you guys work with Object in Typescript long enough, we all are aware of that one of the most used methods are Object.keys always returning a string[] which should return a list of union types of keys.

Here's how the Object.keys is defined within Typescript's built-in types, you can find it in lib.es5.d.ts:

interface ObjectConstructor {
    // other stuff

    /**
     * Returns the names of the enumerable string properties and methods of an object.
     * @param o Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object.
     */
    keys(o: object): string[];
}

Enter fullscreen mode Exit fullscreen mode

This is definitely an annoying issue which should be fixed probably later on. So util then, there's a solution for it :)

  • First of all, we define a proper type which returns a list of keys of a generic object:
// some util types
type IsAny<T> = 0 extends 1 & T ? true : T;
type KnownKeys<T> = {
    [K in keyof T as string extends K ? never : number extends K ? never : symbol extends K ? never : K]: T[K];
}
type IsEmptyObject<T extends Record<PropertyKey, unknown>> = [keyof T] extends [never]
    ? true
    : false;

type ObjectKeys<T> = IsAny<T> extends true
    ? string[]
    : T extends object
    ? IsEmptyObject<KnownKeys<T>> extends true
        ? string[]
        : (keyof KnownKeys<T>)[]
    : T extends number
    ? []
    : T extends Array<any> | string
    ? string[]
    : never;

interface ObjectConstructor {
    keys<T>(o: T): ObjectKeys<T>;
}

// testing
type Test1 = ObjectKeys<{foo: string, bar: string }> // Array<"foo" | "bar">
type Test2 = ObjectKeys<['']> // string[]
type Test3 = ObjectKeys<"foo">  // string[]
type Test4 = ObjectKeys<Record<PropertyKey, any>> // string[]
Enter fullscreen mode Exit fullscreen mode
  • Override the built-in ObjectConstructor interface. Let's define object.d.ts in your repo, normally I place the extra typing files under at typings dir. So in my case it's going to be here typings/object.d.ts:
// typings/object.d.ts
type IsAny<T> = 0 extends 1 & T ? true : T;
type KnownKeys<T> = {
    [K in keyof T as string extends K ? never : number extends K ? never : symbol extends K ? never : K]: T[K];
}
type IsEmptyObject<T extends Record<PropertyKey, unknown>> = [keyof T] extends [never]
    ? true
    : false;

type ObjectKeys<T> = IsAny<T> extends true
    ? string[]
    : T extends object
    ? IsEmptyObject<KnownKeys<T>> extends true
        ? string[]
        : (keyof KnownKeys<T>)[]
    : T extends number
    ? []
    : T extends Array<any> | string
    ? string[]
    : never;

// interface can be merged together
interface ObjectConstructor {
  keys<T>(o: T): ObjectKeys<T>;
}

Enter fullscreen mode Exit fullscreen mode
  • Finally, include all defined typing files under typings to the configuration tsconfig.json:
// tsconfig.json
{
  // ...
  include: [..., "typings"]
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)