In other languages you might use code similar to the TypeScript below for validation. Perhaps it's a little verbose wrapped in a class, but it doesn't hide much complexity and the class has useful properties in terms of type checking. Unfortunately, there are two problems as shown below in code comments that arise from extending JavaScript's base classes.
class PositiveNumber extends Number {
constructor(value: unknown) {
super(value)
if (typeof value !== 'number' || value < 0) {
throw new Error('expected positive number')
}
}
}
const price: PositiveNumber = -1 // Problem 1: This should error but doesn't.
const adjustedPrice = price + 1 // Problem 2: This shouldn't error but does.
Problem 1 is caused by TypeScript's type equivalence checking, but we can work around this by adding a symbol as a protected property. This solution isn't ideal because it requires an unnecessary property and the knowledge of this problem since TypeScript won't raise an error for this problem.
class PositiveNumber extends Number {
protected readonly constraint = Symbol()
constructor(value: unknown) {
super(value)
if (typeof value !== 'number' || value < 0) {
throw new Error('expected positive number')
}
}
}
const price: PositiveNumber = -1 // Problem 1: Solved. This now errors.
const adjustedPrice = price + 1 // Problem 2: This shouldn't error but does.
Problem 2 is caused by TypeScript's requirement for the plus operator to be used on number
type values, but our price
variable is now a PositiveNumber
. We can use the inherited valueOf
method from the Number
class we extended in the PositiveNumber
class. This solution isn't ideal because it requires an unnecessary method call.
class PositiveNumber extends Number {
protected readonly constraint = Symbol()
constructor(value: unknown) {
super(value)
if (typeof value !== 'number' || value < 0) {
throw new Error('expected positive number')
}
}
}
const price: PositiveNumber = -1 // Problem 1: Solved. This does error.
const adjustedPrice = price.valueOf() + 1 // Problem 2: Solved. This doesn't error.
Rulr gets around these two problems with "constrained" (nominal/branded/opaque) types as shown below.
import * as rulr from 'rulr'
const positiveNumberSymbol = Symbol()
function constrainToPositiveNumber(input: unknown) {
if (typeof input === 'number' && input >= 0) {
return rulr.constrain(positiveNumberSymbol, input)
}
throw new Error('expected positive number')
}
type PositiveNumber = rulr.Static<typeof constrainToPositiveNumber>
const price: PositiveNumber = -1 // Problem 1: Solved. This does error.
const adjustedPrice = price + 1 // Problem 2: Solved. This doesn't error.
- Michal Zalecki has written a great post on nominal typing techniques in TypeScript. They have also referenced a further discussion on nominal typing in the TypeScript Github repository.
- Charles Pick from CodeMix has wrote an interesting post introducing opaque types and how they compare in TypeScript and Flow.
- Drew Colthorp has a post on flavoured types which are like branded types but allow inference.
- Ilia Choly created issue #2361 in the TypeScript repository to discuss allowing the
valueOf
method to be used for types when using math operands.