Strawman proposal: new `is` operator

# Isiah Meadows (11 years ago)

There is no way currently to reliably verify types in JS. Type annotations cannot solve this completely, especially when you have an overloaded function. A way to reliably verify types would make this far easier. So far, here are the current options with their respective caveats:

  1. instanceof - This returns true for the class, but also for subclasses. This isn't always desired.

  2. this.contructor === Foo - The constructor property could easily be overwritten, rendering this relatively useless.

  3. typeof - This only is useful for literals, with the exception of null.

  4. Object.prototype.toString.call() - This returns the correct native object type for native objects, but returns [object Object] for all non-native objects, rendering it effectively useless. This includes object instances created from class Foo {}, etc.

This proposed operator, is, would basically use the same algorithm for the instanceof operator, but instead of walking up the prototype chain iteratively, it would only check the first level of the right side. The static and runtime semantics would be equal to that of the instanceof operator.

It would allow for simpler overloading, simpler assertions, simpler type checks, and simpler testing.

Example usage:

class Foo {}
class Bar extends Foo {}

const assert = (message, test) => {
  if (!test) {
    throw new TypeError(message);
  }
};

let obj = new Bar();

// none of these should throw
assert('obj is of type Bar', obj is Bar);
assert('obj is not of type Foo', !(obj is Foo));

One big question I have is whether a different keyword should be used (isa, etc), given its use as a keyword synonym to === in many compile-to-JS languages, most notably CoffeeScript.

# C. Scott Ananian (11 years ago)

On Sun, Aug 24, 2014 at 4:04 PM, Isiah Meadows <impinball at gmail.com> wrote:

There is no way currently to reliably verify types in JS. Type annotations cannot solve this completely, especially when you have an overloaded function. A way to reliably verify types would make this far easier. So far, here are the current options with their respective caveats:

  1. instanceof - This returns true for the class, but also for subclasses. This isn't always desired.

Your proposal fixes this, but does not address the other issues you raise:

  1. this.constructor === Foo - The constructor property could easily be overwritten, rendering this relatively useless.

Object.getPrototypeOf(this) === Foo.prototype is the test which instanceof uses. Does not require new syntax.

  1. typeof - This only is useful for literals, with the exception of null.

Your proposed is operator is not useful for literals.

  1. Object.prototype.toString.call() - This returns the correct native object type for native objects, but returns [object Object] for all non-native objects, rendering it effectively useless. This includes object instances created from class Foo {}, etc.

In general, you can always define:

Object.prototype.is = function(constructor) {
  return Object.getPrototypeOf(this) === constructor.prototype;
};

rather than try to use toString. Unless you want it to work on literals...

# Ian Hickson (11 years ago)

On Sun, 24 Aug 2014, Isiah Meadows wrote:

One big question I have is whether a different keyword should be used (isa, etc), given its use as a keyword synonym to === in many compile-to-JS languages, most notably CoffeeScript.

"is" is used to mean "is an instance of or an instance of a subclass of" in C# and modern Pascals, FWIW. Perl uses "isa" for this.

# Florian Bösch (11 years ago)

Python, Coffeescript and VB use the "is" operator for object identity, not for instanceof like functionality. Dart uses the is operator analogous to instanceof. It would seem to me it'd be beneficial to pick a different operator name, in order to avoid the fuddled meaning this operator has taken to mean in a variety of languages.

# Andrea Giammarchi (11 years ago)

I know it's just a sketch but that Object.prototype.is does not play well with Object.create where no constructor is necessary to create is relations.

# Isiah Meadows (11 years ago)

Cc the list...

# Brendan Eich (11 years ago)

Isiah Meadows wrote:

Cc the list...

On Aug 25, 2014 6:06 PM, "Isiah Meadows" <impinball at gmail.com <mailto:impinball at gmail.com>> wrote:

There really shouldn't be any sort of object construction needed
to check types like this. `isa` may be better, anyways, but I
still find that requirement to build and destroy an object to
check somewhat counterproductive.

What object is built and destroyed?

Note that for primitive types no wrapper need be created just to call a method, in general for JIT-optimized code, and definitely for strict mode code.

