Goga Koreli
Goga Koreli

Reputation: 2947

How to automatically apply argument to class constructor?

I have an es6 class User and a global function map() given below:

class  User {
  constructor(public name: string) {}
}

const map = <T, R>(project: (value: T) => R) => {}

Instead of writing the following:

map((value) => new User(value))

I want to (somehow) write something like:

map(new User)

I am not sure if this is possible or not.

Upvotes: 11

Views: 905

Answers (6)

cypherfunc
cypherfunc

Reputation: 2659

If you want a generic solution, and can't modify the classes, you could use a higher-order function:

function make(klass) {
    return (...params) => new klass(...params);
}

// this:
map((value) => new User(value));

// becomes this:
map(make(User));

Upvotes: 1

zhirzh
zhirzh

Reputation: 3449

This is a well known "problem" in the oop vs fp debate in JS - class constructors vs function constructors

since es6 classes need the new operator, it is impossible to write something like map(new User)

you need a wrapper around the class constructor that creates instances via a function call. IMO, @baboo's approach is way to go

class MyClass {
  // ...

  static create(...args) {
    return new MyClass(...args)
  }
}
const a = new MyClass('hello', [])
const b = MyClass.create('world', 123])

you can read more about the problems of new here.

also, checkout daggy - Library for creating tagged constructors

Upvotes: 2

Karol Majewski
Karol Majewski

Reputation: 25790

The pattern you are describing is called a scope-safe constructor. It can be implemented by overloading a constructor so it works with and without the new keyword.

interface User {
  name: string;
}

interface UserConstructor {
  new (name: string): User;
  (name: string): User;
}

The same trick is used for global objects like Array or Date.

We need to recognize whether the new keyword was used:

const User = function (this: User | void, name: string): User {
  if (!(this instanceof User)) {
    return new User(name);
  }

  this.name = name;
  return this;
} as UserConstructor;

Your class has just become new-agnostic.

console.log(
  new User('Bob'),
  User('Alice'),
);

Which enabled us to write:

['Alice', 'Bob'].map(User); // $ExpectType User[]

Upvotes: 2

Nina Scholz
Nina Scholz

Reputation: 386578

You could add a check with new.target if the function is called without new and call then function with new.

function Person(name) {
    if (!new.target) return new Person(...arguments);
    this.name = name;
}

var names = ['Jane', 'Dan', 'Grace', 'Paul'],
    instances = names.map(Person);

console.log(instances);

Upvotes: 2

Baboo
Baboo

Reputation: 4258

You can create a static function in your class that takes the value param and returns a new User:

class User {
  static createUser(value) {
    return new User(value)
  }
}

And then use:

map(User.createUser)

Upvotes: 6

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249636

You can't do it directly. If you control the target function (ie it's not the stock map function) you can have it take a constructor instead of the function:

class User { constructor(private id: number) { }}
function map<TIn, T>(value: TIn, ctor: new (a: TIn) => T): T{
    return new ctor(value)
}
map(10, User)

Another more flexible solution is to use a helper function that transform the constructor into the desired function, although it's not much shorter then the original version:

class User { constructor(private id: number) { }}
function ctor<TIn, T>(ctor: new (a: TIn) => T): (value: TIn) => T{
    return value => new ctor(value)
}
[10, 11].map(ctor(User));

Upvotes: 5

Related Questions