Skip to content

Instantly share code, notes, and snippets.

@ephys
Last active October 8, 2018 11:36
Show Gist options
  • Save ephys/8fa91ad03137f3b9ad5bd61c51de2c20 to your computer and use it in GitHub Desktop.
Save ephys/8fa91ad03137f3b9ad5bd61c51de2c20 to your computer and use it in GitHub Desktop.

List of things I want in EcmaScript

Frozen Object literal

Flow syntax {| <data> |} could be used in EcmaScript too to mean Object.freeze({ <data> })

Reflect.isOfBuiltinType(obj, type)

Or maybe called crossRealmnInstanceof. The objective is to have a way to determine if an object is an instance of a built-in even cross-realmns.

If I do Reflect.isOfBuiltinType(iframe.contentDocument, Document) it should return true even though iframe.contentDocument instanceof Document will return false because they are from different realmns

Chained exceptions

Issue

Sometimes you need to propagate an exception with some added context specific to your logic.

Such an example would be

function parseUserInput(data) {
  // <snipped />
  
  parsed = JSON.parse(data);
  
  // <snipped />
}

But if the data is invalid, the developer might receive an error "Failed to parse JSON" without much context.

A solution would be to catch the error and add some information relevant to the function.

function parseUserInput(data) {
  // <snipped />
  
  try {
    parsed = JSON.parse(data);
  } catch (e) {
    throw new Error('Could not parse user data. Make sure it\'s JSON!');
  }
  
  // <snipped />
}

But this hides the underlying Error which can also contain valuable information. Logging the previous error works but is a subpart experience for the developper.

Proposed change

Allow specifying which error caused the new one to throw.

function parseUserInput(data) {
  // <snipped />
  
  try {
    parsed = JSON.parse(data);
  } catch (e) {
    // possibly
    throw (new Error('Could not parse user data. Make sure it\'s JSON!')).causedBy(e);
  }
  
  // <snipped />
}

/* 
output:

Uncaught Error: Expression <[name='english-test-exempt']> == 'y': Could not parse expected value 'y'. It must be valid JSON.
  at ...
  at ...
  ...
Caused By SyntaxError: Unexpected token ' in JSON at position 0
  at ...
  at ...
  ...
*/

undefined coalesce operator (??)

Edit 1: A strawman for this idea has actually been proposed 4 years ago. Unfortunately, the conversation seems the have died out

Edit 2: Proposed https://esdiscuss.org/topic/proposal-for-a-null-coalescing-operator

Edit 3: Actual active proposal https://github.com/tc39/proposal-nullish-coalescing (works on both null & undefined)

We already have a very simple way to do what this operator does,

// val will be equal to DEFAULT_VAL if val is undefined.
const val = options.val || DEFAULT_VAL;

The problem is that the || operator will return the value of the second operand if the first one is falsy. Which is far from ideal for default values. Forcing developers to fallback to the much more verbose

// val will be equal to DEFAULT_VAL if val is falsy.
const val = options.val === void 0 ? DEFAULT_VAL : options.val;

As the first syntax works for most cases, it tends to be overused and can be find in places where it will cause problems.
The undefined coalesce operator, being as easy to use as the boolean OR operator, would fix that problem.

// val will be equal to DEFAULT_VAL if val is undefined.
const val = options.val ?? DEFAULT_VAL;

??= operator

Edit: See previous point.

options.val ??= DEFAULT_VAL;
// being equivalent to
options.val = options.val ?? DEFAULT_VAL;
// which itself is equivalent to
options.val = options.val === void 0 ? DEFAULT_VAL : options.val;

Sane number parsing

See this: https://gist.github.com/Ephys/4d9731d5c7a945dcc752d279eaeaa08f

I'v been beaten to it: https://github.com/mathiasbynens/proposal-number-fromstring

Add literals NaN and undefined

Just like null is, to replace global variables global.undefined and global.NaN

Documentation: https://tc39.github.io/ecma262/#sec-reserved-words

object destructured assignement

SomeClass.prototype.{ dependencies, enable } = mixin;

Desugars to

SomeClass.prototype.dependencies = mixin.dependencies;
SomeClass.prototype.enable = mixin.enable;

A complementary version is being discussed on the ES mailing list https://esdiscuss.org/topic/extended-dot-notation-pick-notation-proposal

Having a pick operator seems like an important feature to avoid cases like this one https://github.com/sebmarkbage/ecmascript-rest-spread/blob/master/Issues.md

const aVar = someObj.{ a, b };

Desugars to

const aVar = {
  a: someObj.a,
  b: someObj.b
};

And a similar syntax proposes the following:

obj.{
   a: 1,
   [Symbol.equal]() { return true; },
   c() {}
};

which desugars to

obj.a = 1;
obj[Symbol.equal] = function() { return true; };
obj.c = function c() {};

That syntax might not be compatible with the previous one as the following would be ambiguous:

const a = 1;
const b = 2;

const result = someObj.{ a, b };

Is this assigning a and b to someObj or creating an object result equal to { a: someObj.a, b: someObj.b } ?

Deprecate and Replace Object.toJSON with Object[Symbol.toJSON]

Possible way it could be retrocompatible:

Symbol.toJSON = Symbol('toJSON');

Object.prototype[Symbol.toJSON] = function toJSON() {
  if (this.toJSON) {
    return this.toJSON();
  }
  
  return this;
}

const a = {};
a[Symbol.toJSON](); // a

const b = {
  toJSON() {
    return 'toJSONed';
  }
}

b[Symbol.toJSON](); // 'toJSONed'

Another way would be that assigning a toJSON method to any object also magically assigns [Symbol.toJSON] (unless it was already present on the object) which does nothing but call this.toJSON.

Deprecate Function.name and replace it with Function[Symbol.name]

It would allow classes to have a static property called name.

Symbol.name = Symbol('name');

Object.defineProperty(Function.prototype, Symbol.name, {
  get() {
    return this.name;
  },

  set(name) {
    this.name = name;
  }
})

(function test() {})[Symbol.name]; // 'test'

Standardized Equals and Compare

Useful for Object comparison as they might require some custom logic (Date, Buffer, RegExp, etc).

a[Symbol.equals](b)
a[Symbol.compare](b)
new Date('05 june 2016') === new Date('05 june 2016'); // false
new Date('05 june 2016')[Symbol.equals](new Date('05 june 2016')); // true

/aRegexp/ === /aRegexp/; // false
/aRegexp/[Symbol.equals](/aRegexp/); // true

Function.isCallable and Function.isClass

Edit: A proposal for Reflect.isCallable and Reflect.isConstructable (much better names!) was created a year ago, conversation seems to have died out again.

Function.isCallable - To have a clean way to check if an object has a [[call]] internal method.
Function.isClass - To have a clean way to check if an object has a [[Construct]] internal method.

New left-hand operand for typeof

The idea is to harmonise the syntax of typeof and instanceof. This follows the idea that typeof should be used for primitives and instanceof for objects.

// It would work like this:
aVar typeof aType;
Symbol('aSymbol') typeof 'symbol' // evaluates to true
// this syntax would be equivalent to
typeof Symbol('aSymbol') === 'symbol';

// We could also perhaps support using the primitive wrappers as the second-hand operand. Probably a terrible idea though
// The following expressions would all evaluate to true:
'string' typeof String
Symbol() typeof Symbol
4 typeof Number
true typeof Boolean
{} typeof Object
Object.create(null) typeof Object
void 0 typeof undefined 
null typeof null

!instanceof, !in, !typeof

if (x !instanceof Date);
if (x !in someObject);
if (x !typeof Number);

Desugars to

if (!(x instanceof Date));
if (!(x in someObject));

// if (typeof x !== 'number'); // Never said all of my ideas were brilliant.
if (!(x typeof Number));

Module Top Level await support

Edit: This has been discussed and is on hold tc39/proposal-async-await#9

Just an under-developed idea.

// app.js
import Database from './database';
import Server from './server';

await Database.init(); // Pause module execution until promise has either resolved or rejected.
await Server.run();

export function stop() {
    Server.stop();
}
// index.js - Actual entry point
import { stop } from './app'; // Import is resolved once module is done executing. No more async import hell.

stop();

Allow naming the destructured parameter

function namedUnpackedParameters(parameters: { queryParameters = {}, pathParameters = {} } = {}) {
	console.log('parameters = ', parameters);
	console.log('queryParameters = ', queryParameters);
	console.log('pathParameters =', pathParameters);
}

namedUnpackedParameters();
// OUTPUT:
// parameters = { queryParameters: {}, pathParameters: {} }
// queryParameters = {}
// pathParameters = {}

namedUnpackedParameters({ cors: false, pathParameters: { id: 4 } });
// OUTPUT:
// parameters = { cors: false, queryParameters: {}, pathParameters: { id: 4 } }
// queryParameters = {}
// pathParameters = { id: 4 }

Desugars to

function namedUnpackedParameters(parameters = {}) {
	const { queryParameters = {}, pathParameters = {} } = parameters;

	console.log('parameters = ', parameters);
	console.log('queryParameters = ', queryParameters);
	console.log('pathParameters =', pathParameters);
}

Mixins

class B mixes Observable extends A {

}

Named Arrow Functions

Currently, arrow functions cannot have names. Which is slightly annoying in some cases.

const onError = e => {
  img.removeEventListener('error', onError);
  img.removeEventListener('load', onLoad);
  
  // handle error
  this.submitError(e);
};

