To avoid mixing all my variables typed number with each other, I want to add an additional layer of security by typing them in a way that would generates a compilation error if I try to assign a literal number or another type to this new type.
You can see that as giving a unit to my variables, for instance "second" and "meter". They both are derived from the literal type "number", but I shouldn't be able to add a variable typed second with one typed meter. I shouldn't neither be able to assign a variable of type second to another one of type meter.
let length1 = 5 as Meter;
let length2 = 7 as Meter;
let duration1 = 10 as Second;
let duration2 = 11 as Second;
let lengthSum = length1 + length2 // should type lengthSum as Meter and give it a value of 12
let durationSum = duration1 + duration2 // should type durationSum as Second and give it a value of 21
length1 = duration2 // should generate an error
let foo = length1 + duration1; // should generate an error
By extension, I would also like to do the same with strings:
let myBookTitle = "Best title ever" as Title;
let myBookAuthor = "John Doe" as Author;
myBookTitle = myBookAuthor // should generate an error
.
I have tried with the following custom types:
type Meter = number & { __type: 'meter' };
type Second = number & { __type: 'second' };
They work for most cases but miss the mixed addition case:
let length1 = 5 as Meter;
let length2 = 10 as Meter;
let duration1 = 2 as Second;
let duration2 = 3 as Second;
length1 = length2; // works as expected
length1 = duration1; // generates an error as expected
let length10 = (length1 + length2) as Meter; // works but is not able to automatically infer the length10 type with the "as Meter"
let length11 = (duration1 + duration2) as Second; //same as above
let length12 = (length1 + duration2) as Second; // Shouldn't allow such addition but does it without any error!!
Would anyone know how to create a new type that covers all my constraints? (I know there are some npm packages that cover specifically the SI units, but that is not what I am looking for. Units are used here as examples; I will need to handle multiple types of différents types outside "simple" units. Thanks.)
Unfortunately this is not currently possible as asked. The TypeScript behavior of native JavaScript operators like the addition operator (+
) is baked into the language and you can't override or customize it. So if you write x + y
where x
and y
are some subtype of number
, you will get a plain number
out, undoing any nominal-like type branding you've applied.
There is an open feature request at microsoft/TypeScript#42218 to be able to essentially merge your own overload signatures for these operators, but until and unless such a feature is implemented there's no way to do this.
For now there are only workarounds. The particular workaround you use is probably out of scope for the question, but here are some possibilities that might be useful as a starting point:
You can't customize operator behavior, but you can customize function behavior, so if you want you can wrap operators in functions and then use the functions instead:
function add<T extends number>(t1: T, t2: T): T {
return t1 + t2 as T;
}
let length10 = add(length1, length2);
// ^? let length10: Meter
let length11 = add(duration1, duration2);
// ^? let length11: Second
let length12 = add(length1, duration2); // error
Obviously this isn't ideal, since you need to remember to use add()
instead of +
.
Or, you could refactor to use classes holding number values instead of the numbers directly:
class BrandedNumber<T extends string> {
constructor(public __type: T, public value: number) { }
add(other: BrandedNumber<T>) {
return new BrandedNumber(this.__type, this.value + other.value);
}
}
const Meter = (value: number) => new BrandedNumber("meter", value);
const Second = (value: number) => new BrandedNumber("second", value);
let length1 = Meter(5);
let length2 = Meter(10);
let duration1 = Second(2);
let duration2 = Second(3);
length1 = length2; // okay
length1 = duration1; // error
let length10 = length1.add(length2);
// ^? let length10: BrandedNumber<"meter">
let length11 = duration1.add(duration2);
// ^? let length11: BrandedNumber<"second">
let length12 = length1.add(duration2); // error
which also isn't ideal, especially if you care about preserving the runtime behavior (presumably you wouldn't want to do any computation-intensive processing that wraps and unwraps numbers like this).
Again, the particular workaround one chooses depends on one's use cases and isn't really in scope here; the main point is that without operator overloading, a workaround is necessary.