Implementing "Options Object" Pattern using TypeScript and Joi
The Options Object pattern [1][2] is a technique in JavaScript for configuring a component using a single function parameter:
const client = createClient({
host: 'localhost',
port: 12345,
username: 'foobar',
password: 'work-at-peachjar',
});
This approach has many benefits over using multiple parameters to configure a function or constructor:
Parameter order doesn't matter
function thisIsNoFun(host, port, username, password) {}
Simple to implement defaults
const defaults = { foo: 'bar' };
function createConnection(options) {
const opts = Object.assign({}, defaults, options);
}
Easier to build up options
const options = {
port: 54321,
};
if (process.env.HOST) {
options.host = process.env.HOST;
}
const client = createClient(options);
TypeScript supports the pattern with strongly-typed options:
type Options = {
host?: string,
port?: number,
username?: string,
password?: string,
}
function createClient(options: Options = {}) {
console.log(options.host);
}
You can even avoid needing to specify optional arguments using Partial<T> (which I prefer):
type Options = {
host: string,
port: number,
username: string,
password: string,
}
function createClient(options: Partial<Options> = {}) {
console.log(options.host);
}
Both approaches work but can lead to undefined values:
createClient(); // prints "undefined"
The easy approach for fixing this is to use Object.assign:
function createClient(options: Partial<Options> = {}) {
const opts = Object.assign({
host: 'localhost',
port: 12345,
username: 'admin',
password: 'admin',
}, options);
console.log(opts.host);
}
However, I find combining Joi is a lot nicer of an approach because it adds schema validation (in addition to defaults):
import * as Joi from 'joi';
type Options = {
host: string,
port: number,
username: string,
password: string,
}
const OptionsSchema = Joi.object().keys({
host: Joi.string().default('localhost'),
port: Joi.number().min(1024).max(65535).default(12345),
username: Joi.string().min(3).max(24).default('admin'),
password: Joi.string().min(5).max(24).default('admin'),
});
function createClient(options: Partial<Options> = {}) {
const { error, value as opts } = Joi.validate(options, OptionsSchema);
if (error) {
throw error;
}
console.log(opts.host);
}
Conclusion
Partial<T> and Joi schemas are a powerful combination when using the "Options Object" pattern. In my opinion, it's hard to implement the pattern in a more concise manner. You can skip object validation by omitting Joi and using Object.assign, but I'd argue it's hardly worth the lines of code saved, especially if you care about the correctness of input passed into your component. Another option is to use a different validation mechanism like JSON Schema (AJV being an excellent implementation).
Whatever approach you choose, I hope you found this post inciteful. If you have any questions, hit me up on LinkedIn.
Notes:
[1] Similar to Overrides/Default pattern.
[2] And kind of like RORO.
Stumbling my way through the great wastelands of enterprise software development.
