Search code examples
javascripttypescripttypescript2.1

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


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.


Solution

  • 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.