How to create 2 incompatible number-like types in TypeScript?

后端 未结 2 546
你的背包
你的背包 2021-01-04 11:12

I\'ve been trying to figure out how to create 2 mutually-incompatible number-like/integer types in TS.

For example, in the code below, height and weight are both num

相关标签:
2条回答
  • 2021-01-04 12:03

    So, after banging my head against the wall for several hours, I managed to come up with this:

    class height extends Number {}
    class weight extends Number {}
    

    By sub-classing the Number class, Typescript allows you to create distinct numeric types.

    And then you can go and use the variables as specified above.

    var a: height = 68;
    var b: weight = 184;
    console.log(a+b); // Should be an error.
    

    The issue that I run into is that this also returns an error:

    console.log(a+a); // Should NOT be an error.
    
    0 讨论(0)
  • 2021-01-04 12:05

    The closest thing to a newtype in TypeScript is to create a new "nominal" type (TypeScript doesn't have nominal types but there are workarounds like branding) and create a value constructor and a field accessor function which just uses type assertions in the implementation. For example:

    interface Height { 
      __brand: "height"
    }
    function height(inches: number): Height {
      return inches as any;
    }
    function inches(height: Height): number {
      return height as any;
    }
    
    interface Weight { 
      __brand: "weight"
    }
    function weight(pounds: number): Weight {
      return pounds as any;
    }
    function pounds(weight: Weight): number {
      return weight as any;
    }
    
    const h = height(12); // one foot
    const w = weight(2000); // one ton
    

    The types Height and Weight (sorry, can't bring myself to give new types a lowercase name) are treated as distinct types by the compiler. The height() function is a Height value constructor (takes a number and returns a Height), and the inches() function is its associated field accessor (takes a Height and returns a number), and weight() and pounds() are the analogous functions for Weight. And all of those functions are just identity functions at runtime. So JavaScript treats them as pure numbers with a bit of function-call overhead that's hopefully optimized away by a good compiler, and if you're really worried about that overhead, you can do the assertions yourself:

    const h = 12 as any as Height;
    const w = 2000 as any as Weight;
    

    Now you have distinct named types you can use so you don't accidentally use a Height where a Weight is needed or vice versa. But, just like with a newtype, the compiler will not treat these as a number. Yes, you could make Height and Weight subtypes of number (via intersection types), but that's probably a mistake: the arithmetic operators like + are able to operate on number values and if both h and w are subtypes of number, then h + w will not be an error. And if h and w are not subtypes of number, then h + h will be an error. And you can't change that, since TypeScript does not let you alter the type declarations of operators the way it does with functions. I prefer to prevent both h + h and h + w from compiling, so the Height and Weight types are not numbers. Instead, let's create our own add() function that behaves how you want:

    type Dimension = Height | Weight;
    
    function add<D extends Dimension>(a: D, b: D): D {
      return ((a as any) + (b as any)) as any;
    }
    

    The add() function accepts either two Height or two Weight parameters, and returns a value of the same type. Actually, with the above it's still possible to pass in something like Height | Weight as D, so if you're really serious about locking it down you can use overloads instead:

    function add(a: Height, b: Height): Height;
    function add(a: Weight, b: Weight): Weight;
    function add(a: any, b: any): any {
      return a+b;
    }
    

    And, behold:

    const twoH = add(h, h); // twoH is a Height
    const twoW = add(w, w); // twoW is a Weight
    const blah = add(h, w); // error, Height and Weight don't mix
    

    So we're almost done. For your external measureScale() function you'd just declare the return type to be a Weight:

    declare function measureScale(): Weight;
    
    var a = height(68);
    var b = measureScale();
    

    And verify the intended result:

    console.log(add(a,b)); // err
    console.log(add(a,a)); // okay
    

    Hope that helps; good luck!

    0 讨论(0)
提交回复
热议问题