Home > Software design >  Typescript Hash Member Arythmetical restrictions
Typescript Hash Member Arythmetical restrictions

Time:10-10

Can I define somehow a type in typescript which apply as well arythmeical constraints to object props E.g.

type RestrictedObject = {
a: number,
b: number,
}

so that a % b = 0

CodePudding user response:

No, that would require runtime checking, which is out of bounds for TypeScript.

You can define an object with accessor properties to enforce the relationship:

class Restricted {
    #a: number;
    #b: number;

    static #valid(a: number, b: number): boolean {
        return a % b === 0;
    }

    constructor(a: number,  b:number) {
        if (Restricted.#valid(a, b)) {
            throw new Error(`The values of 'a' and 'b' must be such that 'a % b' is 0; ${a} and ${b} don't fit`);
        }
        this.#a = a;
        this.#b = b;
    }

    get a() {
        return this.#a;
    }
    set a(value) {
        if (this.#b !== null) {
            if (Restricted.#valid(value, this.#b)) {
                throw new Error(`The value of 'a' cannot be ${value} when 'b' is ${this.#b}`);
            }
        }
        this.#a = value;
    }

    get b() {
        return this.#b;
    }
    set b(value) {
        if (this.#a !== null) {
            if (Restricted.#valid(this.#a, value)) {
                throw new Error(`The value of 'b' cannot be ${value} when 'a' is ${this.#a}`);
            }
        }
        this.#b = value;
    }
}

Side note: The above uses JavaScript's native private fields and private static methods, which are specified parts of the language now, but you could use TypeScript's private instead if you liked.

CodePudding user response:

If you want to write less code you can simply throw an error in constructor:

class RestrictedObject {
  constructor(public a: number, public b: number) {
    if (a % b !== 0) {
      throw new Error(`Remainder of ${a}/${b} must be zero.`);
    }
  }
}

console.log(new RestrictedObject(4, 3).a);
// Error: Remainder of 4/3 must be zero.

But if you are the adventurous variety and really want to have a go at doing with types alone, you can achieve it:

type Length<T extends any[]> = T extends { length: infer L } ? L : never;

type BuildTuple<L extends number, T extends any[] = []> = T extends { length: L }
  ? T
  : BuildTuple<L, [...T, any]>;

type Subtract<A extends number, B extends number> = BuildTuple<A> extends [...infer U, ...BuildTuple<B>]
  ? Length<U>
  : never;

type EQ<A, B> = A extends B ? (B extends A ? true : false) : false;
type AtTerminus<A extends number, B extends number> = A extends 0 ? true : B extends 0 ? true : false;

type LT<A extends number, B extends number> = AtTerminus<A, B> extends true
  ? EQ<A, B> extends true
    ? false
    : A extends 0
    ? true
    : false
  : LT<Subtract<A, 1>, Subtract<B, 1>>;

type Modulo<A extends number, B extends number> = LT<A, B> extends true ? A : Modulo<Subtract<A, B>, B>;

type RestrictedObject<A extends number, B extends number, T = Modulo<A, B>> = {
  a: T extends 0 ? A : never;
  b: T extends 0 ? B : never;
};

const foo: RestrictedObject<4, 2> = {
  a: 4,
  b: 2
};

And when wrong:

const foo: RestrictedObject<4, 3> = {
  a: 4, // type error: Type 'number' is not assignable to type 'never'.
  b: 2 // type error: Type 'number' is not assignable to type 'never'.
};
  • Related