Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

No "sidecasting" #16995

Closed
pauldraper opened this issue Jul 7, 2017 · 10 comments
Closed

No "sidecasting" #16995

pauldraper opened this issue Jul 7, 2017 · 10 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@pauldraper
Copy link

pauldraper commented Jul 7, 2017

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};
@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Jul 7, 2017
@RyanCavanaugh
Copy link
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.

@kitsonk
Copy link
Contributor

kitsonk commented Jul 7, 2017

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

@gcnew
Copy link
Contributor

gcnew commented Jul 7, 2017

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.

@pauldraper
Copy link
Author

pauldraper commented Jul 7, 2017

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.

@pauldraper pauldraper changed the title Downcasting does not work with object literal types No "sidecasting" Jul 7, 2017
@kitsonk
Copy link
Contributor

kitsonk commented Jul 7, 2017

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!

@pauldraper
Copy link
Author

pauldraper commented Jul 7, 2017

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}?

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Jul 7, 2017

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".

@pauldraper
Copy link
Author

pauldraper commented Jul 7, 2017

@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.

@RyanCavanaugh
Copy link
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.

@pauldraper
Copy link
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.

@microsoft microsoft locked and limited conversation to collaborators Jun 14, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

4 participants