4

Is there a syntax/API/idiom in JavaScript to get a function that instantiates a type, for any type?

That is, replace a call to new Type(..args) with, for instance, (pseudocode) Type.new(...args) so that I can pass the function object as a parameter?

I've tried using const newTypeAsFn = new Type on its own (without parentheses), but it still results in a constructor call instead of returning a function object that can be passed as a parameter and called later.

This is an example of what I wanted to be able to do:

const newStringAsFn = String.new;
[1, 2, 3].map(newStringAsFn);
// [String {"1"}, String {"2"}, String {"3"}]

I came up with a function that does that:

const newAsFn = (type) => (...args) => new type(...args);

That can be used like this:

const newStringAsFn = newAsFn(String);
[1, 2, 3].map(newStringAsFn);

Is there a way to do that without using this (possibly buggy) function I made up? A convention? Popular library method?

agentofuser
  • 8,987
  • 11
  • 54
  • 85
  • what is `[String {"1"}, String {"2"}, String {"3"}]`? You can do `[1, 2, 3].map(String);` – ASDFGerte Apr 20 '18 at 22:03
  • Personally, I like what you have here already. I don't think there's a "better" way but of course that's subjective. You might try asking on codereview.stackexchange.com, I think this is too opinionated of a question for this site. – CRice Apr 20 '18 at 22:05
  • 1
    @ASDFGerte That won't work for a user-defined class, eg: trying that with `class Foo {}`, would create a type error. – CRice Apr 20 '18 at 22:06
  • 1
    @ASDFGerte But you have to be [**careful**](https://stackoverflow.com/questions/46230125/) with that. Also `String(1)` is not the same as `new String(1)`. – ibrahim mahrir Apr 20 '18 at 22:07
  • Yes, i just didn't understand the commented line for the desired output and wondered if it was `["1", "2", "3"]` and just written in some chrome console shenanigan way. At the time i didn't understand what the question actually was. Also the being careful part applies to the code in the question in just the same way. – ASDFGerte Apr 20 '18 at 22:16
  • 1
    If I might ask... why do you need this? I can't think of the use-case for this at all, so it would be super insightful to know if there really was one. – Mike 'Pomax' Kamermans Apr 20 '18 at 22:36
  • 1
    @ibrahimmahrir great tip on being careful with `map`. I wasn't aware it passed more than one argument to the supplied function. – agentofuser Apr 23 '18 at 17:52
  • @Mike'Pomax'Kamermans this might be kind of a dumb way to do it, but I'm type-checking a json response, and I wanted to write a function like this: `const isDateFromString = fp.compose(_.isDate, newAsFn(Date));` – agentofuser Apr 23 '18 at 17:53
  • 1
    yeah if schema validation is the reason, then something like [joi](https://www.npmjs.com/package/joi) makes a lot more sense. – Mike 'Pomax' Kamermans Apr 23 '18 at 23:31

2 Answers2

4

With all the usual caveats, you could extend Function.prototype with a getter property to give you the syntax you seem to want.

// Your 'new' getter
Object.defineProperty(Function.prototype, "new", {
   get: function() {
     return (...args) => new this(...args)
   }
})


// Call it just like you show. It returns a function that invokes `new String`
const newStringAsFn = String.new;
const res = [1, 2, 3].map(newStringAsFn);

console.log(res);

The arrow function lets you use the outer this value, which is the function on which the new getter was invoked. So it calls it with new and forwards on the args.


You can also implement this using Reflect.construct.

// Your 'new' getter
Object.defineProperty(Function.prototype, "new", {
   get: function() {
     return (...args) => Reflect.construct(this, args)
   }
})


// Call it just like you show. It returns a function that invokes `new String`
const newStringAsFn = String.new;
const res = [1, 2, 3].map(newStringAsFn);

console.log(res);
  • I guess `Reflect.construct` is almost what I was looking for, except I wanted it to be auto-curried and variadic. Your solution is great and fits the example, so I'm accepting it. I prefer not to monkey-patch things though. I was hoping a library like `lodash` would have a wrapper that does what `newAsFn` does and I just happened to not know how to search for it, but if there's nothing wrong with my `newAsFn` I guess I'll use that. – agentofuser Apr 23 '18 at 18:06
  • 1
    Yeah, `Reflect.construct` not being variadic was a little disappointing. If it was, you could do `Reflect.construct.bind(null, String)`, though it's a little long. Your function is fine. Only disadvantage is if you accidentally invoke it with something other than a function. –  Apr 23 '18 at 18:24
2

I think the right approach is using your newAsFn. And to make it even safer, you can make use of some higher-order-functions and closures:

let newAsFn = (type, numberOfArgs) =>
    (...args) => new type(...args.slice(0, numberOfArgs));

So that you can controll how many args to be passed to the constructor on creation. Usage will be something like this:

let strings = [1, 2, 3].map(newAsFn(String, 1));

let newArray = newAsFn(Array, Infinity);
let arr1 = newArray(1, 2, 3, 4, 5),
    arr2 = newArray("Hello", "world", "!");
ibrahim mahrir
  • 31,174
  • 5
  • 48
  • 73