4

Having a Typescript variable of union type A

type A = {
    b: true
    x: number
  } | {
    b: false
    x: string
  }

declare const v: A

i can properly assign property x to correct type, by checking for property b value type with an if discriminant block to protect type A consistency

if (v.b) {  // v.x is number

  // ok for compiler 
  v.x = 3   

  //  compiler error as v.x should be number 
  v.x = ''

} else { // v.x is string

  //  compiler error as v.x should be string 
  v.x = 3   

  // ok for compiler 
  v.x = ''  
}

outside discriminant block v.x correctly appears to be number | string
however, compiler doesn't complain assigning x to number | string despite that would break type A consistency

v.x = 3   // ok for compiler 
v.x = ''  // ok for compiler 

Is there a way to force the compiler to reject this?
check it out on typescriptlang.org/play

aleclofabbro
  • 1,635
  • 1
  • 18
  • 35
  • 1
    While I do think this is an interesting question on its own, at the same time this does feel like an [XY problem](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem) so maybe it would be useful if you provided a bit of motivation or background of what you're trying to achieve. Maybe there's a more appropriate syntax you could be using. – apokryfos Sep 13 '19 at 12:56
  • 1
    You can use Generic type aliases: https://www.typescriptlang.org/docs/handbook/advanced-types.html – Oleg Sep 13 '19 at 13:07
  • @apokryfos This is a general issue, it can be simply resolved when developer carefully check for discriminant properties... The point is that I'd expect TS compiler to preserve types consistency - at compile time - when types are well defined, after all this is one of the main Typescript purposes... The obvious scenario is: a dev-team is working on a codebase involving `type A` and I'd like to drop runtime-errors as much as possible I just wonder if there's a way to avoid this issue or if it is a Typescript functional lack – aleclofabbro Sep 13 '19 at 13:55
  • @Oleg could you please expand your suggestion? – aleclofabbro Sep 13 '19 at 13:57
  • Wow, I'm completely failing to find a relevant issue in GitHub even though I'm sure such a thing has been noticed before. It seems like a design limitation to me (maybe it needs to stay this way to allow the majority of cases which are perfectly acceptable), but I'm surprised I haven't found any authoritative discussion about this yet. – jcalz Sep 13 '19 at 15:08
  • 1
    I mean, [microsoft/TypeScript#9825](https://github.com/microsoft/TypeScript/issues/9825) talks about the *general* case of unsound TypeScript behavior and why it can't be eliminated, but I can't be certain that it's relevant to this particular instance. – jcalz Sep 13 '19 at 15:38
  • @jcalz I don't think this issue is related to TS' type-system 'un-soundness' .. as I recall, soundness concerns discriminating between types with same structure.. which is not this case – aleclofabbro Sep 13 '19 at 16:30
  • In general, a *sound* type system will only let you do type-safe things, and a *complete* type system will only prohibit you from doing type-unsafe things. TypeScript's type system is both unsound (lets you do some unsafe things) and incomplete (stops you from doing some safe things). In this case, the compiler is not stopping you from assigning to `v.x` even when it is not safe to do so... thus it is an example of unsoundness. – jcalz Sep 13 '19 at 17:39

2 Answers2

1

Okay, so I think I've found the canonical GitHub issue about this: microsoft/TypeScript#14150, a suggestion that "unsafe type-incompatible assignments should not be allowed". It's still an open issue (as of 2019-09-13) marked as "awaiting more feedback", so if you think you have a compelling use case that's not already mentioned in there you might want to comment in there. I wouldn't hold my breath waiting for this to be implemented, though, since the related issues like enforcing readonly strictness via flag flag and enabling variance annotations are either closed or haven't been acted upon.

The problem here involves type system's lack of soundness. A sound type system would only let you do safe things. But here it lets you make a property assignment to an object that might violate the object's declared type. This unsafe permissiveness means the type system is unsound. That, by itself, is not considered a bug. It is not one of TypeScript's design goals to "apply a sound or 'provably correct' type system". There is a tradeoff between correctness and productivity, and it is quite possible that fixing this issue might be more trouble than it's worth. See microsoft/TypeScript#9825 for more discussion about TypeScript's soundness and/or lack thereof.

The particular unsoundness here: the compiler assumes that it is safe to write the same type to a property that you can read from it. This is not true in general, as shown in your example, and in this related example from the linked issue:

interface A { kind: "A"; foo(): void; }
interface B { kind: "B"; bar(): void; }

function setKindToB(x: A | B): void {
    x.kind = "B"; // clearly unsafe
}

So what can be done? Not sure. TypeScript 3.5 introduced a change to indexed access writes (such as foo[bar] = baz) so that if the key is of a union type (say bar is Math.random()<0.5 ? "a" : "b") then you must write the intersection of the property types to it, not the union (so the type of baz must be typeof foo.a & typeof foo.b and will no longer accept typeof foo.a | typeof foo.b). This is a soundness improvement that prohibits some invalid things which were previously allowed. And it also prohibits lots of valid things which were previously allowed. And lots of people are still upset about it and new issues about it are still filed fairly frequently. I imagine the same problem would happen here if they fixed this issue... you would get the error you expect, and lots of code bases would break. For now I'd say you should probably just avoid doing these assignments, which I understand is not much consolation.

Anyway, hope that information is of some use to you. Good luck!

jcalz
  • 264,269
  • 27
  • 359
  • 360
0

Code sample for Generic type aliases in your case : check it out on typescriptlang.org/play

Oleg
  • 3,580
  • 1
  • 7
  • 12
  • 1
    That's no longer a discriminated union, unfortunately... or a union at all. I assume the use case here is that the variable in question must be of a union type and cannot be narrowed via annotation. @aleclofabbro can correct me if I'm wrong, though. – jcalz Sep 13 '19 at 15:45
  • Oleg as @jcalz pointed yours is a different use case, not involving a union type at all – aleclofabbro Sep 13 '19 at 16:23