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.