Is there any way to target the plain JavaScript object type in TypeScript?

16.1k Views Asked by At

UPDATE 2021

For a working solution using newer features see this answer https://stackoverflow.com/a/59647842/1323504


I'm trying to write a function where I'd like to indicate that it returns some kind of plain JavaScript object. The object's signature is unknown, and not interesting for now, only the fact that it's a plain object. I mean a plain object which satisfies for example jQuery's isPlainObject function. For example

{ a: 1, b: "b" }

is a plain object, but

var obj = new MyClass();

is not a "plain" object, as its constructor is not Object. jQuery does some more precise job in $.isPlainObject, but that's out of the question's scope.

If I try to use Object type, then it will be compatible to any custom object's too, as they're inherited from Object.

Is there a way to target the "plain object" type in TypeScript?

I would like a type, which would satisfy this for example.

var obj: PlainObject = { a: 1 }; // perfect
var obj2: PlainObject = new MyClass(); // compile-error: not a plain object

Use case

I have kind of a strongly-typed stub for server-side methods, like this. These stubs are generated by one of my code generators, based on ASP.NET MVC controllers.

export class MyController {
  ...
  static GetResult(id: number): JQueryPromise<PlainObject> {
    return $.post("mycontroller/getresult", ...);
  }
  ...
}

Now when I call it in a consumer class, I can do something like this.

export class MyViewModelClass {
  ...
  LoadResult(id: number): JQueryPromise<MyControllerResult> { // note the MyControllerResult strong typing here
    return MyController.GetResult(id).then(plainResult => new MyControllerResult(plainResult));
  }
  ...
}

And now imagine that the controller method returns JQueryPromise<any> or JQueryPromise<Object>. And now also imagine that by accident I write done instead of then. Now I have a hidden error, because the viewmodel method will not return the correct promise, but I won't get a compile-error.

If I had this imaginary PlainObject type, I'd expect to get a compile error stating that PlainObject cannot be converted to MyControllerResult, or something like that.

4

There are 4 best solutions below

3
On BEST ANSWER

Tested in TypeScript 3.7.2:

For a flat plain object, you can do:

type Primitive =
  | bigint
  | boolean
  | null
  | number
  | string
  | symbol
  | undefined;

type PlainObject = Record<string, Primitive>;

class MyClass {
  //
}

const obj1: PlainObject = { a: 1 }; // Works
const obj2: PlainObject = new MyClass(); // Error

For a nested plain object:

type Primitive =
  | bigint
  | boolean
  | null
  | number
  | string
  | symbol
  | undefined;

type JSONValue = Primitive | JSONObject | JSONArray;

interface JSONObject {
  [key: string]: JSONValue;
}

interface JSONArray extends Array<JSONValue> { }

const obj3: JSONObject = { a: 1 }; // Works
const obj4: JSONObject = new MyClass(); // Error

const obj5: JSONObject = { a: { b: 1 } }; // Works
const obj6: JSONObject = { a: { b: { c: 1 } } }; // Works
const obj7: JSONObject = { a: { b: { c: { d: 1 } } } }; // Works

Code is an adaptation from https://github.com/microsoft/TypeScript/issues/3496#issuecomment-128553540

For primitives, see mdn web docs > Primitive.


An alternative solution would be to use a library sindresorhus / type-fest or use the implementation from it found here: https://github.com/sindresorhus/type-fest/blob/main/source/basic.d.ts.

0
On

I've found this to work in TS 4.9:

type PlainObject = Record<string, {}>;

class MyClass {}

function test(one: PlainObject) {}
test({ a: 1, b: "a", c: new MyClass() }); // OK
test(1); // Error
test("a"); // Error
test([1, 2, 3]); // Error
test(new Date()); // Error
test(new Map()); // Error
test(new Set()); // Error
test(new MyClass()); // Error

TS Playground link

The difference with the accepted answer is that it also allows the plain object to contain complex values, and not only primitives:

type ObjectOfPrimitives = Record<string, Primitive>;

function test(one: ObjectOfPrimitives) {}

test({ 
  a: 1, 
  b: "a", 
  c: new MyClass(), // Error: Type 'MyClass' is not assignable to type 'ObjectOfPrimitives'.
}); 

TS Playground link

9
On

In my code I have something similiar to what you're asking:

export type PlainObject = { [name: string]: any }
export type PlainObjectOf<T> = { [name: string]: T }

And I also have a type guard for that:

export function isPlainObject(obj: any): obj is PlainObject {
    return obj && obj.constructor === Object || false;
}

Edit

Ok, I understand what you're looking for, but unfortunately that is not possible.
If i understand you correctly then this is what you're after:

type PlainObject = {
    constructor: ObjectConstructor;
    [name: string]: any
}

The problem is that in 'lib.d.ts' Object is defined like so:

interface Object {
    /** The initial value of Object.prototype.constructor is the standard built-in Object constructor. */
    constructor: Function;

    ...
}

And then this:

let o: PlainObject = { key: "value" };

Results with an error:

Type '{ key: string; }' is not assignable to type 'PlainObject'.
  Types of property 'constructor' are incompatible.
    Type 'Function' is not assignable to type 'ObjectConstructor'.
      Property 'getPrototypeOf' is missing in type 'Function'.
0
On

You can try recursive type (naive solution)

type SerializableObject = { [x: string]: SerializableObject | number | string | [] };

Not good, not terrible :)