Home > Mobile >  Typescript classes equivalent when they should be different
Typescript classes equivalent when they should be different

Time:04-12

I am trying to put a lightweight layer on top of Float32Array to create vec2 and vec3's. Here is an example:

class vec3 extends Float32Array {
//    w : number;
    constructor() {
        super(3);
//        this.w = 1;
    }
    static add3(x: vec3) : vec3 {
        x[2]  = 1;
        return x;
    }
};

class vec2 extends Float32Array {
    constructor() {
        super(2);
    }
    static add2(x: vec2) : vec2 {
        x[1]  = 1;
        return x;
    }
};

var test1: vec3 = new vec3();
vec3.add3(test1);
console.log(test1);

var test2: vec2 = new vec2();
vec3.add3(test2);                       // This should be an error
console.log(test2);

(TS playground link)

Typescript thinks that these two classes are equivalent and allows the vec3.add3(test2);

If I uncomment out the line that defines the w member variable it correctly understands that they are different classes.

Is there a way to differentiate these two classes?

CodePudding user response:

Typescript uses structural typing by default, so if your class A is declared to have public fields x and y and a method called foo, then any object with fields x and y and a method called foo (with matching types) is considered an instance of A.

However, there is one intentional loophole in this system. If your class has a private field called x, then only actual instances of that class are considered valid instances of the type. Some other object with a field called x is incompatible.

class vec3 extends Float32Array {
  private __tag: unknown = {};
  ...
}

class vec2 extends Float32Array {
  private __tag: unknown = {};
  ...
}

Even though these two classes have the same shape and both have a field called __tag, the two are considered incompatible since the fields are private.

CodePudding user response:

Looks like you can do this by intentionally placing an error in your code that changes the signature of the class but does not actually take up any space in the structure:

class vec3 extends Float32Array {
// @ts-ignore: unused member to keep the type unique
    __vec3_type : number;
    constructor() {
        super(3);
    }
    static add3(x: vec3) : vec3 {
        x[2]  = 1;
        return x;
    }
};

class vec2 extends Float32Array {
// @ts-ignore: unused member to keep the type unique
    __vec2_type : number;
    constructor() {
        super(2);
    }
    static add2(x: vec2) : vec2 {
        x[1]  = 1;
        return x;
    }
};

var test1: vec3 = new vec3();
vec2.add2(test1);
console.log(test1);

var test2: vec2 = new vec2();
vec3.add3(test2);                       // This should be an error
console.log(test2);

This does what I'd like without adding any space to object itself.

I guess this from Typescripts decision to use structural instead of nominal typing: https://www.typescriptlang.org/docs/handbook/type-compatibility.html

CodePudding user response:

Without any branding and double underscores, you could make a generic class that saves the number of dimensions as a property of the class:

class Float32ArrayN<T extends number> extends Float32Array {
    constructor(public dimensions: T) {
        super(dimensions);
    }
}
const a = new Float32ArrayN(4)
a.dimensions // type: 4

const b: Float32ArrayN<3> = new Float32ArrayN(4) // error

Now each class is structurally different since dimensions is a different literal number type. Now you can make your subclasses like so:

class vec3 extends Float32ArrayN<3> {
    constructor() {
        super(3);
    }
    static add3(x: vec3) : vec3 {
        x[2]  = 1;
        return x;
    }
};

class vec2 extends Float32ArrayN<2> {
    constructor() {
        super(2);
    }
    static add2(x: vec2) : vec2 {
        x[1]  = 1;
        return x;
    }
};

Which throws errors as you would expect:

var test1: vec3 = new vec3();
vec3.add3(test1);
console.log(test1);

var test2: vec2 = new vec2();
vec3.add3(test2);             // error
console.log(test2);

var test3: vec3 = new vec2()  // error

Playground

  • Related