Description
openedon Oct 30, 2014
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
type result = "ok" | "fail" | "abort";
// "string enum"
function compute(n : number) : result {
if(...) {
return "ok";
} else if (...) {
return "fail";
} else {
return "crash"; // Error, not accepted
}
}
function checkSuccess(o : result) : void { ... }
var res : result = compute(42);
checkSuccess(res); // OK
checkSuccess("crash"); // Error
res = "crash"; // Error
var message = res; // OK, message infered as string
var verbose = "Current status : " + res; // OK
// Specifications constrains
interface operationAction {
name : string;
id : number;
status : result;
}
// Usable as and with regular types
var results : result[] = [compute(3), compute(27), "ok"]; // Ok
results = ["crash", "unknown"]; // Error
type error_level = "warning" | "fatal";
interface Foo<T> {
foo : T;
}
interface Toto<U> {
value : Foo<result> | { unknown : string; value : U };
}
var foo : Toto<error_level> = { foo : "ok" }; // OK
var foo : Toto<error_level> = { foo : "crash" }; // Error
var foo : Toto<error_level> = { unknown : "Unknown error", value : "warning" }; // OK
var foo : Toto<error_level> = { unknown : "Unknown error", value : "trace" }; // Error
// Disjoint union
type obj = { kind : "name"; name : string } | { kind : "id"; id : number } | { kind : "internal" ; id : number }
var o : obj = { kind : "name", name : "foo", id : 3 }
var o : obj = { kind : "id", name : "foo", id : 3 }
// Both object are strictly distinguished by their kind
var o : obj = { kind : "unknown", name : "something" }; // Error
type classical_obj : { name : string } | { id : number };
var c_o : classical_obj = { name : "foo", id : 3 }; // A supertype of both is assignable, we lose infos
Typing specifications
- any string literal as type "s" (StringLiteralType) where s is the content of the literal.
- every StringLiteralTypes are subtypes of string, the reverse is not true.
- two StringLiteralTypes are compatible if and only if they represent the same string literal.
type ok = "ok"
var a : ok = "ok";
var b : "ok" = a; // OK
var a : ok = "no"; // Error
var c : string = a; // OK
var a : ok = c; // Error
Pitfalls and remaining work
Backward compatibility
This prototype is backward compatible (accepts programs that were accepted before) but in one case :
function ... {
if (...) {
return "foo";
} else {
return "bar";
}
}
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 :
type obj = { kind : "name"; name : string } | { kind : "id" ; id : number };
var o : obj = ...;
if (o.kind == "name") {
/* o considered as left type */
} else if(o.kind == "id") {
/* o considered as right type */
}