44

I have a class where I have the constructor defined with 3 parameters which are all optional. I was hoping to be able to pass in named parameters so I don't need to pass in undefined.

constructor(year?: number,
            month?: number,
            date?: number)

I was hoping to create an intance of the class like so

  const recurrenceRule = new MyNewClass(month: 6)

but it didn't work and i tried

  const recurrenceRule = new MyNewClass(month = 6)

and that didn't work.

The only way i got it to work was either

  const recurrenceRule = new MyNewClass(undefined, 4)

or

  const recurrenceRule = new MyNewClass(, 4)

But it seems so messy, I was hoping to pass in named arguments and becasue they are all optional I should be able to just pass in 1 - right ?

Martin
  • 23,844
  • 55
  • 201
  • 327

6 Answers6

67

You can use Object destructuring introduced in ES6 to archieve the desired behavior: reference. TypeScript is able to transpile this feature for usage with ES5 to target older browsers. However, as of ES6, this is also perfectly valid JavaScript.

Basically, it looks like this: constructor({ year, month, day}) and is invoked, for instance, as new Bar({ year: 2017 }). Then you can access year as a variable within the constructor, e.g. for assigning this.year = year.

More interesting than that is the usage with default values, e.g.

constructor({ year = new Date().getFullYear(), 
              month = new Date().getMonth(), 
              day = new Date().getDay()
            } = {})

which allows invoking the constructor with 0, 1, 2 or 3 parameters respectively (see snippet below).

The somewhat cryptic = {} is for the case when you create a new instance without any parameters. First, {} is used as the default value for the parameter object. Then, since year is missing, the default value for that one is added, then for month and for day respectively.

For usage with TypeScript, you can, of course, add additional typings,

constructor({ year = new Date().getFullYear(),
              month = new Date().getMonth(),
              day = new Date().getDay()
}: { year?: number, month?: number, day?: number } = {}) { 
    ...                
}

Although this really looks cryptic.

class Bar {
  constructor({ year, month, day }) {
    this.year = year;
    this.month = month;
    this.day = day;
  }
  
  log () {
    console.log(`year: ${this.year}, month: ${this.month}, day: ${this.day}`);
  }
}

new Bar({ day: 2017 }).log();

class Foo {
  constructor({ year = new Date().getFullYear(), 
                month = new Date().getMonth(), 
                day = new Date().getDay()
              } = {}) {
    this.year = year;
    this.month = month;
    this.day = day;
  }
  
  log () {
    console.log(`year: ${this.year}, month: ${this.month}, day: ${this.day}`);
  }
}

console.log('with default value:');
new Foo().log();
new Foo({ day: 2 }).log();
new Foo({ day: 2, month: 8 }).log();
new Foo({ year: 2015 }).log();
SVSchmidt
  • 6,269
  • 2
  • 26
  • 37
19
class Bar {
  constructor({a, b}: {a?: number, b?: number}) {}
}

new Bar({b: 1})

For more information, see ES6 Object Destructuring with functions.

elm
  • 351
  • 2
  • 3
  • 1
    I like this solution – John Tribe Oct 08 '20 at 14:32
  • Doesn't work. A `console.log()` on `new Bar({b: 1})` gives `Bar {}` – TBG Sep 08 '22 at 13:24
  • 3
    @TBG You still have to assign the parameters in the constructor like `this.b = b` if you want them to appear as properties on the class. A shorthand for this would be nice since it's what you often want, but Typescript is not designed for concision. – elm Sep 09 '22 at 17:25
5

Simple parameter:

constructor (private recurrenceSettings: {year?: number, month?: number, date?: number})

The private keyword instantiates argument as an instance variable, saving you needing to instantiate in the constructor. Can also be public if you want to instatiate public properties.

Use like this:

const recurrenceRule = new MyClass({month: 12})

Or use destructuring (usage same as above):

constructor({day, month, year}: {day?: number, month?: number, year?: number})

The above version loses the ability to use the private/public shortcut for instance variables though (see https://github.com/Microsoft/TypeScript/issues/5326).

Paul Grimshaw
  • 19,894
  • 6
  • 40
  • 59
4

You could also use Partial to achieve the same result.

You would instantiate the same way as in the previous answers.

class Bar {
  year = new Date().getFullYear();
  month = new Date().getMonth();
  day = new Date().getDay();
  constructor(bar?: Partial<Bar>) {
    Object.assign(this, bar);
  }
  
  log () {
    console.log(`year: ${this.year}, month: ${this.month}, day: ${this.day}`);
  }
}

new Bar().log();
new Bar({ day: 2 }).log();
new Bar({ day: 2, month: 8 }).log();
new Bar({ year: 2015 }).log();

Note 1. The Partial<Bar> will accept anything as long as the properties of the same name have the same type. Yes, this somewhat violates type-safety. The reason this is still powerful is that if you start typing an object literal with {, the intellisense will tell you what properties you can initialize.

Note 2. The Object.assign will disregard readonly properties since it is a JavaScript-specific thing, not TypeScript-specific. This means that while you can't assign a readonly property a new value, you certainly can with Object.assign.

pyzon
  • 59
  • 3
2

It seems this still isn't as easily possible as it could be, though it would be nice.

But the solution only took a few minutes.

Here's what I did.. I used this fantastic answer to get a list of properties minus methods.

export class Member {
    private constructor(public memberID: number = -1,
        public email: string = '',
        public loggedIn: boolean = false) {
    }

    static newByNamed(obj: NonFunctionProperties<Member>) {
        Object.assign(new Member(), obj);
    }
    
    public clear() {
        this.memberID = config.nonExistentID;
        this.email = '';
        this.loggedIn = false;
    }
}

type NonFunctionPropertyNames<T> = {
    [K in keyof T]: T[K] extends Function ? never : K
  }[keyof T];

type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;

And then I can call it with

let member = Member.newByNamed({email = 'me@home'...});
Regular Jo
  • 5,190
  • 3
  • 25
  • 47
0

In some cases, it's just easier to have an interface and separate maker function. (For future readers.)


interface Foo {
  x: number
  y: number
}

function newFoo({x=5,y=10}) : Foo {return {x,y}}

const f = newFoo({x:100})
Luke Miles
  • 941
  • 9
  • 19