Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suggestion: The object primitive type #1809

Closed
fdecampredon opened this issue Jan 26, 2015 · 17 comments
Closed

Suggestion: The object primitive type #1809

fdecampredon opened this issue Jan 26, 2015 · 17 comments
Labels
Help Wanted You can do this Suggestion An idea for TypeScript

Comments

@fdecampredon
Copy link

The object primitive type

This proposal describe a new primitive type for typescript object.

Use case

JavaScript core api contains some functions that takes object as parameter :

  • Object.getPrototypeOf
  • Object.getOwnPropertyDescriptor
  • Object.create
  • ...etc

Currently there is no way in typescript to prevent passing other primitive type (string, number, etc ...) to those functions.
For example Object.create('string') is a perfectly valid typescript statement, even if it will ends up with an error.
Creating a new object primitive type would allows to model more correctly the signature of those function, and every function that share similar constraint.

Type Checking

Assignability

The object type is the equivalent of {} minus the assignability of other basic type, that means that :

  • any other basic types are not assignable to object
  • any non-basic type is assignable to object
  • object is only assignable to {} and any

This behavior is coherant to how javascript works, the type represent every value that respect the constraint typeof value === 'object' plus undefined.

Type guard

A new type guard has to be introduced for object : typeof value === 'object'.
Optionally the compiler could also accept comparaison with the Object function cast : Object(value) === value.

@fdecampredon fdecampredon changed the title Siggestion: The object primitive type Suggestion: The object primitive type Jan 26, 2015
@RyanCavanaugh RyanCavanaugh added In Discussion Not yet reached consensus Suggestion An idea for TypeScript labels Jan 26, 2015
@RyanCavanaugh
Copy link
Member

It'd be useful to list some APIs other than the Object. functions which would benefit from this.

@fdecampredon
Copy link
Author

In the javascript core api except from Object only perhaps some es6 constructs will benefit from this (WeakMap, Proxy etc).
But for example every underscore collections function would also gain better typing, framework like React use a 'setState' method that accept only object or null, ImmutableJS, Mori etc etc.

Edit: underscore collection work with any types

@RyanCavanaugh
Copy link
Member

Discussed at suggestion review meeting. This all makes sense, but we need to justify the complexity of a new primitive type with the number of errors / richness of description it is able to provide. Basically, more examples of APIs that can't operate on primitives, especially ones that aren't "expert" APIs (e.g. Proxies)

@RyanCavanaugh
Copy link
Member

Posting more as reference than recommendation 😉

interface String { _primitiveBrand?: string; }
interface Boolean { _primitiveBrand?: boolean; }
interface Number { _primitiveBrand?: number; }

interface ObjectOnly {  _primitiveBrand?: void; }

function fn(x: ObjectOnly) { }

// Error
fn(43);
// Error
fn('foo');
// OK
fn({});
// OK
fn(window);

@sccolbert
Copy link

+1 on this proposal. I wouldn't consider WeakMap an "expert" API, but I do consider it reason enough alone to implement this. Since the runtime makes a distinction between object types and primitive types, it only makes sense to have a facility in TypeScript to make the distinction as well.

This proposal will also solve the problem of typing "object bags" where each property is optional, but other primitive types should be disallowed.

@sccolbert
Copy link

Object.observe requires a non-primitive target as well: http://arv.github.io/ecmascript-object-observe/#Object.observe

@rotemdan
Copy link

I think the title is a bit confusing since it mentions the word "primitive". This could alternatively be called the "ECMAScript object type" or the "Runtime object type". Which would be assignable from anything other than primitive types (including undefined type and symbols), function or constructor types. The criteria for what is a legal object type should be based on the following guard:

let isObject (x) => typeof x === "object";

According to MDN, this is the output of typeof:

Undefined: "undefined"
Null: "object" (see below)
Boolean: "boolean"
Number: "number"
String: "string"
Symbol (new in ECMAScript 2015): "symbol"
Host object (provided by the JS environment): Implementation-dependent
Function object (implements [[Call]] in ECMA-262 terms): "function"
Any other object: "object"

Without this type, there is no way to correctly match a precise type for the above mentioned guard, not even as a user-defined guard. This is also essential for use with any function that strictly requires an object that has properties and allows for in loops and calls to the Object prototype, and for implementing generic constraints like T extends object and then safely referencing properties or using the prototype functions.

