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

10,914

Solution 1

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

Solution 2

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'.
Share:
10,914

Related videos on Youtube

Zoltán Tamási
Author by

Zoltán Tamási

Recently focusing on Cloud-based architectures and software design. I mostly work with C#, TypeScript, JavaScript, HTML/CSS. I develop large ASP.NET WebApi/MVC web applications, and rich HTML5/TypeScript web clients using mostly today's technologies. I like hanging out here in my free time.

Updated on May 26, 2021

Comments

  • Zoltán Tamási
    Zoltán Tamási about 3 years

    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.

    • toskv
      toskv over 7 years
      could you give us an example of what you'd like to return?
    • Zoltán Tamási
      Zoltán Tamási over 7 years
      Thank you, added some further clarifications
    • Gromski
      Gromski over 7 years
      In the end this means you'll pretty much accept any value, since pretty much everything in Javascript is an object and you don't even care about any specific characteristics of it. The caller of your function may decide to implement your desired object as a class for their own purposes; the resulting object will still be perfectly compatible with your expected "plain" object, especially if you don't even really care about anything about that object. While an interesting question, I somewhat fail to see the practicality of it.
    • Zoltán Tamási
      Zoltán Tamási over 7 years
      @deceze Thanks for the good comment, in theory you're right. I need it for a specific use-case. I need it for a method which returns a promise of an AJAX call, and I want to indicate that the promise value is a plain object (parsed by JSON.parse for example), and not any instance of any class. So I'd use it as an output type, not an input.
    • Gromski
      Gromski over 7 years
      You typically type hint to enforce specific characteristics of an object; why exactly are you trying to type hint for the absence of specific characteristics? It shouldn't really matter whether the method returns a class or not, that's an implementation detail. As long as that class instance still conforms to the expected behaviour, which in this case is any, that shouldn't matter.
    • Mike Cluck
      Mike Cluck over 7 years
      @ZoltánTamási Do you expect the result of that AJAX call to have a consistent layout? As in, could you describe the results as a type even though there isn't a constructor?
    • Zoltán Tamási
      Zoltán Tamási over 7 years
      I've updated the post with my detailed example use-case
    • Mike Cluck
      Mike Cluck over 7 years
      Presumably, MyControllerResult still expects plainResult to have certain properties, right? Why don't you create a type definition for that?
    • Zoltán Tamási
      Zoltán Tamási over 7 years
      @MikeC The assignment happens dynamically at runtime (like jQuery's extend), and I don't want to kind of duplicate the layout of MyControllerResult. Actually MyControllerResult is a knockoutjs viewmodel.
    • Gromski
      Gromski over 7 years
      Last attempt: the type system allows for inheritance and sub classes of the hinted type are equally accepted. Since any object will be a sub class of "a plain object", this type hint, if it existed, won't be able to enforce very much.
    • John Weisz
      John Weisz over 7 years
      What might work in your case is requiring an object with an explicitly defined index signature. This will require the --noImplicitAny compiler option. Any class or interface that does not explicitly implement the index signature will be rejected by TypeScript, although I believe Object is one of them (thus you'll need to modify the native type lib).
    • Zoltán Tamási
      Zoltán Tamási over 7 years
      @deceze Sorry but I don't get your points. There is nothign like any in the use-case example code.
    • Gromski
      Gromski over 7 years
      I keep using any half as a pun, and half as a hint that that may be the only real type applicable here. :)
    • Mike Cluck
      Mike Cluck over 7 years
      @ZoltánTamási Here's the thing: types are used to enforce contracts. If you don't have a specific contract, then you can't expect to statically enforce the typing. However, again, since MyControllerResult requires the object to have some specific properties, you should document those then use that as your type. I fail to see how PlainObject is useful here.
    • toskv
      toskv over 7 years
      how about something like this?typescriptlang.org/play/…
    • Zoltán Tamási
      Zoltán Tamási over 7 years
      I do have a contract, the contract that it should be a plain object, meaning that it's constructor is Object. This is a pretty well defined contract in my opinion. See this jsfiddle jsfiddle.net/pyd466tb
    • Mike Cluck
      Mike Cluck over 7 years
      @ZoltánTamási It's really not. You're only stating that it should be an Object but that doesn't tell you anything about the kind of data you're expecting. Please explain to me why defining your contract based on the kind of data you're going to receive (i.e., what properties you expect the object to have) is a bad idea. Right now it seems like you're just focused on applying your flawed idea rather than correcting your approach.
    • Zoltán Tamási
      Zoltán Tamási over 7 years
      I misused the word "contract", sorry. It's indeed not literally a contract, but it's a kind of type restriction Generally it's of course not a bad idea in any sense to enuemrate the properties, I just want to omit that, because it's irrelevant in my code, as I have dynamic mappings from these plain results. Some of the web API results have really complex structure, what I already have defined in the strongly-typed viewmodel classes, I wouldn't like to duplicate them
    • Zoltán Tamási
      Zoltán Tamási over 7 years
      Okay, thank you guys for your constructive ideas, I think the end result is that it's simply not possible, so I'll rethink it.
    • Mike Cluck
      Mike Cluck over 7 years
      @ZoltánTamási If you've already defined it for your viewmodels, why not just change those descriptions to an interface and have your viewmodels use that interface? Then there's no duplication, you just say that both the viewmodel and the AJAX data use the same interface.
    • Zoltán Tamási
      Zoltán Tamási over 7 years
      @MikeC There is a code duplication, because I have to build the interface with the same property names as my viewmodel. But indeed, that would be the most robust and theoretically correct approach, you're right. I just wouldn't make use of it anywhere. I just need the fact that the return type is a plain object :)
  • Zoltán Tamási
    Zoltán Tamási over 7 years
    The problem is that an assignment like var obj: {[index:string]: any} = new MyControllerResult() is still accepted by the compiler using TS2.1. It might be related to noImplicitAny, I don't have that option, and I can't even turn it for in the current codebase.
  • Nitzan Tomer
    Nitzan Tomer over 7 years
    Why is that a problem? Every object in js is a "PlainObject", a class instance is also a plain object. It similar to doing: class A {} then class B extends A {} and let a: A = new B().
  • Zoltán Tamási
    Zoltán Tamási over 7 years
    No, a plain object is plain in the sens that it's not an instance of a "custom" class. In your code you're checking exactly that. But a type-guard is not a type hint unfortunately.
  • Zoltán Tamási
    Zoltán Tamási over 7 years
    Yes, exactly that.. thank you for pointing out the key. Do you have any idea why it is defined so? I mean why isn't the Object.constructor defined as ObjectConstructor?
  • Zoltán Tamási
    Zoltán Tamási over 7 years
    Nevermind, I've figured it out I guess. It needs to be a Function to allow any class instance to still be an Object.
  • Nitzan Tomer
    Nitzan Tomer over 7 years
    Well, that's actually a good question. In js functions are objects, and it even states in the comment above the constructor that this is the case (updated the comment in my answer). I'm not sure why then this isn't the case in the lib.d.ts file. Maybe there are reasons for it that I'm unaware of. Maybe this calls for posting an issue
  • Zoltán Tamási
    Zoltán Tamási over 7 years
  • Zoltán Tamási
    Zoltán Tamási over 7 years
    Here is the related TS ticket: github.com/Microsoft/TypeScript/issues/13866
  • DanielM
    DanielM about 6 years
    Keep in mind that this PlainObject type will incorrectly report that arrays are plain objects. This was unexpected to me, and I opened an issue about it - github.com/Microsoft/TypeScript/issues/24083.
  • Zoltán Tamási
    Zoltán Tamási about 3 years
    Thank you for posting this, sorry for not noticing it so long. This is indeed a better approach using the up-to-date feature.
  • Maciej Krawczyk
    Maciej Krawczyk almost 3 years
    Note that symbol is not a part of JSON.