"Improving" the typing of Box
is a little vague, but assuming you'd like Box
to keep track of the type of value it contains, then I'd introduce a generic interface to represent the intended behavior:
interface Box<T> {
inspect(): string;
map<U>(f: (arg0: T) => U): Box<U>;
fold<U>(f: (arg0: T) => U): U;
}
A Box<T>
's inspect
method always produces a string
. Its map()
method takes a callback turning a T
into some other type U
and produces a Box<U>
, and fold
takes a callback turning a T
into some other type U
and produces just a U
.
Then you can annotate the function named Box
as a generic one which accepts a value of type T
and returns a Box<T>
:
function Box<T>(x: T): Box<T> {
return {
inspect: () => `Box(${x})`,
map: f => Box(f(x)),
fold: f => f(x)
}
}
Note how you can remove the annotations inside the implementation of Box
because they are contextually typed by the expected Box<T>
method parameter types.
Then when you use Box
, you will see how the compiler keeps track of whether it is holding a number
or a string
(or anything else), and again the callbacks can be contextually typed so you can leave off their annotations:
const expr = Box(Math.PI / 2) // Box(1.5707963267948966)
.map(x => Math.cos(x)) // Box(6.123233995736766e-17)
.map(x => x.toFixed(4)) // Box("0.0000")
.map(x => Number(x)) // Box(0)
console.log(expr.fold(x => x).toFixed(2)) // "0.00"
And the compiler will complain if you do the wrong thing, like treat a string
as a number
:
const badExpr = Box("Hello")
.map(x => x.toFixed(2)); // error!
// -------> ~~~~~~~
// Property 'toFixed' does not exist on type 'string'.
Playground link to code