Having functions and constructors excluded, though, would be a bit limiting in some cases, so there could be another type that would include them as well and called the "non-primitive object type". A partial workaround could be the union object | Function though casts or additional guards would be needed to handle it correctly.

@RyanCavanaugh
Copy link
Member

Accepting PRs. This seems to be a common problem; this will be a breaking change for anyone brave enough to have written interface object { ... }. Implementation should be straightforward and follows the rules outlined above.

Side note: We're all wondering where @fdecampredon has been!

@fdecampredon
Copy link
Author

@RyanCavanaugh Change of town of work, and unfortunately not a lot of typescript in my new job :D

@falsandtru
Copy link
Contributor

Great. I want to use this type in TS1.8.

@mcmath
Copy link

mcmath commented Jun 27, 2016

I would also like to see something like an object type. However, I question whether it should conform to typeof value === 'object', as this would exclude functions. Instead, it seems to me that the prospective object type should reflect the Object language type, which includes functions. There are a few reasons for this:

  • APIs that accept plain objects always, as far as I know, also accept functions.
  • Functions inherit from the Object constructor.

I'll take these in order.

APIs

APIs such as WeakMap that only take objects accept the Object language type, not just plain objects. The following is ok:

function theAnswer() {}

let map = new WeakMap();

map.set(theAnswer, 42);
console.log(map.get(theAnswer)); // 42

This is as true for custom APIs as it is for built-in APIs. If I expect an object, I usually just want a bundle of properties. Functions are just callable bundles of properties.

Inheritance

Since Function extends the Object constructor, we can use the static and instance methods available on Object on functions as well. For example, the following is possible:

class DeepThought {
  static getAnswer() {
    return 42;
  }
}

let computer = Object.create(DeepThought);

console.log(computer.getAnswer()); // 42
console.log(Object.getPrototypeOf(computer)); // [Function: DeepThought]

While the example above is a bit silly, it just illustrates that APIs that use the Object methods internally could just as well accept functions.

So I would suggest that the object type conform to the following, which corresponds to the Object language type (except that it includes null, but there's no guarding against null in TypeScript in any case).

function isObject(value) {
  return typeof value === 'object' || typeof value === 'function';
}

Use Case

I have a project called Deep Map that recurses through the key-value pairs of nested objects and transforms primitive values along the way. As such, it distinguishes between primitive and non-primitive types. I had to define a custom NonPrimitive interface, as follows:

interface NonPrimitive {
  [key: string]: any;
  [index: number]: any;
}

export class DeepMap {
  // ...
  private mapObject(obj: NonPrimitive): NonPrimitive { /* ... */ }
}

This is not the first time I've had to define the NonPrimitive interface. It would be nice if I could just do this:

export class DeepMap {
  // ...
  private mapObject(obj: object): object { /* ... */ }
}

As I suggested above, I don't really care whether the obj parameter is callable or not. All that matters is that it is of the Object language type, i.e., that it is a bundle of properties whose key-value pairs I can iterate through.

@RyanCavanaugh
Copy link
Member

I would agree that functions are objects. The intent is to exclude primitives.

@mcmath
Copy link

mcmath commented Jun 28, 2016

Ok, I thought that must have been the intent. I just thought maybe I was missing something with all this talk of typeof value === 'object'.

@jdanyow
Copy link

jdanyow commented Jul 31, 2016

would this object type allow ad-hoc property assignment (like any)? For example:

const foo: object = {};

foo.bar = 'baz';

@RyanCavanaugh
Copy link
Member

No

@trusktr
Copy link
Contributor

trusktr commented Jan 30, 2017

What's the practical difference between

export class DeepMap {
  // ...
  private mapObject(obj: object): object { /* ... */ }
}

and

export class DeepMap {
  // ...
  private mapObject(obj: Object): Object { /* ... */ }
}

? (Using object vs Object). It seems to me that a Function extends from Object, so I don't see why we can't already do that. Can you explain?

@mcmath
Copy link

mcmath commented Jan 30, 2017

@trusktr All values other than null and undefined are assignable to the Object type; a function that accepts an Object will take a string, number, boolean, or symbol. Only non-primitive values are assignable to object; passing a string, etc, will produce a compile-time error. The object type is useful where we expect a non-primitive value, but where we don't care what its properties are.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Help Wanted You can do this Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

9 participants