Andrea gets what I'm talking about. Also, another (possibly
separate) proposal would be to make cases like `"foo" instanceof
String" === true` instead of their current behavior, throwing a
TypeError. (I believe...I'm on a phone, not a PC where I can test.
Correct me if I'm wrong.)

(Your phone doesn't have a browser with a console? :-P)

js> "hi" instanceof String

false js> 42 instanceof Number

false js> false instanceof Boolean

false

These are well-defined for instanceof, without throwing. Changing results to true would be backward incompatible.

ES4 had 'is' as a type-classifying operator:

proposals:is_as_to, discussion:is_as_to

It would be a mistake to define 'is' without defining the (unsound) type system it depends on. This is a challenge, but TypeScript and other close-to-ES6 languages have sallied forth. We need a detailed proposal.

# Till Schneidereit (11 years ago)

On Tue, Aug 26, 2014 at 3:56 AM, Brendan Eich <brendan at mozilla.org> wrote:

ES4 had 'is' as a type-classifying operator:

proposals:is_as_to, discussion:is_as_to

It would be a mistake to define 'is' without defining the (unsound) type system it depends on. This is a challenge, but TypeScript and other close-to-ES6 languages have sallied forth. We need a detailed proposal.

Though TypeScript cannot support an is operator because it only has duck typing/structural types. Which makes sense for the web, I think, given that lots of code has to interact across realms, and staticly-checked types with their inherent anti-modularity are a problem for that. AS3 (which implements ES4's is operator) has the same issue across Flash's ApplicationDomains.

Note that ES6's formulation of instanceof in terms of an optional @@hasInstance trap can, in combination with the cross-realm symbol registry, probably be used to implement the desired behavior, even across realms. For non-primitives, at least.

# Isiah Meadows (11 years ago)

I stand corrected on the creation aspect.

Isiah Meadows

# Isiah Meadows (11 years ago)

Here's my (more formalized) proposition. I also have added a proposed @@isInstance property for the isa equivalent for @@hasInstance. I know this was mostly put on hold initially because of my lack of proper formulation, but here it is.

Rationale:

Include an isa operator to determine if an object is a direct instance of a constructor, and not simply a child constructor. It is also meant to work with native primitives as well, which the instanceof operator lacks. It helps when child constructors are not necessarily known, and a method specific to the parent is made (there is no way currently to determine this) or when implementing an abstract class that defines methods that must be overridden for all subclasses. Example usage:

// Example 1:

class Foo {
  constructor() {
    if (this isa Foo) throw new TypeError('Abstract class')
  }
}

class Bar extends Foo {}

let foo = new Foo(); // Error!
let bar = new Bar(); // Good

/****************************************************************************/

// Example 2:

class Foo {
  constructor() {}

  doSomething() {
    if (!(this isa Foo)) {
      throw new TypeError('Must be a Foo instance');
    }
    // do stuff
  }
}

class Bar extends Foo {}

let bar = new Bar();
bar.doSomething(); // Error!

/****************************************************************************/

// Example 3: (adapted from old code)

const assert = (obj) => {
  let isInstance = (b) => { if (!(a instanceof b)) throw new TypeError() };
  let isType = (b) => { if (!(a isa b)) throw new TypeError() };
  return {
    isInstance: isInstance,
    isType: isType,
  };
};

// ...

class WhileStatement extends Statement {
  constructor(condition, body, label = '') {
    super();
    assert(condition).isInstance(Expression);
    assert(body).isType(Array);
    body.forEach((i) => assert(i).isInstance(Expression));
    assert(label).isType(String); // native string
    this.condition = condition;
    this.body = body;
    this.label = label;
  }
}

// ...

class Foo extends String {
  constructor(val) {
    super(val);
  }
}

new WhileStatement(expr, [...elements], 'label') // Good
new WhileStatement(expr, [...elements])          // Good

new WhileStatement(stmt, [...elements], 'label') // Error!
new WhileStatement(expr, {}, 'label')            // Error!
new WhileStatement(expr, typedArray, 'label')    // Error! (before iterating)
new WhileStatement(expr, funcArray, 'label')     // Error!

/* Currently cannot make this throw */
new WhileStatement(expr, [...elements], new Foo('label')) // Error! (desired)


/****************************************************************************/

// Example 4: (Example 3 using Node builtins)
class WhileStatement extends Statement {
  constructor(condition, body, label = '') {
    super();
    assert(condition instanceof Expression);
    assert(body isa Array);
    body.forEach((i) => assert(i instanceof Expression));
    assert(label isa String); // native string
    this.condition = condition;
    this.body = body;
    this.label = label;
  }
}

// ...

class Foo extends String {
  constructor(val) {
    super(val);
  }
}

new WhileStatement(expr, [...elements], 'label') // Good
new WhileStatement(expr, [...elements])          // Good

new WhileStatement(stmt, [...elements], 'label') // Error!
new WhileStatement(expr, {}, 'label')            // Error!
new WhileStatement(expr, typedArray, 'label')    // Error! (before iterating)
new WhileStatement(expr, funcArray, 'label')     // Error!

/* Currently cannot make this throw */
new WhileStatement(expr, [...elements], new Foo('label')) // Error! (desired)

This already exists in some form in several languages, such as Ruby. It would be great for unit testing, library API type checking (e.g. options isa Object, options isa Map, code isa String), and it may lay some solid groundwork for this proposition to more easily get off the ground (internal implementation will become relatively easy).

Well-known Symbols:

Add this row to the table.

<!-- Apologies to those of you reading this text-only:

Please pardon my HTML...it's the only way I could get this to display right otherwise. I apologize...I've been using a text editor to type all this crap, with no more than automatic indention, brace detection and syntax highlighting. :(

I have made it a point to keep each line to a maximum of 79 columns so it can still be easily read in emacs/etc. in a popup terminal. -->

<table> <thead> <th>Specification Name</th> <th>[[Description]]</th> <th>Value and Purpose</th> </thead> <tbody> <tr> <td style="text-align:center">...</td> <td style="text-align:center">...</td> <td style="text-align:center">...</td> </tr> <tr> <td>@@isInstance</td> <td>"Symbol.isInstance"</td> <td style="width:40%">

A method that determines if a constructor object recognizes an object or primitive as of the same type as the constructor. Called by the semantics of the isa operator. </td> </tr> <tr> <td style="text-align:center">...</td> <td style="text-align:center">...</td> <td style="text-align:center">...</td> </tr> </table>

Syntax:

RelationalExpression:

RelationalExpression<sub>[?In, ?Yield]</sub> isa ShiftExpression <sub>[?Yield]</sub>

Static Semantics: IsFunctionDefinition

RelationalExpression:

RelationalExpression isa ShiftExpression

  1. Return false.

Static Semantics: IsValidSimpleTarget

RelationalExpression:

RelationalExpression isa ShiftExpression

  1. Return false.

Runtime Semantics: Evaluation

RelationalExpression isa ShiftExpression

  1. Let lref be the result of evaluating RelationalExpression.

  2. Let lval be GetValue(lref).

  3. ReturnIfAbrupt(lval)

  4. Let rref be the result of evaluating ShiftExpression.

  5. Let rval be GetValue(rref).

  6. ReturnIfAbrupt(rval).

  7. Return IsaOperator(lval, rval).

Abstract Operation: IsaOperator(O, C)

  1. If Type(C) is not Object, throw a TypeError exception.

  2. Let isaHandler be GetMethod(@@isInstance)

  3. ReturnIfAbrupt(isaHandler)

  4. If isaHandler is not undefined, then

  5. Let result be the result of calling the [[Call]] internal method of isaHandler with C passed as thisArgument and a new List containing O as its argumentsList.

  6. Return ToBoolean(result).

  7. If O is a primitive value, then

  8. If IsPrimitiveConstructor(C, O), return true.

  9. Return false.

  10. If IsCallable(C) is false, throw a TypeError exception.

  11. Return IsInstance(C, O).

Abstract Operation: IsInstance(C, O)

  1. If IsCallable(C) is false, return false.

  2. If C has a [[BoundTargetFunction]] internal slot, then

  3. Let BC be the value of C's [[BoundTargetFunction]] internal slot.

  4. Return IsaOperator(O, BC).

  5. If Type(O) is not Object, return false.

  6. Let P be Get(C, "prototype").

  7. ReturnIfAbrupt(P).

  8. If Type(P) is not Object, throw a TypeError exception.

  9. Set O to the result of calling the [[GetPrototypeOf]] internal method of O with no arguments.

  10. ReturnIfAbrupt(O).

  11. If O is null, return false.

  12. If SameValue(O) is true, return true.

  13. Return false.

Abstract Operation: IsPrimitiveConstructor(C, O)

  1. If C is not the same Object value as one of the following intrinsic constructors, return false:
  • %Boolean%

  • %Number%

  • %String%

  • %Symbol%

  1. If Type(O) and C are the same Object value, return true.

  2. Return false.