1

I hive 2 classes:

export interface Vec2I {
    x: number;
    y: number;
}

export class Vec2 implements Vec2I {
    constructor(public x: number = 0, public y: number = 0) {}

    public set(x: Vec2I): void;
    public set(x: number, y: number): void;

    public set(x: Vec2I | number, y?: number) {
        if (typeof x === "number") {
            this.x = x;

            if (y !== undefined) {
                this.y = y;
            }
        } else {
            this.x = x.x;
            this.y = x.y;
        }
    }
}

and

import { Vec2, Vec2I } from "./Vec2";

export interface Vec3I extends Vec2I {
    z: number;
}

export class Vec3 extends Vec2 implements Vec3I {
    constructor(x: number = 0, y: number = 0, public z: number = 0) {
        super(x, y);
    }

    public set(x: Vec3I): void;
    public set(x: number, y: number, z: number): void;

    public set(x: Vec3I | number, y?: number, z?: number) {
        super.set(x, y);

        if (typeof x === "number") {
            if (z !== undefined) {
                this.z = z;
            }
        } else {
            this.z = x.z;
        }
    }
}

and the error I see when I try to overload a method in inherited class is:

Property 'set' in type 'Vec3' is not assignable to the same property in base type 'Vec2'.
  Type '{ (x: Vec3I): void; (x: number, y: number, z: number): void; }' is not assignable to type '{ (x: Vec2I): void; (x: number, y: number): void; }'.
    Types of parameters 'x' and 'x' are incompatible.
      Type 'number' is not assignable to type 'Vec3I'.

enter image description here

I don't understand the problem. Vec3I extends from Vec2I, so in should be ok to substitute in method parameters. If I comment overloading, everything works:

enter image description here

1 Answer 1

2

The error is correct. You're trying to say that Vec3 "is a" Vec2 (Vec3 extends Vec2) but that its set method doesn't accept the same thing that Vec2's set method accepts: a Vec2I object. You can't do that in TypeScript and most similar languages, it violates the Liskov substitution principle. If a Vec3 "is a" Vec2, then you have to be able to use it where a Vec2 can be used, which means you have to be able to call set with an argument of type Vec2I, you can't restrict it to only things that are also Vec3Is.

How you solve it depends on what you're trying to do. Looking at your classes, you could add additional overloads for the third coordinate, and handle that in the updated implementation:

// Inherited
public set(x: Vec2I): void;
public set(x: number, y: number): void;

// Additional
public set(x: Vec3I): void;
public set(x: number, y: number, z: number): void;

// Implementation handling both
public set(x: Vec2I | Vec3I | number, y?: number, z?: number) {
    // Call the correct version of `super.set`
    if (typeof x === "object") {
        super.set(x);
    } else {
        // Here we have to assert that `y` isn't nullish, but we
        // know it isn't. I've used a type assertion function here,
        // but some would probably just use the "trust me, I know
        // it's not nullish" type assertion operator (postfix `!`).
        // I'm not a fan of avoidable type assertions, but there's
        // an argument for it here
        assertNotNullish(y);
        super.set(x, y);
    }

    if (typeof x === "number") {
        if (z !== undefined) {
            this.z = z;
        }
    } else if ("z" in x) { // *** Have to guard against `Vec2I` here
        this.z = x.z;
    }
}

where assertNotNullish is (see comments above for why I used it; you could just use the non-nullish type assertion operator instead):

function assertNotNullish<T>(value: T | null | undefined): asserts value is T {
    if (value == null) {
        throw new Error(`value expected not to be nullish`);
    }
}

Playground link

The other alternative that comes to mind is to have a single generic class that accepts a type argument for the data it should be dealing with. That's often the solution you'd want to reach for, but doesn't seem appropriate in this specific case.

Sign up to request clarification or add additional context in comments.

4 Comments

Thanks for reply, but I don't understand why method set() with super works correctly, but overloads don't work, because method set() also have Vec2I in parent class, but even I put argument x with type Vec3I it works? Why in this case it isn't "violates the Liskov substitution principle"? Vec3 have all properties needed for Vec2, so it can replace Vec2 and can't break anything.
@YuriiHorchynskyi - It works because Vec3I "is a" Vec2I (it has all the properties that Vec2I requires). The problem with your original code was that it relied on the opposite (Vec2I "is a" Vec3I), which isn't true, because it doesn't have the z property.
why it is opposite ? (Vec2I "is a" Vec3I). If I create Vec3 instance it will use set() from Vec3 firstly, then super.set() and call from Vec2. How it can be in reversed queue?
@YuriiHorchynskyi - Because of the Liskov principle mentioned above, Vec3's set has to accept an object for x that is just a Vec2I, because in the general case code may be using it as though it were a Vec2 instance, and so passing it something that's just a Vec2I. Vec2I isn't a Vec3I, so that doesn't work. It works the other way around (passing a Vec3I object into a method accepting a Vec2I) because Vec3I "is a " Vec2I.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.