const onLoad = () => {
  img.removeEventListener('error', onError);
  img.removeEventListener('load', onLoad);
  
  // do stuff 
  this.addImage(img);
};

img.addEventListener('error', onError);
img.addEventListener('load', onLoad);

Naming them would enable us to do the following:

img.addEventListener('error', onError(e) => {
  img.removeEventListener('error', onError);
  img.removeEventListener('load', onLoad);
  
  // handle error
  this.submitError(e);
});

img.addEventListener('load', onLoad() => {
  img.removeEventListener('error', onError);
  img.removeEventListener('load', onLoad);
  
  // do stuff 
  this.addImage(img);
});

These functions would be block-scoped and cannot be redefined in the same scope. Would they be hoisted though ?

Optional function keyword even outside of object and class definitions

This is probably not web-compatible, but I'm writing it down anyway.

Instead of writing

function doTheThing() { /* ... */ }

doTheThing();

We could write

doTheThing() { /* ... */ }

doTheThing();

Or maybe

const doTheThing() { /* ... */ }

doTheThing();

Or even

let doTheThing() { /* ... */ }

doTheThing();

These later options would be possible and could solve a few issues. More research on the matter is required.

Enums

Possible Use Cases:

  • Having a counterpart to SQL Enums for Node.

The current way to implement enums in JavaScript is to just use object literals, as it looks and is as easy to use as enums.

const A = {
  ITEM_ONE = 0,
  ITEM_TWO = 1,
  ITEM_THREE = 2
}

Object.freeze(A);

This approach isn't brilliant for type systems however, as instanceof does not work on them, nor can you hint properly at what kind of argument is expected.

A proper approach would use classes. However, emulating the right enum behavior is so complicated and troublesome that nobody actualy does it.

The idea is to put the enum keyword into use to ensure the right behavior is respected.

enum A {
  ITEM_ONE('cat');
  ITEM_TWO('dog');
  ITEM_THREE('bird');

  // enums should be allowed to have custom properties
  constructor(type) {
    this.type = type;
  }
  
  // enums should be allowed to have methods
  bark() {
    console.log('bark!');
  }
}

A.ITEM_ONE.type; // 'cat'
A.ITEM_ONE.bark(); // prints 'bark!'
A.ITEM_ONE.num; // 0, id representing the item order in the enum list.
A.ITEM_ONE.name; // 'ITEM_ONE'

// enums should be immutable
A.ITEN_ONE.type = 'elephant'; // Throws

// non extensibles
class B extends A {} // does it throw here ?

// can't make new instances
new A() // throws!

Object.create(A.prototype); // should this throw or work ?
Object.setPrototypeOf({}, A.prototype); // should this throw or work ?

desugars to

// new global obj
class Enum {
    constructor(num, name) {
        if (new.target === Enum) {
            throw new TypeError('Enum cannot be instanciated directly');
        }

        this.num = num;
        this.name = name;
    }
}

const A = (function() {
  let __locked = false;
  let num = 0;
  class A extends global.Enum {
    constructor(name, id) {
      if (new.target !== A) {
        throw new TypeError('Cannot extend enum');
      }

      if (__locked === true) {
        throw new TypeError('Cannot instanciate enums');
      }
      
      super(num++, name);
      this.id = id;
      Object.freeze(this);
    }
  }

  Object.defineProperties(A, {
    ITEM_ONE: {
      enumerable: true,
      writable: false,
      configurable: false,
      value: new A('ITEM_ONE', 0)
    },
    
    ITEM_TWO: {
      enumerable: true,
      writable: false,
      configurable: false,
      value: new A('ITEM_TWO', 1)
    },
    
    ITEM_THREE: {
      enumerable: true,
      writable: false,
      configurable: false,
      value: new A('ITEM_THREE', 2)
    }
  });

  __locked = true;
  Object.freeze(A);
  Object.freeze(A.prototype);
  
  return A;
}());

Array Index Syntax

This is IMO a bad idea and I don't really want it because you should not have holes in your arrays but I wrote it down anyway

const i = 3;

const anArray = [
    2: 'apple',
    [i]: 'another apple',
];

anArray[0]; // undefined
anArray[1]; // undefined
anArray[2]; // 'apple'
anArray[3]; // 'another apple'

const anArray = [
    indexedItem: 'apple', // throws ?
];

Unify some Node & DOM APIs, including:

  • setTimeout,
  • setInterval
  • setImmediate,
  • nextTick,
  • atob (but renamed, because seriously)
  • btoa (same)
  • WhatWG fetch (deprecate http.request)
  • WhatWG Streams (deprecate node streams)
  • WhatWG URL (done)

Immutable parameter bindings

aka. const parameters. Fairly straigtforward.

