0

Given a literal type, for instance 1 | 2, assigning a value to a variable that correctly corresponds to the literal type will fail.

interface SomeInterface {
    foo: (1 | 2);
}
class SomeClass implements SomeInterface {
    foo = 1; // <- generates the following compiler error:
    /*
        Property 'foo' in type 'SomeClass' is not assignable to the same 
        property in base type 'SomeInterface'.
        Type 'number' is not assignable to type '1 | 2'.
    */
}

However, explicitly declaring the literal type will satisfy the compiler.

interface SomeInterface {
    foo: (1 | 2);
}
class SomeClass implements SomeInterface {
    foo: (1 | 2) = 1; // <- no error
}

Type assertion may also be used:

foo = 1 as (1 | 2); // <- no error
// or
foo = <1 | 2>1; // <- no error

For primitive types, it should be trivial for the compiler to know that the value 1 is of type (1 | 2) (heck, even just of type (1)!). Is there a design reason why TypeScript's compiler does not implicitly infer literal types from literal values?

  • More generally, there comes a point at which the cost of precise language tunings exceeds the benefits, a phenomenon we call "diminishing returns." Language design and feature implementation are expensive, companies that create programming languages do not have unlimited resources, and sometimes good enough is good enough. In other words, there doesn't have to be a good reason for a particular language design choice. – Robert Harvey Jul 26 '18 at 15:22
  • 1
    With all due respect, I'm sorry if you think that the need to drill down the exact cause of this documented issue is pedantic, but the purpose of downvoting is not for subjective dislike, but for malformed questions or factual errors. Your nebulous mention of diminishing returns is irrelevant to the topic, and your namecalling is inappropriate. – Jacob Stamm Jul 26 '18 at 15:39
  • Might I point out that holding back on an article that describes this issue in excruciating detail and then asking why on a Stack Exchange site is more than a little disingenuous? You appear to already know the answer to the question you asked, making this a "stump the chump" question. I reiterate: this isn't a discussion forum. Don't ask questions of people if you're just going to argue with their conclusions. – Robert Harvey Jul 26 '18 at 15:45
  • 2
    Why all the assumptions? I did not know know about the TypeScript defect before asking this question. @amon's explanation made me understand the root issue, which led me to find the GitHub issue, which I then relayed in the solution. Is there a problem with this process? – Jacob Stamm Jul 26 '18 at 15:48
  • I doubt that the downvoters read your conversation with @amon before downvoting. Those that did probably figured that you didn't do your homework before asking, a perfectly valid reason to downvote (hover your mouse over the downvote button). But again, I'm still merely speculating. At the end of the day, the only people that really know the answer to your question are the language designers themselves, and they're probably not here. – Robert Harvey Jul 26 '18 at 15:54
4

Is there a design reason why TypeScript's compiler does not implicitly infer literal types from literal values?

You can't do it in C# either:

SomeEnum s = 1; // Fails, even though one of the enum values corresponds to 1.

SomeEnum s = (SomeEnum)1; // succeeds.

Generally speaking, implicit casting must not cause loss of information. Implicit casting from, say, int to long is allowed because long has a much greater range than int.

But if your programming language implicitly casts 1 to 1 | 2, it takes a type (an int) that can be one of 4 billion possible values, and changes it to a type that can only be one of two possible values. Such an implicit cast can easily fail at runtime, if the number you provided is out of range.

If, on the other hand, you explicitly cast to the enum type, you're essentially telling the compiler "Allow the cast; I know what I'm doing."

  • Your explanation makes sense for the type assertion, but what about having to redeclare the type to the same type that's already specified in the interface? It seems to me that if casting is the issue, then foo: (1 | 2) = 1 should still produce an error, but it doesn't. It's not like the compiler doesn't already know that foo is (1 | 2) — it already has that info from the interface. This behavior seems inconsistent. Is there some basic understanding here that I'm lacking? – Jacob Stamm Jul 25 '18 at 20:37
  • 1
    @Jacob this behaviour indicates that the type checker does not inherit this info from the interface, but first type-checks the class (incl. type inference for foo) and then checks whether the class conforms to the interface. It does not conform because the type of a read-write variable must be invariant. – amon Jul 25 '18 at 21:37
  • @amon "It does not conform because the type of a read-write variable must be invariant." I followed you up until this part. Could you explain this further? – Jacob Stamm Jul 25 '18 at 22:08
  • 1
    @JacobStamm That's type theory speak for "the type is not allowed to change in subtypes". This follows from the Liskov Substitution Principle. For a writable variable, the subtype can widen the variable type, i.e. accept more values (contravariance). For a readable variable the subtype can narrow the variable type, i.e. promise a more specific type (covariance). A read-write variable must satisfy both these constraints, this is only possible if it doesn't change at all (invariance). In your example the type of foo is widened from an enum to all numbers. – amon Jul 25 '18 at 22:25
  • 1
    @JacobStamm Sorry I don't have the time for writing a proper answer. And I don't actually know anything about TypeScript, I'm afraid. Now that you understand what's going on you could answer your own question! – amon Jul 26 '18 at 13:58
3

From @amon's comment:

this behaviour indicates that the type checker does not inherit this info from the interface, but first type-checks the class (incl. type inference for foo) and then checks whether the class conforms to the interface. It does not conform because the type of a read-write variable must be invariant.

It looks like the actual issue here is that classes do not properly infer the types of their members from interface(s) they implement. The issue is not casting. If it were, then foo: (1 | 2) = 1 in the implementing class would not compile. The compiler does know that 1 is a valid value of type 1 | 2, but the default type of 1 is number. Without the class knowing the type of foo by looking at the interface (which is really what it should be doing), foo = 1 tries to type foo as a number, and at this moment the interface decides to do its job by disallowing compilation because number is not of type 1 | 2... better late than never, I suppose.

Type assertion should almost never be used with literals. foo = 3 as (1 | 2) does not produce a compiler error, allowing an accidental typo to break an application, while foo: (1 | 2) = 3; does not compile, which in most cases, is desired behavior.

  • 2
    Yes, it's a question of using the info beforehand for inference, or afterwards for error-checking. Both make sense, depending on whether you value comfort or explicitness (not safety) more. It would be a different story if one tried to assign from a variable instead of interpreting a literal in the program. – Deduplicator Jul 26 '18 at 15:18
  • Isn't foo: (1 | 2) = 1 an explicit cast? Nobody made the assertion that such a cast could not succeed. foo = 3 as (1 | 2) is not a compiler error because you've told the compiler to do the cast anyway, that you know what you are doing. It's still an error, of course, but you've deferred the error to runtime. – Robert Harvey Jul 26 '18 at 15:24
  • No, foo: (1 | 2) = 1; is not a cast. The value 1 is assignable to the primitive type 1 | 2, just as it is assignable to type number. – Jacob Stamm Jul 26 '18 at 15:35
  • 1
    @Deduplicator The question is "Why don't variables in TypeScript implicitly infer literal typing?" The answer is this TypeScript behavior, and the workaround is to explicitly redeclare the property type in the class as defined in the interface. Casting is closely related, but not the answer to the question. – Jacob Stamm Jul 26 '18 at 15:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.