1
function Box(x: any) {
  return {
    inspect: () => `Box(${x})`,
    map: (f: (arg0: any) => any) => Box(f(x)),
    fold: (f: (arg0: any) => any) => f(x)
  }
}

const expr = Box(8)
  .map((x: number) => x + 8)
  .map((x: any) => Number(x))
  .map((x) => Math.cos(x))

console.log(expr.fold((x: any) => x))

How to improve the typing of the Box expression?

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
Raphael Mansuy
  • 213
  • 1
  • 4
  • 10
  • 2
    Take a look at [generics](https://www.typescriptlang.org/docs/handbook/generics.html) – Mulan Jan 21 '21 at 02:41
  • You've tagged this [typescript-generics], so it seems you already know what to do. Can you show us your attempt, please, or explain what you don't understand about using generics? – Bergi Jan 21 '21 at 02:43
  • 1
    Note that [I cannot reproduce](https://tsplay.dev/DWKn8m) your ESLint warning; perhaps something about your IDE configuration is weird? Maybe [this](https://stackoverflow.com/questions/40900791/cannot-redeclare-block-scoped-variable-in-unrelated-files) is your problem? – jcalz Jan 21 '21 at 02:43
  • 1
    Please ask only a single question per post. I've removed the one about ESLint, you might want to [create a new question](https://stackoverflow.com/questions/ask) for it (but please give more details about how and where the warning happens) – Bergi Jan 21 '21 at 02:44

1 Answers1

3

"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

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • 1
    In theory [one doesn't even need the interface](https://www.typescriptlang.org/play?ts=4.1.3#code/GYVwdgxgLglg9mABAITgDwDwBUB8AKNALkSwEpEBvAKEUQCcBTKEOpa22mMAZwAcHoxPOQC8ORAANUaPABIKaAL6kJAGhocAtgENexDAFVV+YEKiEyYg6PHTDJgqVLqOiYHAA2AE31GThPHNLHGtEMTdHDUUqaMQqCARuKEQGNF46MJR0PABZbSgACwA6AAUASUQAekQAJlIinV5c-OKE7nrGgjDxNCKoOAAxGDQGLzwAFicG3TwAORBNACMGOlIAbip4xM8GIo84AHM8VPSi928u8LR6-qGRsbrSIA), but then the types that TS infers get unwieldy. – Bergi Jan 21 '21 at 02:56
  • 1
    Yeah, recursive anonymous types are . – jcalz Jan 21 '21 at 02:59