Closed Nevor closed 8 years ago
Clarifying my last comment... I would like to know the justification for this statement:
we need to provide a way to denote a nullable singleton type
What need does a nullable singleton satisfy?
Also isn't undefined
itself (as in the value of the expression void 0
) an instance of both:
a) a singleton type
b) a non-nullable type
So its not like they are unprecedented.
Instead it has more to do with type guards that involve property accesses. ... we will only narrow the type in the true branch. The false branch could trick us into doing a faulty process of elimination
I don't follow the reasoning.
First, for a variable whose type is an union of singletons and a guard testing one of the values (which does not involve any property access): the fact that the value can be null
or undefined
should not change the fact than in the false branch, there is one less possibility. Now if you test all possibilities (with a cascade of if-then-else or a switch), you're left with an empty union, which could still denote either null
or undefined
(if you decide so). This can be tested with further branches, or asserted by calling a function taking precisely this empty type (a soft version of "exhaustivity modulo non-nullness" checking).
Now, if the guard in on a property access (with an initial type being typically a union of objects with a discriminator field), the same approach would still work: when testing if the property has a specific value, the false branch should have the knowledge that the corresponding case(s) in the union are no longer possible. I don't see why allowing null/undefined would affect that.
@yortus The reason it is valuable to denote a nullable singleton type is the example @nycdotnet gave: https://github.com/Microsoft/TypeScript/issues/1003#issuecomment-112094477. Like you said, in order to get around this, you have to have a sentinel value ("zero" in the example you gave), which seems kind of unnatural.
Your point about undefined being a non-nullable singleton type: That is true, but the type system treats undefined specially in two important ways, which make it not function as a precedent for non-nullable types:
@jbondc Your suggestion to fold in constant propagation seems to mix too many features together, and it seems to have much wider scope than the feature calls for.
The tagged union usage is the most similar thing to variant types that Typescript can have. The problem is that there are no first class "labels" in Javascript, so we use type guards instead. And then to make a label at compile time, there is a choice between using const entities (what you suggested), or types that represent constant values (what this proposal was about).
Flowtype implemented similar feature: http://flowtype.org/blog/2015/07/03/Disjoint-Unions.html
@teppeis looks nice, and demonstrates the simplicity of coding with truly disjoint unions. According to the discussion above it appears TypeScript may not allow type narrowing in the else
case due to null
and undefined
being implicitly allowed values of every singleton type.
@teppeis , @yortus Actually, it's the same feature, the examples are very familiar since it's actually this issue that was cross posted on flow's github (https://github.com/facebook/flow/issues/135) completing an other proposal (https://github.com/facebook/flow/issues/20).
Concerning the type narrowing, the softer handling of null
and undefined
of TypeScript do not prevent us from doing it in the else
case. Actually all examples given in this blog post should work in my proposal prototype.
@Nevor regarding type narrowing that's great if it can be done in the presence of null
/undefined
. Are you talking along the lines of option 3 in this comment?
Yep, in the case of Typescript, we are already in a world where everything can be null/undefined
, I don't see the need to treat this case differently. Moreover, it's very easy to transform an "unsafe" if-then-else
into a safe switch
or if-then-else-if-then
.
Yes, there are certainly no technical limitations to narrowing a nullable in the else clause, I was just wondering whether people wanted this looseness. Sounds like the answer is yes.
Btw, @RyanCavanaugh made a good point that by this philosophy, we should also narrow in the else clause of an instanceOf guard.
Would this work with my proposal, particularly with the const enum
feature? To take the first few lines of the initial overview as an example:
const enum Result : string {
ok,
fail,
abort,
}
function compute(n : number) : Result {
if(...) {
return Result.ok;
} else if (...) {
return Result.fail;
} else {
// This couldn't be any more clear what the problem is.
return Result.crash;
}
}
Although now that I come to think about it, this could complement enum types, as enums are effectively a type union of constants. Enums are typed like classes, and type unions would be the duck-typing equivalent.
Also, shouldn't this be expanded for other primitive constants as well? I know of a few use cases for numeric constants, and a few cases where false is treated specially, while true is ignored or not used.
I see that @DanielRosenwasser is now working on this. Any way we can help with community contributions?
The original issue is addressed by #5185. the remaining part is extending the type guards to support the string literal types. but this should be tracked by a different issue.
Hey, shouldn't this result in a warning about either unreachable code or type incompatibility?
type Animal = "dog" | "cat";
function DoStuff(animal:Animal) {
if(animal === "fish")
return;
}
@Elephant-Vessel we're working on it. See #6196, though I don't know if we'll see it in 2.0.
This proposal is based on a working prototype located at https://github.com/Nevor/TypeScript/tree/SingletonTypes
String literal types extended to the whole language
This change would bring singleton types created from literal string to complete recent addition of type aliases and union types. This would hopefully satisfy those that mentioned string enum and tagged union in previous PRs (#805, #186).
This addition would be short thanks to a concept that was already implemented internally for ".d.ts" files.
Use cases
Often operational distinction of function and values are done with an companion finite set of string that are used as tags. For instance events handler types rely on strings like "mouseover" or "mouseenter".
Sometimes string are often themselves operating on a finite definite set of values that we want to convey this through specifications and have something to enforce it.
And for more advanced typing, we sometimes use types themselves as informations to guide function usages, constraining a little further the base types.
Current workarounds
There is no way to create string enum for now, the workaround is to manipulate variables assigned once and for all. This does not protect from typos that will gladly propagate everywhere a string is used.
When we want to implement tagged union types for symbolic computation, we must use some number enum coupled with subtyping and casting machinery, losing all type safety.
In general, advanced type constraint are done through classes and this put us further away from simple records that would have been used in javascript.
Overview examples
Typing specifications
Pitfalls and remaining work
Backward compatibility
This prototype is backward compatible (accepts programs that were accepted before) but in one case :
The compiler will raise an Error saying that there is no common type between "foo" and "bar". This is because the compiler only accept one of the return type to be supertype and does not widen before. We might add a special case for StringLiteralTypes and keep other types as is, or, do some widening and therefore accept empty records for conflicting records for instance.
Error messages
It might confuse users that their literal strings are mentioned as types when they are expecting to see "string" even though this difference as no incidence on normally rejected string. The compiler might display StringLiteralTypes as "string" whenever the conflict is not involved between two StringLiteralTypes.
Extending type guards
To be fully usable to distinguish records by type tags, type guards should be extended to take into account this kind singleton types. One would expect the following to work :