Since ES6, most variable bindings can be immutable using const, except arguments (not the arguments binding itself though, that one is immutable in strict mode).

function aFunc(const aParam) {
  // Uncaught TypeError: Assignment to constant variable.
  aParam = 'another value';
}

for-else, while-else

Allow an else statement to follow a for statement. The else block gets executed if the loop did not iterate.

for (const item of myList) {
} else {
    // ELSE BLOCK
}

// desugars to

let iterated = false;
for (const item of myList) {
    iterated = true;
}

if (!iterated) {
    // ELSE BLOCK
}

Variants:

for (const key in myObj) {
} else {
}

for (let i = 0; i < x; i++) {
} else {
}

let i;
while (i < x) {
    i++;
} else {
}

let i;
do {
    i++;
} while (i < x) else {
}

Boolean XOR operator

We already have the bitwise ^ (xor) operator,
What about a boolean XOR operator?

true  XOR false  // => true
false XOR true  // => true
false XOR false // => false
true  XOR true   // => false

Note, that operator would behave basically like !== when used exclusively on booleans

true  !== false // => true
false !== true  // => true
false !== false // => false
true  !== true  // => false

But would have a different behavior on other types

0 !== ''  // => true
0 XOR ''  // => false (because Boolean(0) XOR Boolean('') is false XOR false)

Actually after some thinking I realised it would then be the same as the coercing !=

0 != ''   // => false
0 XOR ''  // => false

Date#toISODateString(), Date#toISOTimeString(), Date#toISODateTimeString()

We already have Date#toISOString(), but we often only need the date part of it, or the time part of it (mostly for the DOM datetime/datetime-local/time input types)

Whereas Date#toISOString() outputs the date with time and timezone (2017-11-29T10:33:08.708Z)

  • Date#toISODateString() would only output the date (2017-11-29)
  • Date#toISOTimeString() would only output the time (10:33:08.708)
  • Date#toISODateTimeString() would output the date and time without the timezone (2017-11-29T10:33:08.708)

Alternatively we could add an option object parameter to Date#toISOString():

Date#toISOString({
  time: true,
  date: false,
  timezone: false,
})

Allow rest parameter to not be the final parameter

There is a convention in node to put the callback as the final parameter. This is currenctly incompatible with the rest parameter as that parameter needs to be the final parameter.

// this is obviously a dummy example
function arrayify(...data, cb) {
  cb(data);
}

arrayify(1, 2, 3, (data) => {
  console.log(data); // outputs [1, 2, 3]
});

There is currently no way to achieve this without using arguments.

Allow rest element to not be the final element

Same as above, for iterable destructuring:

const [a, ...b, c] = [1, 2, 3, 4, 5]; // a = 1, b = [2,3,4], c = 5
const [a, ...b, c] = [1, 2, 3]; // a = 1, b = [2], c = 3
const [a, ...b, c] = [1, 2]; // a = 1, b = [], c = 2
const [a, ...b, c] = [1]; // a = 1, b = [], c = undefined
const [a, ...b, c] = []; // a = undefined, b = [], c = undefined

How it could work: https://gist.github.com/Ephys/2c7cd3a2afdea4e3ad2bc7910f4d3281

await.<identifier> <something> as an alias for await Promise.<identifier>(<something>);

Examples:

// await Promise.all([...])
await.all [
	getPost(7);
	getPostComments(7);
];

// await Promise.race([...])
await.race [
	getPost(7);
	getPostComments(7);
];

// await Promise.reject(error)
await.reject error; // = throw error;

// await Promise.resolve(7)
await.resolve 7; // = await 7;

// as it's just an alias, it also works with any method added to the Promise object.
Promise.resolve7 = function resolve7() { return Promise.resolve(7); }
await.resolve7; // => 7

Fix JavaScript weirdnesses

I know this is never going to happen.

Fix new Array()

new Array(number) creates an array with its length property set to number, whereas new Array with a non-number first parameter or more than one parameter creates an Array containing the constructor arguments.

Given that new Array(number) fills the array with empty items, that behavior should be removed.

Note: This has been mostly fixed using Array.from

Fix typeof

(Not happening https://esdiscuss.org/topic/typeof-null)

Typeof should return the name of the primitive type of the object.

// currently:

typeof 5 				// number
typeof 'hello' 			// string
typeof Symbol('Hello') 	// symbol
typeof false 			// boolean
typeof null 			// object
typeof void 0 			// undefined
typeof {} 				// object
typeof [] 				// object
typeof function () {}	// function
// With 'use typeof';
// returns the actual type

typeof 5 				// number
typeof 'hello' 			// string
typeof Symbol('Hello') 	// symbol
typeof false 			// boolean
typeof null 			// null
typeof void 0 			// undefined
typeof {} 				// object
typeof [] 				// object
typeof function () {}	// object
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment