Sometimes we’ve got business rules that we want to model as a table of rule in our code. I created a minimalist implementation for such a rule engine in TypeScript.
Creating Rules
For our demo we want to create rules regarding salary ranges for software engineers. Our salaries are dependent on the following 4 characteristics: university degree, minimum years of experience, engineer has a blog, and last year’s performance. Our generic rule looks like the following:
interface Rule {
input: Input;
output: Output;
}
Hence, we can define the Input as an object considering all the input variables:
interface Input {
universityDegree: 'NONE' | 'BACHELOR' | 'MASTER';
minimumYearsOfExperience: 0 | 2 | 5 | 8 | 10 | 15;
hasBlog: boolean;
lastYearsPerformance: 1 | 2 | 3 | 4 | 5;
}
Because the output should have a lower and upper salary boundary we define an Output interface with two number fields:
interface Output {
minSalary: number;
maxSalary: number;
}
Based on these two types we can now define the rules:
const ruleDefinitions: Rule[] = [
{
input: { universityDegree: 'MASTER', minimumYearsOfExperience: 8, hasBlog: false, lastYearsPerformance: 3 },
output: { minSalary: 120_000, maxSalary: 130_000 }
},
{
input: { universityDegree: 'NONE', minimumYearsOfExperience: 10, hasBlog: true, lastYearsPerformance: 4 },
output: { minSalary: 125_000, maxSalary: 135_000 }
}
];
We see that you don’t really need a university degree to get a good salary. If you work well and write a tech blog you can even get a higher salary than people having a master’s degree 😉.
Evaluating Rules
The rule evaluation is simple. We just use the evaluate() method of rule-apparatus, define the input and output types, and call the method by passing the rules alongside the input arguments (here candidates):
let candidateA: Input = { universityDegree: 'MASTER', minimumYearsOfExperience: 8, hasBlog: false, lastYearsPerformance: 3 };
let candidateB: Input = { universityDegree: 'NONE', minimumYearsOfExperience: 10, hasBlog: true, lastYearsPerformance: 4 };
expect(evaluate<Input, Output>(ruleDefinitions, candidateA)).toEqual({
minSalary: 120_000,
maxSalary: 130_000,
});
expect(evaluate<Input, Output>(ruleDefinitions, candidateB)).toEqual({
minSalary: 125_000,
maxSalary: 135_000,
});
We get the expected salary ranges for our two candidates back from the evaluate method. And everything is type safe.
Rule apparatus sources and installation
These rules defined above could also be defined in a JSON format and delivered to wherever the evaluation should happen. The functionality is kept basic intentionally. There is for example no greaterThan functionality at the moment. It’s the responsibility of the consumer to define the inputs intelligently.
Please check GitHub to see the full code for this simple TypeScript rule engine. If you see any issues or potential for improvements please file an issue. The package itself can be found here on npmjs or can be installed by executing:
npm i rule-apparatus