I have a set of Calculators, which each implement a different way to calculate something.
Say each of those Calculators has a function sum()
type Sum = (numbersToSum: number[]) => number
type Calculator = {
sum: Sum,
}
Now I want to have one Calculator that sums exactly 2 numbers and one that sums exactly 3, so I could implement the type as such:
type TwoNumbers = [number, number]
type ThreeNumbers = [number, number, number]
type Sum = (numbersToSum: TwoNumbers | ThreeNumbers) => number
When I then implement these functions in a TwoNumberCalculator or ThreeNumberCalculator 'class' I would like to implement these function as
const TwoNumberCalculator: Calculator = {
sum: (numbersToSum: TwoNumbers) => numbersToSum[0] + numbersToSum[1],
}
const ThreeNumberCalculator: Calculator = {
sum: (numbersToSum: ThreeNumbers) => ...,
}
However I get a is not assignable type error.
TwoNumberCalculator:
numbersToSum: TwoNumbers is not assignable to type TwoNumbers | ThreeNumbers7
ThreeNumberCalculator:
numbersToSum: ThreeNumbers is not assignable to type TwoNumbers | ThreeNumbers
Of course I could just keep
type Sum = (numbersToSum: number[]) => number
to describe the parameter, however having fixed array sizes would be nice, since I sometimes have to access the array elements directly through index.
The issue is that your interface’s function signature expects a parameter that can be either a two-element tuple or a three-element tuple, it must handle both cases. However, when you implement the function for two numbers only, its parameter type is too narrow. In TypeScript a function that only accepts a two-number tuple isn’t assignable to one that is declared to accept a union of two-number and three-number tuples.
So this function type Sum = (numbersToSum: TwoNumbers | ThreeNumbers) => number;
must be able to handle either a two-element array or a three-element array. But when you write:
const TwoNumberCalculator: Calculator = {
sum: (numbersToSum: TwoNumbers) => numbersToSum[0] + numbersToSum[1],
};
the function only accepts exactly two numbers. If someone were to pass three numbers (which is allowed by the union in the interface), the implementation would not handle it.
A good solution is to make your Calculator
interface generic so that each implementation can specify exactly what tuple length it expects. For example:
interface Calculator<T extends number[]> {
sum: (numbersToSum: T) => number;
}
type TwoNumbers = [number, number]
type ThreeNumbers = [number, number, number]
const TwoNumberCalculator: Calculator<[number, number]> = {
sum: ([a, b]) => a + b,
};
const ThreeNumberCalculator: Calculator<[number, number, number]> = {
sum: ([a, b, c]) => a + b + c,
};
const twoNumbers: TwoNumbers = [3, 7];
const threeNumbers: ThreeNumbers = [3, 7, 5];
const twoNumbersResult = TwoNumberCalculator.sum(twoNumbers);
const threeNumbersResult = ThreeNumberCalculator.sum(threeNumbers);
console.log(`The sum of ${twoNumbers[0]} and ${twoNumbers[1]} is ${twoNumbersResult}.`);
console.log(`The sum of ${threeNumbers[0]} and ${threeNumbers[1]} and ${threeNumbers[2]} is ${threeNumbersResult}.`);
Each implementation now has a signature that exactly matches its intended use, and the type error will be resolved.
Here is a Playground