Closed Nevor closed 8 years ago
I _really_ like the idea of creating tagged unions though literal types.
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
radius: number;
}
type Shape = Rectangle | Circle | Line;
function area(s: Shape) {
if (s.kind === "square") return s.size * s.size;
if (s.kind === "rectangle") return s.width * s.height;
if (s.kind === "circle") return Math.PI * s.radius * s.radius;
}
Per already existing rules for members of union types, the type of s.kind
would be
"square" | "rectangle" | "circle"
Type guards could relate the common kind
property to the corresponding object type in the union and narrow the type appropriately in the guarded block.
This idea could extend equally well to const enum types from #970.
const enum ShapeKind { Square, Rectangle, Circle }
interface Square {
kind: ShapeKind.Square;
size: number;
}
interface Rectangle {
kind: ShapeKind.Rectangle;
width: number;
height: number;
}
interface Circle {
kind: ShapeKind.Circle;
radius: number;
}
type Shape = Rectangle | Circle | Line;
function area(s: Shape) {
if (s.kind === ShapeKind.Square) return s.size * s.size;
if (s.kind === ShapeKind.Rectangle) return s.width * s.height;
if (s.kind === ShapeKind.Circle) return Math.PI * s.radius * s.radius;
}
:+1: for using const enum types as the discriminator, this is the most ideal scenario for us.
I've wanted something like this since we first got union types. The problem is that much of our compiler uses many-to-many relationships between discriminators and types.
interface UnaryExpression extends Expression {
kind: SyntaxKind.PrefixExpression | SyntaxKind.PostfixExpression;
operator: SyntaxKind.PlusToken | SyntaxKind.MinusToken | SyntaxKind.TildeToken | /*...*/ | SyntaxKind.VoidKeyword;
operand: Expression;
}
interface BinaryExpression extends Expression {
kind: SyntaxKind.BinaryExpression
left: Expression;
operator: SyntaxKind.PlusToken | SyntaxKind.MinusToken | /*...*/ | SyntaxKind.AsteriskToken;
right: Expression;
}
function doSomething(expr: Expression) {
switch (expr.kind) {
case SyntaxKind.PrefixExpression:
case SyntaxKind.PostfixExpression:
/* 'expr' is a 'UnaryExpression' here */
break;
default:
/* 'expr' is still an 'Expression' */
}
switch (expr.operator) {
case SyntaxKind.AsteriskToken:
/* 'expr' is a 'BinaryExpression' here */
break;
case SyntaxKind.TildeToken:
/* 'expr' is a 'UnaryExpression' here */
break;
case SyntaxKind.PlusToken:
/* 'expr' is a 'UnaryExpression | BinaryExpression' here */
break;
default:
/* 'expr' is still an 'Expression' */
}
}
This isn't a problem from a usability standpoint, but rather, it's probably difficult to implement, though I haven't thought much about it. It's probably worth limiting the scope on something like this anyhow.
Edited to reflect exclusivity of case clauses.
This also gives us a very easy way to reason about the desirable memberof
operator, as its definition would simply be "the union type consisting of all the property names of the given type":
interface MyModel {
name: string;
type: number;
isReady: boolean;
}
// ember.d.ts
function get<T>(model: T, memberName: memberof T): any { }
var m: MyModel;
get(m, 'isready'); // Error, cannot convert 'isready' to 'name' | 'string' | 'isReady'
@DanielRosenwasser It's not that difficult to implement, a lot of the infrastructure is already there. As your example highlights, we would want type guards to also support switch statements. Again, not too complicated.
One thing that is interesting is that Expression
would no longer be a base type, but rather a union type of all possible expression types. And those expression types would no longer inherit a kind: SyntaxKind
property, they would all have to introduce it themselves (with a particular set of values).
I have updated my prototype to accept the first program of @ahejlsberg. The type guards where extended as such :
For any predicate of the form ident.prop === expr
with ident
of union type and expr
of literal type, ident
is narrowed to retain only types that are assignable to { prop : typeof expr }
if typeof expr
is smaller that typeof prop
.
type Ready = { kind : "ready" };
type Finished = { kind : "finished"; result : number };
type Aborted = { kind : "aborted" ; error : string };
type Status = Ready | Finished | Aborted;
var s : Status;
if(s.kind === "ready") {
// treated as Ready
} else {
// treated as Finished | Aborted
if(s.kind == "aborted") {
// treated as Aborted
console.log(s.error);
}
}
The commit of these changes is there : https://github.com/Nevor/TypeScript/commit/d288ece46155f550a99c0f22a0dacfb29848cb58
@Nevor very cool! What do you do in the following case?
type Ready = { kind: "ready" };
type ReallyReady = { kind: "ready", howReadyAreYou: string }
/* ... */
type Status = Ready | ReallyReady | Finished | Aborted;
var s : Status;
if(s.kind === "ready") {
// treated as ???
} else {
// stuff
}
In this case ˋsˋ will be treated as Really | ReallyReady
and indeed we can't do nothing more with it.
For the sake of demonstration we can imagine this instead :
type Ready = { kind : "ready" ; really : "no" };
type ReallyReady = { kind : "ready" ; really : "yes"; howMuch : number };
and then use two guards either separated or with the already supported operator &&
if(s.kind === "ready") {
// here Ready | ReallyReady
if(s.really === "yes") {
// here ReallyReady
}
}
if(s.kind === "ready" && s.really === "yes") {
// here ReallyReady
}
@Nevor that is really awesome. I think we may need to discuss it a bit tomorrow/in the upcoming week, but given that this brings us pretty close to generalized user-defined type discriminators, I like it a lot.
While working on switch/case
guards, I have realized that my algorithm for if-then-else
guards was wrong, it did not take into account variance of properties, only variance of record width. The guards were therefore too strong in there constrain. So I have switched to a filter that directly compares properties type in union of records.
That being said, I have been able extend my prototype with switch/case
guards. This implementation narrows guarded types as expected while supporting fall through and mixed types as demoed bellow.
Guarding and fall through :
type Ready = { kind : "ready" };
type Finished = { kind : "finished"; result : number };
type Aborted = { kind : "aborted" ; error : string };
type Status = Ready | Finished | Aborted;
var s : Status;
swtich (s.kind) {
case "aborted":
// v as Aborted
case "finished":
// v as Aborted | Finished
break;
case "ready":
// v as Ready
}
Mixed types and fall through :
type Number = { value : number };
type Null = { value : "null" };
type Special = { value : "special" };
type Value = Number | Null | Special;
var v : Value;
switch (v.value) {
case "null" :
// v as Null
case 0:
// v as Null | Number
break;
case 3:
// v as Number | Special
break;
case "special":
// v as Special
case 10:
// v as Number
case default :
// v as Number
}
One might ask what happens when there is nothing left to consume in the union-type in either the if-then-else
or switch/case
guard :
type Value = { value : "value", content : string };
var v : Value;
if(v.value === "value") {
....
} else {
v.content // ???
}
type Ready = { value : "ready" };
type NotReady = { value : "notready" };
type Value = Ready | NotReady;
var v : Value;
switch (v.value) {
case "ready":
break;
case "notready":
break;
case "ready": ???
break;
}
In the current implementation the empty union type is transformed to the type void
, restricting any use of the guarded type as expected. This give us a free alarm on dead code where a union type totally consumed will have an empty type with which nothing can be done.
Further more, if we declare the accidentally allowed function impossible(v : void) { throw "impossible"; }
, we have just crafted a cheap and javascript compatible exhaustiveness checker :
switch (v.value) {
case "ready":
break;
case "notready":
break;
default:
impossible(v); // okay as long as the type is not extended
}
Another solution would be to consider the type as full or any.
The discussed changes can be found in the commits https://github.com/Nevor/TypeScript/commit/5c8c82d79ba2eff3c02033540fa797e5c36d2791, https://github.com/Nevor/TypeScript/commit/d82f8f5bfc055be04d3bb7e4f69767ad7f35d626 and https://github.com/Nevor/TypeScript/commit/fd5c4017d7a98e0f035b35b05471c4d8da7aef49.
For simplicity the fall through is broke only in presence of a break
or return
as last statement of a case clause, extending the code to visit all execution paths should not be difficult.
@Nevor Can you use these new types with function overloading? e.g.
type mouseEvents = "mouseup" | "mousedown" | "mousemove";
type pointerEvents = "pointerup" | "pointerdown" | "pointermove";
function simulate(element: EventTarget, event: mouseEvents);
function simulate(element: EventTarget, event: pointerEvents);
function simulate(element: EventTarget, event: string);
function simulate(element: EventTarget, event: string) {
if (mouseEvents.test(event)) {
// create a mouse event
} else if (pointerEvents.test(event)) {
// create a pointer event
}
}
Addition/Suggestion:
I would be cool if we could add a .test()
method (or some other syntax) which would do a runtime check. It shouldn't be too hard to emit a regex test since all the possible values for the type are known at runtime.
Yep, they can be used with overloading, when running your simplified code through our prototype we get this :
type mouseEvents = "mouseup" | "mousedown" | "mousemove";
type pointerEvents = "pointerup" | "pointerdown" | "pointermove";
function simulate(element: EventTarget, event: mouseEvents);
function simulate(element: EventTarget, event: pointerEvents);
function simulate(element: EventTarget, event: string) {
/* ... */
}
var e : EventTarget;
simulate(e, "mousedown"); // OK
simulate(e, "pointerup"); // OK
simulate(e, "foo"); // Type error
simulate(e, 10); // Type error
/* etc */
I removed the event: string
case to really show that the typing works. Overloading working is expected because string literal types already exist in Typescript declaration files, so our extension of this to implementation files shouldn't have broke overloading.
Concerning the test
function, yes it should not be too hard indeed but in more general way, it's joining the idea of user defined type guards proposed in this issue https://github.com/Microsoft/TypeScript/issues/1007, that might interest you.
Given:
interface Factory {
kind: string;
}
interface Canary {
sing(): string;
}
interface CanaryFactory extends Factory {
kind: "canary";
make(colour: string): Canary;
}
var myFactory: CanaryFactory = {
kind: "canary", // <----------------- have to specify this
make(colour) {
return {
sing() {
return "I'm a " + colour + " canary";
}
};
}
};
It seems odd that in the object literal I assign to myFactory, having imposed the type CanaryFactory on it, I have to explicitly repeat kind: "canary"
. It's like a piece of mandatory boilerplate. Would it be possible to just omit it in the TS and generate it for me in the JS?
@danielearwicker one of our aims is to avoid adding in runtime type information when it's not explicitly requested by the user.
This thread is simply amazing.
@DanielRosenwasser Sure. I guess what I'm saying is that by stating that myFactory is a CanaryFactory, I have explicitly requested that RTTI. By adding the literal string type to CanaryFactory, I've turned it into an alias for requesting that RTTI on anything that implements CanaryFactory, in that my program won't compile without it, but I have to repeat it. Unlike an interface method (where the body varies between implementations), this must always have the same "implementation". It's implied.
So It's not the requesting it I'm wondering about, it's needing to request it twice.
In which I have to say beetlejuice three times:
interface Beetlejuice {
kind: "beetlejuice";
}
class MichaelKeaton implements Beetlejuice {
kind: "beetlejuice" = "beetlejuice";
}
console.log(new MichaelKeaton().kind);
If I leave out the assignment = "beetlejuice"
, the program prints undefined
. Perfectly consistent (if confusing at first glance), because we're writing a class, not an object literal, so in this context the colon means "of type", not "initialised with". It's also consistent (if unnerving) that it still compiles in that state: class members are always implicitly optional (and cannot be explicitly/redundantly declared optional), even when implementing a property that is non-optional. It just seems like quite a ceremony, albeit a perfectly consistent one.
BTW if I just assign without declaring the type:
class MichaelKeaton implements Beetlejuice {
kind = "beetlejuice";
}
Then:
Types of property 'kind' are incompatible.
Type 'string' is not assignable to type '"beetlejuice"'.
Is that just a glitch in the prototype? Assignment at the point of introducing a property decides its type, and I guess that process isn't influenced by the interface's requirements.
@danielearwicker, first off, major :+1: for your example. Clearly we don't want anyone writing "Beetlejuice" three times or else we'll end up singing the banana boat song. ;)
The syntax within a class is a good point. In some sense, one could argue people shouldn't be using classes in that fashion - a big idea behind OO is that you can tackle the other side of the expression problem. Still, a tag check can be much faster, is often more convenient, and you raise a good point - there's some obviously redundant information there in the type, and it's probably an error to omit the instantiation.
I think such a suggestion might warrant its own issue, separate from (but contingent on the acceptance of) this one. For instance, what would we do in the following case?
class SchrödingerCat {
state: "Dead" | "Alive"
}
@danielearwicker about your question on incompatible types, this is by design. The type inference for your field kind
is done on the assignation and then the inferred type of the class is matched with expected types (here the interface Beetlejuice
). For backward compatibility I have chosen to widen literal types when inferring assignations. This is to avoid this kind of problem :
var foo = "foo";
if(pred) {
foo = "bar"; // would not compile with type error
}
// the same applies to classes
class Bar {
name = "unknown";
}
new Bar().name = "John"; // would give also a type error
Without this limitation, a lot of currently valid Typescript would not compile. A solution would be to infer types on all usages of a variable. An other solution would be to use some trick of keeping two types (string and literal) and then widen on usage. Both of these would require a complete rework of Typescript's type checker, this is another issue on itself.
@DanielRosenwasser Me say day!
This feature is already excellent - I have had great results writing a spec of a JSON dialect that specifies queries, made of nestable expressions. So you're right, this repeating-myself issue should be separate. It's a minor thing.
Re: "Dead" | "Alive", a union type doesn't have a unique thing to be auto-initialized to (by definition) so I wouldn't expect any auto-initialization to happen. It only makes sense for a simple string-literal type, which has this interesting property that it can only be initialised one way - well, apart from null
, undefined
and absolutely anything with an <any>
in front of it! But only one overwhelmingly useful way.
@Nevor - Thanks. That makes sense for ordinary variables and new properties introduced in a class. But what about a class property P = "P"
that has the same name as a property P: "P"
defined in an interface that the class is marked as implementing? Does the current structure of the compiler make it hard for it to deduce that P
is already constrained to "P"
and therefore should not (indeed, cannot) be widened to "string"
?
:+1: having this feature would be invaluable, I'm sorry to ask that but is it scheduled for a typescript version ?
We still need to talk about it a bit further but the idea was well liked. At the moment our near term schedule is fairly full with ES6 related features though.
Thanks for working this out @Nevor, another big thumbs up for this suggestion! Our codebase could really benefit from this feature.
Most people coming from functional languages may first think of an addition closer to #186 (crazy thoughts of a TypeScript pre-processor were thought). But this tagged union (literally) approach seems much more idiomatic and a perfectly natural extension of TypeScript.
Are there any steps we could take to help with this? Any open questions to explore? What is missing?
:+1:
Accepting PRs. With something like this, please include lots of tests with your PR :smile:
Yay! :grinning:
Can we clarify the intended scope? As far as I can tell, this issue brought up the following main ideas:
foo.kind === "name"
)switch
-case
statements to serve as type guardsWhich of these are within scope for a suggested implementation? Does it make sense to tackle 1. and 2. together? Should 3. be treated as part of #1007?
1 and 3 only, I think. 2 is probably separable; 4 is covered by #2388
We may consider this for boolean literal types as well. They could be very useful for generators, as per issue https://github.com/Microsoft/TypeScript/issues/2983
I'm wondering whether this would allow forwarding to string literal overloads such as document.createElement
. treating such overload sets like a reusable way to map runtime strings to compile-time interfaces, so for example, the caller can specify the type once (as a string literal) and have it checked both at compile and runtime:
function safeQuerySelector<TTagName /* constraint? */>(sel: string, tagName: TTagName) {
// To discover what createElement would return, given string type tagName
var elem = ((x?: void) => x ? document.createElement(tagName) : null)();
// actually do the query
elem = <typeof elem>document.querySelector(sel);
// validate that the found element is of the desired type
if (!elem || elem.nodeName !== tagName.toUpperCase()) {
throw new Error(`Selector ${sel} does not identify a ${tagName} element`);
}
return elem;
}
var a = safeQuerySelector(".crazyLink", "a");
a.href = "blah"; // fine, as safeQuerySelector returned HTMLAnchorElement
But TTagName
needs a constraint to ensure that it is a string. TTagName extends string
is presumably wrong because it allows anything structurally compatible with string
, which is not strict enough for DOM methods that want a real string.
Wouldn't be ideal to have to manually write down the union of all parameters acceptable to createElement
:
function safeQuerySelector(
sel: string,
tagName: string|"a"|"abbr"|"acronym"|"address"|"applet"
//... and so on for ~100 cases
@danielearwicker Actually, extends string
would mean that it is really a string. Something structurally compatible with a string does not satisfy extends string
. The one you are thinking of is extends String
(note the capitalization). That is the structural one.
I'm just wondering how it should be dealt with if we want to extend the type with more string literals.
Use case:
type Event = "click" | "drag" | "touch";
declare function addEventListener(event: Event);
addEventListener("click");
createEvent("dbclick");
addEventListener("dblick"); // error
We don't have a way to augment a type declared by a type alias, whether or not it's a union type. You'd have to do what @jbondc suggested.
I'd like to clarify something about my interpretation of the proposal. Somebody please correct me if I'm wrong:
string
number
, string
is neither assignable to nor a subtype of a string literal type.(5). The apparent type of a string literal type is the String
interface type.
No, you are talking about constant propagation, which is based on how a certain entity is declared (whether it has the const
keyword or not determines whether it is a string literal type or not). I am talking about flowing this information via types, and doing the discrimination based on contextual typing info. This is what we have done for tuples vs arrays. Namely, an array literal contextually typed by a tuple type has a tuple type. In a similar way, a string literal contextually typed by a string literal type has a string literal type (the thing you are calling a set type). This is in order to maintain backward compatibility.
I will add that unless I misunderstood your proposal, the difficulty is that it does not maintain the same overload selection rules when you pass a string literal as an argument to a call.
Yes, while it doesn't seem like typing const
-declared strings as string literal types would be a problem, it unfortunately is:
function foo() {
const a = "hello";
return a;
}
var x = foo();
x = "world";
Here, a
would be typed as "hello"
, so foo
's return type is now "hello"
. In turn, x
is typed as "hello"
, but now x
cannot be reassigned any other string.
@DanielRosenwasser With the contextual typing approach, you could still run into a similar thing. I'm modifying your example a bit to add a type annotation:
function foo(): "hello" {
return "hello";
}
var x = foo(); // Inferred as "hello"
x = "world"; // Still an error
If you wanted x
to be of type string
, you would have to add a widening rule that all string literal types widen to string
. But I'm not sure we would want that.
With the contextual typing approach, you could still run into a similar thing.
But at that point you've opted into an explicit string literal type, whereas having const
declarations implicitly do so is a breaking change, so I'm not seeing the issue in that case.
Yes, I agree. Just wanted to make sure.
I don't think that would be intuitive for users (it is inconsistent between types of declarations), but I can see some merits to that approach.
Neither the contextual type approach nor the widening approach results in different treatment for const declarations versus variable declarations. Unless somebody voices a clear advantage for the widening approach, I favor contextual typing.
One interesting question is whether a string literal type should be nullable (allow null / undefined as a value). Right now, our type system has types for null and undefined, but these types are assignable to all types. Should string literals be an exception because they denote one particular value? The motivation is this:
interface StringWrapper {
kind: "string";
value: string;
}
interface ArrayWrapper {
kind: "array";
value: string[]
}
var v: StringWrapper | ArrayWrapper = { kind: undefined, value: ["hello"] }; // Suppose this assignment is allowed
if (v.kind === "array") {
// Clearly, v has type ArrayWrapper here
}
else {
// Process of elimination suggests that v is of type StringWrapper, but that is not true.
v.value; // string[]
}
There are 3 options:
Thoughts??
Interesting dilemma! I've been thinking about this for an hour or so, and have typed up three different responses to how there was an obvious choice and the rest were evil.
I must say that the most vexing part of this code sample is // Suppose this assignment is allowed
.
My selections in order of decreasing preference:
--strictTypeGuardNarrowing
which lets people opt-in to choice 2 :bike: :house:I don't think choice 1 fits with today's TypeScript.
I think option 1 is correct and preferrable for the kind of type-level reasoning that singleton types are able to bring to the language.
I think of a singleton type as a subset of a broader type where the subset is a single value from the broader set, ie a singleton set. null
and undefined
are always members of the broader set, but a singleton type is a subset and can therefore exclude these two values.
If singleton types implicitly always also include null
and undefined
as allowed values, they are not really singletons at all, they are 'tripletons' or something, with the weaker powers of type-level reasoning that go with that.
eg var abc: 'abc'
is stating explicitly that abc
can only have the value 'abc'
. null
and undefined
are explicitly not members of this type's value set.
Is there some scenario using singleton types where assigning null
and/or undefined
would be useful? Or is this perhaps more an issue with the way the compiler currently works? (Or both?)
@yortus A very straightforward example that I can think of is setting the value of a variable programmatically, e.g.:
function numberRangeName(num: number)
var x: "positive"|"negative"|"zero"; // x is undefined here.
if (num > 0) {
x = "positive";
}
///etc...
return x;
}
Of course you could always have a "NotSure"
in there, but then that's basically the same thing as allowing undefined, just more explicit. I know you could write this simple example slightly differently to eliminate the temporary variable, but more complex examples might require it.
@jbondc, you cannot denote the null type and undefined types explicitly today. I'm not sure there is value in adding those words in the type syntax, rather than some sort of nullability marker.
To @yortus's comment, I was going to say that null and undefined are not really in the spirit of singleton types, and having the types be nullable is just a type system limitation. But I am actually convinced by @nycdotnet's example, where the value is set after the declaration point. If we made these types non-nullable, then you would have to modify the code in that example to take advantage of var scoping:
function numberRangeName(num: number)
if (num > 0) {
var x: "positive"|"negative"|"zero" = "positive";
}
///etc...
return x;
}
But while this is a pattern we support, it's not a pattern we want to encourage.
var x: "positive"|"negative"|"zero"; // x is undefined here.
That's just an uninitialised variable. The compiler can already flag an error for this - eg:
const a; // Error: 'const' declarations must be initialized
Its also trivially easy to avoid (with no need to exploit var
scoping)
function numberRangeName(num: number)
var x: "positive"|"negative"|"zero" = "zero";
if (num > 0) {
x = "positive";
}
///etc...
return x;
}
I guess there's a trade-off for the user of singletons: either they must pay the cost of initialising singleton variables to a valid value, or they must pay the cost of having to have extra type guards and checks everywhere the values are used.
I guess I was wondering if there is a useful example of allowing null
/undefined
in a singleton? ie where it helps express or solve a problem at hand.
Talked with @mhegazy. We think that this issue really doesn't have much to do with string literal types. Instead it has more to do with type guards that involve property accesses. To this end, our inclination is that if you have a type guard based on a property access, we will only narrow the type in the true branch. The false branch could trick us into doing a faulty process of elimination.
@JsonFreeman can you clarify what this means for whether or not the type system would consider null
/undefined
as valid values for singleton types?
Sure, it means that singleton types are just like any other type, in that they implicitly allow null and undefined. This minimizes friction with the rest of the type system.
I do see the merit in making singleton types not allow null or undefined. However, doing that would mean that we need to provide a way to denote a nullable singleton type. We have thought about nullability in general from time to time, but so far have not come to any promising conclusions about how it would be accomplished. For better or worse, our type system today is nullable at its very core.
Got it. Still curious to see an example illustrating why someone might want to constrain a type to a singleton value and also want it to be nullable. Bearing in mind this is all purely about compile time type checking since singleton types obviously have no runtime support.
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 :