How To Assume Two Union Types As The Same
Solution 1:
You are getting the error because both s
and ss
are just a Shape
the entire time. The compiler knows that both have a value called "kind" but still does not know the actual type of each. A Square
might have a "size" but a Shape
does not and the compiler knows it.
Creating a function that needs to know the details of a Shape
defeats the purpose of using an interface
in the first place. You could achieve what you want much more cleanly like so:
interfaceSquare {
area(): numbersize: number
}
interfaceRectangle {
area(): numberwidth: numberheight: number
}
typeShape = Square | Rectanglefunctionareas(s: Shape, ss: Shape) {
return s.area() + ss.area()
}
However if you really want to do it you could do it by explicitly casting each object to the desired type before you access it's properties
interfaceSquare {
size: number
}
interfaceRectangle {
width: numberheight: number
}
typeShape = Square | Rectanglefunctionareas(s: Shape, ss: Shape) {
if (typeof s != typeof ss) {
return
}
switch (typeof s) {
case'Square': {
s = s asSquare; ss = ss asSquarereturn s.size * s.size + ss.size * ss.size
}
case'Rectangle': {
s = s asRectangle; ss = ss asRectanglereturn s.width * ss.height + s.width * ss.height
}
}
}
Note that this second example won't actually work (even if you explicitly declare something as one of your union types), despite the fact that is compiles since typeof
will return "object"
, but it demonstrates how to tell the compiler which type to use (using as)
classSquareImplimplementsSquare {
size: number = -1constructor(size : number) {
this.size = size
}
}
let s : Square = newSquareImpl(10)
console.log(typeof s) // logs "object"
You might try and implement it using instanceof
:
if (s instanceofSquare && ss instanceofSquare) {
s = s asSquare; ss = ss asSquarereturn s.size * s.size + ss.size * ss.size
}
// similar code for Rectangle etc
However Typescript won't let you use check if an object implements an interface at runtime, so you're back to using a custom type guard:
interfaceSquare {
kind: stringsameShape(obj: Shape): booleanarea(): numbersize: number
}
classSquareImplimplementsSquare {
kind: string = "square"size: number = -1area() { returnthis.size * this.size }
sameShape(obj: Shape): obj is Square {
return obj.kind == "square"
}
constructor(size: number) { this.size = size }
}
// similar for Rectangle
...
let r : Rectangle = newRectangleImpl(1, 2)
let s : Square = newSquareImpl(3)
let ss : Square = newSquareImpl(2)
if (s.sameShape(ss)) {
console.log('s + ss: '+ s.area() + ss.area())
}
if (s.sameShape(r)) {
console.log('s + r: '+ s.area() + r.area())
}
Solution 2:
Convincing the compiler that two variables of union types are correlated is not really easy or even possible in the general case. The compiler will almost always think two such values are independent, unless you test them separately. Meaning: the only solutions here are going to look like: you only do enough work to convince yourself that the types are the same, and you have to tell the compiler not to worry about it via something like a type assertion:
functionareas(s: Shape, ss: Shape) {
if (s.kind !== ss.kind) return; // check if the kind of them are the sameswitch (s.kind) {
case"square":
return s.size * s.size + (ss astypeof s).size * (ss astypeof s).size;
case"rectangle":
return (
s.height * s.width + (ss astypeof s).height * (ss astypeof s).width
);
}
}
Or, you will have to do more work than you think should be necessary so that the compiler is convinced that the only possibilities are the ones you expect. Meaning you'll do what feels to you like redundant type guards. I think in this case, I'd refactor your code to be like this, which only adds one more check:
function areas2(s: Shape, ss: Shape) {
if (s.kind === "square" && ss.kind === "square") {
return s.size * s.size + ss.size * ss.size;
}
if (s.kind === "rectangle" && ss.kind === "rectangle") {
return (
s.height * s.width + ss.height * ss.width
);
}
return;
}
Okay, hope that helps. Good luck!
Solution 3:
So I see a few ways to improve your code.
In my opinion, Union Types should only be used when the two type are disparate types. For example, CSS allows values for some properties to either be a string or a number. So how do you convey to the consumer of your function that you only want them to pass one of those two? That's a good case for a Union Type:
varelement: HtmlElement;Z
functionbad(width: any) {
element.style.width = width;
}
// no typescript error about the wrong type being passed inbad(newDate());
type widthType = string | number | null;
functiongood(width: widthType) {
element.style.width = widthType
}
// typescript error about the wrong type being passed ingood(newDate());
Typescript Playground Example.
While many people decide to go the route of using a kind
property, I avoid it as much as possible since it's a Magic String. If two types are compatible, someone must know the inter workings of your square to build there own square (yikes). You can technically avoid that by moving to abstract classes:
abstractclassSquare {
static kind = 'square'
}
But then you can just use instanceOf
, so no real point to that.
However, in Object Oriented Programming, we need to be aware of Inheritence (is-a) and Composition (has-a). Since both Rectangle is-a shape, and Square is-a shape, then we should be modeling our objects as such:
interfaceShape{ }
interfaceRectangle : Shape { }interfaceSquare : Shape { }
Now that we have a good staring point for the models we need to take a look at the method. What is an area? An Area is the quantity that expresses the extend of a two dimensional figure or shape. So we should now modify our inheritance chain/tree/whatever to require this functionality:
interface Shape {
areas(shape: Shape): number;
}
interface Rectangle : Shape { }
interface Square : Shape { }
We encapsulate that method at the shape level, because all shapes (2D or larger assuming) have an area (0 is still a size).
It's easy to review this and thing, why should the shapes do this calculation and I would simply suggest many frameworks (if not most OOP frameworks) do this exact thing. When you compare two objects in .Net via Equals you should always test that the types are the same. But notice that the method is at the root of object, not a disconnect/global method.
So this might be a good result of that improvement:
interfaceShape{
// null would indicate we can't join the two// I prefer null to indicate a known invalid value// and only use undefined to indicate an unknown (always invalid) value
areas(shape: Shape): number | null;
}
interfaceRectangle : Shape { }interfaceSquare : Shape { }classMyRectangle : Rectangle {
width: number;
height: number;
area(shape: Shape){
if (!(shape instanceOf Rectangle)) {
returnnull;
}
returnthis.height * this.width + shape.height * shape.width;
}
}
classMySquare : Square {
size: number;
area(shape: Shape){
if (!(shape instanceOf Square)) {
returnnull;
}
returnthis.size * this.size + shape.size * shape.size;
}
}
// Example call:const mySquare = new MySquare();
const mySquare2 = new MySquare();
const areas = mySquare2.area(mySquare); // fully type checked.
The previous examples are good if the interfaces are a separate library then the classes, where someone might actually want to express those values differently. If that's not the case, and there should only be 1 type of square and 1 type of rectangle, then interfaces are not the best choice, and I would suggest using classes instead. Because implementing Circle in the previous example would be very difficult (both the interfaces and the classes need to change). Uses classes would look like:
abstractclassShape{
area(shape: Shape);
}
classRectangle : Shape {
width: number;
height: number;
area(shape: Shape){
if (!(shape instanceOf Rectangle)) {
returnnull;
}
returnthis.height * this.width + shape.height * shape.width;
}
}
classSquare: Shape {
size: number;
area(shape: Shape){
if (!(shape instanceOf Square)) {
returnnull;
}
returnthis.size * this.size + shape.size * shape.size;
}
}
Now implementing Circle becomes trivial.
classCircle: Shape {
radius: number;
area(shape: Shape){
if (!(shape instanceOf Circle)) {
returnnull;
}
returnthis.size * this.size + shape.size * shape.size;
}
}
Solution 4:
Replace your if condition as if (s.kind !=== ss.kind)
Hope it will work for you
Post a Comment for "How To Assume Two Union Types As The Same"