Skip to content

Assigning to a wider union type breaks type safety #55451

Closed as not planned
Closed as not planned

Description

🔎 Search Terms

invalid, type safety, union, type, broken type, no error

🕗 Version & Regression Information

  • This is an invalid type checker behaviour
  • This never changed, it was "always" like that >3.3.3
  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about 3.3, 3.5, 4.5, 5.0, 5.2.0-beta

⏯ Playground Link

https://www.typescriptlang.org/play?ts=5.2.0-beta#code/C4TwDgpgBAcgrgWwEYQE4HkB2AbEBhAe02AEMBLTNKAXigG8AoKKMVAsALik0RVQG4GAXwYNsEYFBKYQwABYUA5l0bNW7LgGdgqJVAA+3XmkFDBYiVAQEAJlC7xkaLLkLFylVDXrrOUAIxmotKyCpiK3tY2-MyxcfEJiQD0SWSSmnIEcNh2AGbk2FByaBBcAERR3ASSJJqaZIqYJEjiUMAEUjLyeigAxiRwmtAAFNq64QZGTqgAlFBkmlU1dQ1NLdDtU3xlDCHd4QB0vt5lNhC5FBK4S1JbaGUxKQReaVAZWTncJKhsAO5txSgZRIvWAcBIhVAkDKbQ6dF8XB40yEnTyBSKJXKezCiiObDA80WTR+BF+EDsmyRfAANKi3jo9AsbrV6o1mq1KcZUDsGL0iJoCOIDtgCIphsC7l5ygBqKJ49gzR6pTBVKD9IZvTLZOxy44oKTrWE3fUkSVQYYQAAevQgYEkuWeLGwHigACsSAA3Wq9XR2qzSMhgbIkYBkIgzIA

💻 Code

type NumberOnlyContainer = {
  prop: number;
}

let anything: {
  prop: string | number;
};

let mod : NumberOnlyContainer = {prop: 1};

anything = mod;                           //it should fail here: "mod not assignable to anything because (string | number) is not assignable to number"
anything.prop = "definetly not a number"; //or it should narrow the "actual type" to {prop: number} and fail here: "anything.prop is narrowed to number, and string is not assignable to number"

console.log("a number : "+mod.prop); //in no case should mod.prop be able to not be a number

The result will allways be a number : definetly not a number which is a violation to the types since mod should be of type NumberOnlyContainer which's prop should only be able to be a number

🙁 Actual behavior

The code compiles / ts does not complain

🙂 Expected behavior

The code should no "compile/transpile" since it is invalid and breaks type safety.
It should not be possible to effectively expand/redefine a variable by assigning its value to another mutable variable.

The only way where this behaviour should be allowed is if the wider type ((string|number)[] for example) is readonly since you cannot break type safety with it.

Options to prevent this:

  • Disallow assigning the value of a variable of type A to a variable defined as type B if
    1. A & B are reference types
    2. B is mutable (anything except a readonly array)
    3. B is a "wider" type than A
  • Narrow down/merge the type of a variable of type B (down to A) when a value of type A is assigned to it
  • (currently not possible since readonly only works for arrays) A variable of type B is narrowed to readonly B when a value of type A is assigned to it and:
    1. A & B are reference types
    2. B is mutable (anything except a readonly array)
    3. B is a "wider" type than A

All of these solutions would work for all the use cases (when considering passing a var to a function as a variable assignment)

Additional information about the issue

There are a lot more examples where this missing check should fail:

Since this behaviour (or rather the lack thereof) is pretty severe, I would be surprised to see that this is not a duplicate - that said, i do not found a matching issue. I guess it has already been reported or is at least known. If so - link the issue and close this one, maybe i am just too small brained to find the correct issue.

Activity

  1. RyanCavanaugh commented on Aug 21, 2023

    @RyanCavanaugh
    Member

    Properties are assumed to be covariant (i.e. only read from); writes to supertype aliases can be unsound. We don't really have intent to try to "fix" this; it's an accepted unsoundness.

  2. stafyniaksacha commented on Aug 22, 2023

    @stafyniaksacha

    I've made another example, which I think it's related (let me know if it's not):

    interface A {
      name: 'A'
    }
    
    interface B {
      name: 'B'
      field: string
    }
    
    interface C {
      inner: A | B
    }
    
    const element: C = {
      inner: {
        name: 'A'
        // field not available, inner is infered as A
      }
    }
    
    // assigning inner to another type from the union
    element.inner.name = 'B'
    
    // @ts-expect-error - element.inner still infered to A
    element.inner.field = 'test'
    
    if (element.inner.name === 'B') {
      // works with unnecessary check due to assignation above
      element.inner.field = 'test'
    }

    view on playground

  3. typescript-bot commented on Aug 25, 2023

    @typescript-bot
    Contributor

    This issue has been marked as "Working as Intended" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

  4. locked as resolved and limited conversation to collaborators on Oct 22, 2025
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