TypeScript: Assignment operators allow assigning invalid values to literal types

Bug Report

🔎 Search Terms

  • addition assignment string literal
  • append string literal

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about string literals

⏯ Playground Link

Playground link with relevant code

💻 Code

type Data = { value: 'literal' }

const data: Data = { value: 'literal' }
data.value += 'foo' // Appending a string is possible, despite it being a string literal type.

// data is still of type Data and can be passed along,
// even when the value has the wrong type.
useData(data);

function useData(_: Data): void {}

The same applies to any literal type (e.g. number). See #48857 for more examples.

🙁 Actual behavior

Appending a random string to the property typed as the literal "literal" is possible.

🙂 Expected behavior

I would expect an error, saying that the the type "literalfoo" is not assignable to "literal".


Ping @S0AndS0

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Reactions: 8
  • Comments: 22 (5 by maintainers)

Most upvoted comments

However, mutation of a value of a union of literal types is likely to be correct in a way that we can’t statically verify, so it’s a false positive.

I would personally be very suspicious of any code that switched between members of a finite set of known string literals by way of mutation rather than direct assignment and wouldn’t mind the (hypothetical) error that disallowed it.

My opinion of this pattern for number literals is admittedly more fuzzy though, given that TS doesn’t have range types.

See also https://github.com/microsoft/TypeScript/issues/14745#issuecomment-459881335

This is a bit of a weird spot. Mutation of a value of a single literal type is clearly wrong, but also unlikely to be something you do accidently, so it’s a low-value error despite high confidence. However, mutation of a value of a union of literal types is likely to be correct in a way that we can’t statically verify, so it’s a false positive.

The natural way to split that would be to say that mutation of non-union literals is disallowed but mutation of union literals is “presumed OK”, but this is a subtyping violation because an operation that’s allowed on T | U should also be valid on a T or a U.

This code smells really bad, though, and it’d be nice to find a more reasonable solution.

OT: To whomever it may concern, please take my personal ensurances that OP understands what they are talking about.

Much confusion here shown in initial report.

I’m sure there are more specific doc mentions, but regarding objects this mentions:

When you initialize a variable with an object, TypeScript assumes that the properties of that object might change values later.

That is, something like this is not protected and produces no errors:

  const req1 = { url: "https://example.com", method: "GET" };
  req1.url += 'toobad'

Same doc mentions later that a strange construct like this will protect against property modification:

  const req2 = { url: "https://example.com", method: "GET" } as const;
  req2.url += 'toobad'
  // Cannot assign to 'url' because it is a read-only property.

But there it is the entire object literal that is being declared constant (or actually the properties ‘readonly’)

In the second comment I was struck by:

  let example_one = 'first' as const;
  example_one   += 'hiya'

Consider the difference between that and

  let example_one = ('first' as const);
  example_one   += 'hiya'

There is no difference. Neither generates an error.

You have marked the literal string ‘first’ as constant. And every string literal is constant. You have said nothing about the variable example_one. Changing that to:

  const example_one = ('first' as const);
  example_one   += 'hiya'
  //  Cannot assign to 'example_one' because it is a constant.

fails as expected because now the variable is marked as constant.

I’m afraid I see nothing surprising here (except the { url: "https://example.com", method: "GET" } as const; wow).

Please consult the docs again. Each time more things make sense.

@tshinnic You missed the point of the issue by a staggering degree. There’s absolutely no confusion present, and I’m very aware that the object is mutable. The point is about the += operator allowing to assign incompatible types. Perhaps you should read the documentation about literal types.

You also don’t seem to know what const contexts (aka as const) do. The as const will narrow the type of the value, so the type will be "first", and not string. Here’s an example that will hopefully illustrate the issue more:

// No difference between these two:
let a = 'abc' as const;
let b: 'abc' = 'abc';

// This is an error.
a = 'foo'

// This is not an error, but IMO should be:
a += 'foo'

Or to make a more elaborate demonstration with objects using your example:

type MyRequest = { url: string, method: 'GET' | 'POST' }
const req: MyRequest = { url: '...', method: 'GET' }

// This is okay, the assigned value matches the type:
req.method = 'POST'

// This is an error, the assigned value does not match the type:
req.method = 'EXAMPLE'

// Why is this not an error? The assigned value does not match the type.
req.method += 'EXAMPLE'

@S0AndS0 That would be `prefix-${string}`:

type MyString = `prefix-${string}`

let test: MyString
test = "prefix-abc"
test = "prefix-foo"
test = "error"

Oh good, a teachable moment awaits.