Skip to content

No "sidecasting" #16995

Closed
Closed

Description

TypeScript Version: 2.4

Code

interface A { }
interface B { }
class AandB implements A, B { }
const u: AandB = new AandB;
const v: A     = u;      // upcast
const w: B     = v as B; // downcast

const x: {a:number, b:number} = {a:0, b:1};
const y: {a:number}           = x;               // upcast
const z: {b:number}           = y as {b:number}; // downcast, ERROR

Playground

Expected behavior:

Both downcasts compile without error.

(Alternatively, neither downcast compiles.)

Actual behavior:

The class downcast compiles. The object literal downcast for z does not compile.

FYI, u: A & B also works

Workaround:

Upcast to Object first.

const x: {a:number, b:number} = {a:0, b:1};
const y: {a:number}           = x;
const z: {b:number}           = y as Object as {b:number};

Activity

  1. RyanCavanaugh commented on Jul 7, 2017

    @RyanCavanaugh
    Member

    That's a sidecast (?), not a downcast. There's no relation whatsoever between y's declared type {a: number} and the target type { b: number } - neither is a supertype nor subtype of the other. You can only type-assert between types where one is a subtype or supertype of the other.

  2. kitsonk commented on Jul 7, 2017

    @kitsonk
    Contributor

    And the earlier interfaces were empty, so they were simply recasts. TypeScript is a structural type system, not a nominal one.

  3. gcnew commented on Jul 7, 2017

    @gcnew
    Contributor

    Talking about sidecasting, shouldn't we take a second look at #14156? Sidecasting is allowed for literal types, which is both inconsistent and incorrect, for no apparent reason.

  4. pauldraper commented on Jul 7, 2017

    @pauldraper
    Author

    TypeScript is a structural type system, not a nominal one.

    Ah.

    That's a sidecast (?), not a downcast.

    In C#

    interface A {}
    interface B {}
    class AandB : A, B {}
    public class Program {
        public static void Main() {
            AandB x = new AandB();
            A y = x;      // upcast
            B z = y as B; // downcast/sidecast/whatever
        }
    }

    Similarly in Java. Can't really find anything about C# or Java sidecasting, so IDK what the proper terms are.

    You can only type-assert between types where one is a subtype or supertype of the other.

    Huh.

    const x1 = {};
    const y1 = {a:0};
    
    const x2 = x1 as {b:number}; // okay
    const y2 = y1 as {b:number}; // not okay

    That sems weird. The presence of a:number makes it no more or less likely to have b:number.

    Maybe this makes sense; it just seems foreign and odd at first glance.

  5. changed the title [-]Downcasting does not work with object literal types[/-] [+]No "sidecasting"[/+] on Jul 7, 2017
  6. kitsonk commented on Jul 7, 2017

    @kitsonk
    Contributor

    Because it is structural, and not nominal, classes and object literals are duck typed in the sense that if they look like a duck and quack like a duck, they are equivalent. A super type and a sub type are determined by their structural relationship, not their nominal relationship. This is because JavaScript behaves largely in a structural way (with the exception of instanceof operator but let's not go there).

    If you wanted to explore the assignability of interfaces and classes, you would have to have them be structurally different:

    interface A { a: string; }
    interface B { b: string; }
    class AandB implements A, B { a = 'foo'; b = 'bar'; }
    const u: AandB = new AandB;
    const v: A     = u;      // this is fine
    const w: B     = v as B; // nope, this is now a side cast

    Also, casts are casts in TypeScript. Basically if the compiler allows it, it trusts what you are saying. This is because, even with good CFA, there are some assignments that can't be tracked by the type system, because JavaScript is so malleable... So unless it knows for certain you are doing something wrong, it will stop you, but even then, then you might be tempted to cast as any to really just go a f* it, stop bugging me.

    Type {} relates to type { b: number } (I am not so good on if it is strictly super or sub) but { b: number } and { a: number } are unrelated types, therefore TypeScript disallows it. To demonstrate though how {} could actually be { b: number } at runtime, where TypeScript wouldn't notice it would be something like this:

    const x1 = {};
    Object.defineProperty(x1, 'b', { value: 0, enumerable: true, configurable: true, writable: true });

    Now, x1 contains a property b with a value of 0. TypeScript doesn't know that. Yeah, if you are coming from C# or Java, it is foreign. For those of us who have been living in JavaScript land, we are so used to a weakly typed language, that TypeScript doesn't seem so foreign.

    Of course this would work...

    const x1 = {};
    const y1 = {a:0};
    
    const x2 = x1 as {b:number}; // okay
    const y2 = y1 as {a: number; b:number}; // this is fine!
  7. pauldraper commented on Jul 7, 2017

    @pauldraper
    Author

    I completely understand structural typing (though I wasn't aware TS had it). Many type systems have it (Go, Scala).

    The main issue comes from not from structural typing but casting.

       Super
       /   \
    SubA   SubB
    

    Every type system with casting I know of (C, C++, Java, C#, Go, Scala, Closure JS, Flow) allows casting from SubA to SubB (as long as the SubA & SubB intersection is non-empty).

    But TS does not.

              {}
            /    \
    {a:number} {b:number}
    
    // okay
    const a1 = {};
    const a2 = a1 as {b:number};
    
    // not okay
    const b1 = {a:0};
    const b2 = b1 as {b:number};

    I understand what tsc is doing, but it seems highly unintuitive. Why should the presence or absence of a:number in the original type influence the castability to {b:number}?

  8. RyanCavanaugh commented on Jul 7, 2017

    @RyanCavanaugh
    Member

    Every type system with casting I know of (C, C++, Java, C#, Go, Scala, Closure JS, Flow) allows casting from SubA to SubB

    Huh?

    This isn't legal in C#. For example, you can't cast from Button to TextBox despite the common heritage of Control:
    image

    This isn't legal in Java https://stackoverflow.com/questions/21370179/java-casting-one-subclass-to-another

    Flow doesn't even allow downcasting, let alone casting between two types with no relationship. Same with Scala as far as I can tell.

    The intuition here is that your type assertion need to be plausible. It's plausible that someone gave you a HTMLElement that you know to actually be a HTMLDivElement. It's not plausible that someone gave you a Truck and you actually know it to be a Dog - there's a difference between refining to a subtype and just "let's make this a totally different type".

  9. pauldraper commented on Jul 7, 2017

    @pauldraper
    Author

    Ryan Cavanaugh (@RyanCavanaugh) it is possible, with my qualication that the SubA & SubB intersection is non-empty .

    Button and TextBox are classes and C# types are such that a Button can never, ever, ever be a TextBox (well, except for null which is simultaneously everything).

    And I completely agree that it's good to reject casts between "totally different" types that share no instances.

    But if Button and TextBox are interfaces, then the intersection is not empty and it does work.

    Same for Java, same for Scala. (I guess not for Flow -- my mistake.)


    It's not plausible that someone gave you a Truck and you actually know it to be a Dog

    It is plausible, assuming a TruckDog is possible. http://www.littletikes.co.uk/big-dog-truck.html

    Typescript says that JanitorPerson isn't castable to StudentPerson, even though Person is castable to StudentPerson and JanitorPerson is a Person. Were you to have Person, not specifically JanitorPerson, only then could you cast to StudentPerson.

    It may not technically violate the Liskov substitution principle (since that operates on instances not types), but it comes pretty close.

  10. RyanCavanaugh commented on Jul 7, 2017

    @RyanCavanaugh
    Member

    And I completely agree that it's good to reject casts between "totally different" types that share no instances.

    It is plausible, assuming a TruckDog is possible.

    You can always synthesize some type that is a subtype of two other types. The two statements of "Some assertions should be rejected" and "You should allow it if some possible go-between type may exist" are contradictory.

  11. pauldraper commented on Jul 7, 2017

    @pauldraper
    Author

    are contradictory

    Not in general. Your example proved C# rejects known impossibilities.

    You can always synthesize some type that is a subtype of two other types.

    No. For example, you can't have a number & function subtype.


    I concede the point. Any casting except upcasting is undesirable. So if TS chooses very special rules and is more awkward that other systems for casts, it's not a big deal.

    Probably there are many other things more important things do to.

  12. locked and limited conversation to collaborators on Jun 14, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Working as IntendedThe behavior described is the intended behavior; this is not a bug

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions