Is there any way to target the plain JavaScript object type in TypeScript?
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'.
Related videos on Youtube
![Zoltán Tamási](https://i.stack.imgur.com/8fqkP.jpg?s=256&g=1)
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, 2021Comments
-
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 notObject
. 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 fromObject
.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>
orJQueryPromise<Object>
. And now also imagine that by accident I writedone
instead ofthen
. 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 thatPlainObject
cannot be converted toMyControllerResult
, or something like that.-
toskv over 7 yearscould you give us an example of what you'd like to return?
-
Zoltán Tamási over 7 yearsThank you, added some further clarifications
-
Gromski over 7 yearsIn 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 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 byJSON.parse
for example), and not any instance of any class. So I'd use it as an output type, not an input. -
Gromski over 7 yearsYou 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 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 over 7 yearsI've updated the post with my detailed example use-case
-
Mike Cluck over 7 yearsPresumably,
MyControllerResult
still expectsplainResult
to have certain properties, right? Why don't you create a type definition for that? -
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 ofMyControllerResult
. ActuallyMyControllerResult
is aknockoutjs
viewmodel. -
Gromski over 7 yearsLast 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 over 7 yearsWhat 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 believeObject
is one of them (thus you'll need to modify the native type lib). -
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 over 7 yearsI keep using
any
half as a pun, and half as a hint that that may be the only real type applicable here. :) -
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 howPlainObject
is useful here. -
toskv over 7 yearshow about something like this?typescriptlang.org/play/…
-
Zoltán Tamási over 7 yearsI 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 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 over 7 yearsI 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 over 7 yearsOkay, 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 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 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 over 7 yearsThe 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 tonoImplicitAny
, I don't have that option, and I can't even turn it for in the current codebase. -
Nitzan Tomer over 7 yearsWhy 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 {}
thenclass B extends A {}
andlet a: A = new B()
. -
Zoltán Tamási over 7 yearsNo, 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 over 7 yearsYes, 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 asObjectConstructor
? -
Zoltán Tamási over 7 yearsNevermind, I've figured it out I guess. It needs to be a
Function
to allow any class instance to still be anObject
. -
Nitzan Tomer over 7 yearsWell, 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 thelib.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 over 7 yearsLet us continue this discussion in chat.
-
Zoltán Tamási over 7 yearsHere is the related TS ticket: github.com/Microsoft/TypeScript/issues/13866
-
DanielM about 6 yearsKeep 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 about 3 yearsThank 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 almost 3 yearsNote that
symbol
is not a part of JSON.