How to define a custom type with `px` suffix

Viewed 730

I know how to define a Position type class:

class Position {
  x: number = 0;
  y: number = 0;
}

But now I need to the x and y value is a integer with px as suffix like:

const position = {
  x: '1px',
  y: '2px'
}

How can I define a type like this in TypeScript?

3 Answers

You can't. That would be a dependent type which are not only not present in Typescript put you have to go waaaay outside the bounds of normal languages to find. Best known language with dependent types is probably Idris.

In terms of actually solving the problem, the best solution is probably something like this:

const VALID_XY_VAL = /^(-?\d+)px$/;

class Point {
  _x: number = 0;
  _y: number = 0;

  constructor(x: string, y: string) {
    // NOTE: not _x and _y, we want to invoke the
    // setters here
    this.x = x;
    this.y = y;
  }

  get x () {
    return `${this._x}px`;
  }

  get y () {
    return `${this._y}px`;
  }

  set x(xVal: string) {
    const num = xVal.match(VALID_XY_VAL);
    if (num === null) throw new Error('Invalid value');
    this._x = Number(num[1]);
  }

  set y(yVal: string) {
    const num = yVal.match(VALID_XY_VAL);
    if (num === null) throw new Error('Invalid value');
    this._y = Number(num[1]);
  }
}

const p = new Point('1px', '2px'); // Point<1, 2>
p.x; // '1px'
p.y; // '2px'
p.x = '5px'; // a-ok
p.y = '5ac3px'; // KABOOM! Error.

This stores the pixel value internally (and type-safely) as a number. It will throw at runtime if you try to set an invalid value.

Typescript playground

Regex101

EDIT

Altocumulus makes the case in the comments that the regex should be a read-only static member of the Point class. There are plusses and minuses to each approach: the static member follows the principle of least privilege but impedes reuse (e.g. for another class or function in the same file with pixel values). I think a declared-const module-level immutable (in any way that matters) value is sufficient, but YMMV.

class Point {
  private static readonly VALID_VALUE: RegExp = /.../

Edit, reasoning of the answer

Some of you can think this is not the answer for given question. But reality is that we as developers are responsible for modeling our data structures. Questioner asks here very specific question about how to model "{number}px" type in typescript. The answer is one - it is not possible. But the question is like that because questioner thinks this kind of type is a solution for his problem, what is misleading. The real problem is how to represent in type system the 2D point with two numeric values x,y and the unit. And this can be achieved by simple [number, number, 'px'], where we say we want two numbers and our unit is static 'px'. This kind of structure is flexible and type safe. Using some regexp, classes with setters and getters with throwing exception has nothing to compilation step, but is complex runtime validation.

Original answer:

Why you need a class for representing a pair. Using class for such simple construct is like killing a fly with a hammer. What you really need is simple pair + unit representation, which can be represented as tuple [a, b, unit] or record {a:a, b:b, unit: unit}. As others also pointed, there is no possibility to define type which have number with 'px' suffix, but you can model this in many ways. Few propositions:


1.Proposition one - modeling with additional unit information

// modeling as 3-nd tuple
type Point = [number, number, string]
const point = [1,2,'px']

// modeling as key-value map 
type Point = {x: number, y: number, unit: string}
const point = {x:1,y:2,unit:'px'}

2.Modeling with strict unit type. If we are sure that there will be strict unit like 'px', it can be define in type. To not repeat myself, I will show examples only in key-value map types.

type Point = {x: number, y: number, unit: 'px'} // unit property can be only px
const point = {x:1,y:2,unit:'px'}

Also we can create point constructor in order to avoid putting px by hand:

const createPoint = (x: number, y: number):Point => ({x,y,unit:'px'});
const point = createPoint(1,2) // {x:1,y:2,unit:'px'}

3.Modeling as pair of strings but with constructor We can leave the type as pair of string but generate this by number constructor

type Point = {x: string, y: string}
const createPoint = (x: number, y: number):Point => ({x: x + 'px', y: y + 'px'});
const point = createPoint(1,2) // {x:'1px',y:'2px'}

4.Modeling as an object with special get functions

type Point = {values: () => {x: number, y: number}, pixels: () => {x: string, y: string}}
// below is using closure and stored in it arguments of createPoint function
const createPoint = (x:number, y:number): Point => ({
  values: () => ({x, y}),
  pixels: () => ({x: x + 'px', y: y + 'px'})
})
const point = createPoint(1,2);
point.values() // {x: 1, y: 2}
point.pixels() // {x: '1px', y: '2px'}

You can either define a type with numerical x and y properties, as you did, or you define another type which allows both numbers and strings.

class Position {
  x: number | string = 0;
  y: number | string = 0;
}
Related