Skip to content

Singleton types under the form of string literal types #1003

Closed

Description

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 */
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Labels

Domain: Literal TypesUnit types including string literal types, numeric literal types, Boolean literals, null, undefinedFixedA PR has been merged for this issueSuggestionAn idea for TypeScript

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions