The config-interface constructor design pattern in TypeScript

These days it’s fairly common to see classes that require the initialization of numerost properties before you can use the class. Typically configuring a class takes place in a class’s constructor.

When a class requires the initialization of only one or two of a class’s properties, you can get away with applying the initialization values for the properties as parameters to the constructor, like so:

export class Person {
  firstName: string;
  lastName: string;
  constructor(firstName: string, lastName: string) {
    this.firstName = firstName;
    this.lastName = lastName;
}

Notice that the constructor takes two parameters: firstName and lastName.

Extended constructor properties

Where things get tricky is when you initialize a class that has a large number of properties. For example, consider the following Employee class:

export class Employee {
  firstName: string;
  lastName: string;
  department: string;
  hireDate: Date;
  supervisor: string;
  salary: number;
}

Do you create the following constructor?

export class Employee {
  firstName: string;
  lastName: string;
  department: string;
  hireDate: Date;
  supervisor: string;
  salary: number;
  constructor(
    firstName: string,
    lastName: string,
    department: string,
    hireDate: Date,
    supervisor: string,
    salary: number
  ) {
    this.firstName = firstName;
    this.department = department;
    this.hireDate = hireDate;
    this.supervisor = supervisor;
    this.salary = salary;
  }
}

If so, this means that another developer using your class must write the following code:

const employee = new Employee(
  'John',
  'Doe',
  'Accounting',
  new Date('2022/9/1'),
  'Mary Smith',
   97000
);

This can get unwieldy in no time at all. And, if the programmer is using an IDE, such as Visual Studio Code, that isn’t smart enough to perform parameter verification, there are a lot of opportunities to make typing errors.

As an alternative to a class with a large number of parameters in the constructor, you can define a constructor that takes only one parameter, and that parameter is a predefined interface.

For example, consider an interface named IEmployeeConfig that defined like so:

export interface IEmployeeConfig {
  firstName: string;
  lastName: string;
  department: string;
  hireDate: Date;
  supervisor: string;
  salary: number;
}

Next, use the interface as the initialization parameter for a constructor in an Employee class like so:

export class Employee {
  firstName: string;
  lastName: string;
  department: string;
  hireDate: Date;
  supervisor: string;
  salary: number;

  constructor(config: IEmployeeConfig) {
    this.validateConfiguration(config);
    this.firstName = config.firstName;
    this.lastName = config.lastName;
    this.department = config.department;
    this.hireDate = config.hireDate;
    this.supervisor = config.supervisor;
    this.salary = config.salary;
  }

  private validateConfiguration(config: IEmployeeConfig): void {
    // Put some validation behavior in here, but in the
    // meantime, throw an error until that behavior is written
    throw new Error('NOT IMPLEMENTED');
  }
}

As you can see, life just got a lot easier. Rather than pass a whole bunch of parameters around a number of times, the programmer using your code just has to initialize an instance of the IEmployeeConfig interface and then create an instance of the Employee object, like so:

const config: IEmployeeConfig = {
  firstName: 'John',
  lastName: 'Doe',
  department: 'Accounting',
  hireDate: new Date('2022/9/1'),
  supervisor: 'Mary Smith',
  salary: 9700,
};

const employee = new Employee(config)

The benefit of using an interface to configure an object is that overall the code is more concise and easier to manage. A lot of the work to ensure proper configuration is done at design time when the code is compiled. If change configuration must be changed, the change can be done in a way that is easier to manage. You change the structure of the configuration interface and then adjust the class code accordingly. The developer using your class can adjust the code on their end in a more structured manner because they’re not fiddling with a lot of constructor parameters, only the constructor interface.