Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

String literal types #5185

Merged
merged 44 commits into from
Nov 10, 2015
Merged

String literal types #5185

merged 44 commits into from
Nov 10, 2015

Conversation

DanielRosenwasser
Copy link
Member

@DanielRosenwasser DanielRosenwasser commented Oct 8, 2015

String Literal Types

This pull request adds reasonable support for string literal types as proposed in #1003.

A string literal type can be described by a string literal type node, which is otherwise the same lexically as the StringLiteral production in ECMAScript.

A string literal type is a type whose expected value is a string with textual contents equal to that of the string literal type. In other words, there is no difference between "hello'world" and 'hello\'world'.

let tradition: "hello world" = 'hello world';

let quotes: "hello'world" = 'hello\'world';

String literal types can be used in conjunction with union types to describe a finite set of possible string values:

type CardinalDirection = "North"
                       | "East"
                       | "South"
                       | "West";

function move(distance: number, direction: CardinalDirection) {
    // ...
}

A string literal type can be considered a subtype of the string type. This means that a string literal type is assignable to a plain string, but not vice-versa.

Because of this distinction, string literals no longer always have the type string. Instead, a string literal's type is informed by its contextual type. The rules are the following:

  • If a string literal is contextually typed a string literal type, then it returns a string literal type with the textual contents of the string literal.
  • If a string literal is contextually typed by a union type containing a string literal type, then it returns a string literal type with the textual contents of the string literal.
  • Otherwise, the literal is typed as string.
let a: "foo" | number = "foo"; // valid
let b: "bar" | "baz" = "foo";  // invalid

Note that intersections of string literals do not affect the type; "baz" will have the string type below, and the following assignment will fail:

let c: "baz" & { prop: number } = "baz"; // invalid

String literal types have the same apparent properties as string (i.e. the String global type), and are mostly compatible with operators like + in the same way that a string is:

var HELLO: "HELLO" = "HELLO";
var WORLD: "WORLD" = "WORLD";
var hello = HELLO.toLowerCase();  // type: string
var HELLOWORLD = HELLO + WORLD; // type: string

Specialized signatures are also compatible with string literal types:

interface HTMLElement {
    addEventListener(type: "click", listener: (ev: MouseEvent) => any, useCapture?: boolean): void;
    addEventListener(type: "focus", listener: (ev: FocusEvent) => any, useCapture?: boolean): void;
    addEventListener(type: "drag", listener: (ev: DragEvent) => any, useCapture?: boolean): void;
}

namespace EventType {
    export const Click: "click" = "click";
    export const Focus: "focus" = "focus";
    export const Drag: "drag" = "drag";
}

let element: HTMLElement = /*...*/;
element.addEventListener("click", ev => {
    // 'ev' is correctly inferred to be a 'MouseEvent'
    console.log(`x: '${ev.x}', y: '${ev.y}'`);
}
// also works
element.addEventListener(EventType.Click, ev => {
    // 'ev' is correctly inferred to be a 'MouseEvent'
    console.log(`x: '${ev.x}', y: '${ev.y}'`);
}

Open Questions

  • We could introduce a new form of type guards so that equality checks with string literals narrow strings to their respective string literals. Is this worthwhile?
  • Right now, signatures are only specialized if they have a parameter whose type is a string literal type. Should we expand this so that signatures are specialized if any of their parameters' types contain a string literal type within them?
  • Given that we have the textual content of a string literal type, we could reasonably perform property lookups in an object. I think this is worthwhile to consider. This would be even more useful if we performed narrowing.
  • Right now a string literal's type is only informed by a contextual type. Should const declarations imply that a string literal type should be used?
  • We currently allow number to be assignable to enum; however, this is due to how frequently people use flags in enums. Should we allow string to be assignable to a string literal type? I am personally against this idea.
  • Should the same typing rules for string literals apply to no-substitution template strings?
…ing literal types.

In most cases, expressions are interested in the apparent type of the
contextual type. For instance:

    var x = { hasOwnProperty(prop) { /* ... */ };

In the above, 'prop' should be contextually typed as 'string' from the
signature of 'hasOwnProperty' in the global 'Object' type.

However, in the case of string literal types, we don't want to get the
apparent type after fetching the contextual type. This is because the
apparent type of the '"onload"' string literal type is the global 'String'
type. This has adverse effects in simple assignments like the following:

    let x: "onload" = "onload";

In this example, the right-hand side of the assignment will grab the type
of 'x'. After figuring out the type is "onload", we then get the apparent
type which is 'String'. This is problematic because when we then check the
assignment itself, 'String's are not assignable to '"onload"'s.

So in this case, we grab the contextual type *without* getting its
apparent type.
@danquirk
Copy link
Member

danquirk commented Oct 8, 2015

Very cool. Definitely seems highly desirable to find a way to do narrowing/type-guard sort of behavior here:

type CardinalDirection = "North" | "East" | "South" | "West";

function move(distance: number, direction: CardinalDirection) {
    if(direction === "North") {
        // ok
    }
    else if(direction === "SouthWest") {
        // oops, no error at the moment
    }
}
@@ -10403,6 +10433,7 @@ namespace ts {
case SyntaxKind.TemplateExpression:
return checkTemplateExpression(<TemplateExpression>node);
case SyntaxKind.StringLiteral:
return checkStringLiteralExpression(<LiteralExpression>node);
case SyntaxKind.NoSubstitutionTemplateLiteral:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not that I want to sound negative but for every string literal, this gets called.

It's a huge slowdown.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What kind of numbers are you seeing (and on what codebase)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you consider that every string has on average 5 bytes, you're making 1-5 integer checks for every comparison:

I think the VM already optimize for this?
http://jsperf.com/string-vs-int-comparison-1

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's useless to speculate about what's fast and what's not. Measure it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Meh. Measure it in 1 version of Chrome, it changes in the next. You measure it, I've said what I had to say.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jbondc 👍 for the benchmark test.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the original issue was so terrible. You only ran into this case when you were contextually typed by a type with a string literal constituent to begin with. In practice, this is not frequently encountered.

However, @jbondc, I think you'll like the the current implementation a lot better. We now only create a string literal type if the constituent types have a string literal type, not just if their content is equal.

Furthermore, the types are cached in a map of strings to string literal types. Whenever testing the assignability of two string literal types, reference equality kicks in (which is fast).

Additionally, error messages are slightly better because you have a specific literal type to report (i.e. Type '"Foo"' is not assignable to '"Bar"'. instead of Type 'string' is not assignable to '"Bar"'.).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jbondc at worst that's probably much faster than trying to resolve the 100 different overloads. Though, I'm going to try to avoid speculating. I wasn't seeing a real perf hit in our benchmarks even with the initial change.

@franza
Copy link

franza commented Mar 2, 2016

I assume that it was mentioned somewhere in thread and I didn't find it. Can I use string literal constraint for names of properties? I don't see that it works right now (playground)

type Columns = 'name' | 'email' | 'age';

interface TableRow {
    [key: Columns]: string;
}
@kitsonk
Copy link
Contributor

kitsonk commented Mar 2, 2016

Currently indexers can only be string | number. Other types have been brought up before. See #2049, #1863 and mainly #6080.

@DanielRosenwasser DanielRosenwasser added the Domain: Literal Types Unit types including string literal types, numeric literal types, Boolean literals, null, undefined label Mar 2, 2016
@whatisaphone
Copy link

I'm really hoping to one day be able to quickly write ADTs in a succinct manner. something like this:

type ListMutation<T> =
    { type: 'insert-one', index: number, value: T } |
    { type: 'remove-range', index: number, count: number };

function apply<T>(mutation: ListMutation<T>) {
    if (mutation.type === 'insert-one')
        console.log(`inserting ${mutation.value}`);
    else if (mutation.type === 'remove-range')
        console.log(`removing ${mutation.count} items`);
}

Today, the compiler is unable to make the jump from mutation.type === 'insert-one' to mutation being guaranteed to have a value property. Is support for this kind of inference on the roadmap?

@DanielRosenwasser
Copy link
Member Author

@johnsoft we're looking into it, but right now we need to get a sense of how to make that play well with certain other language features.

@whatisaphone
Copy link

Good to hear! Is there another issue I could follow that's more closely related to that use-case? I couldn't find anything after a quick search.

@DanielRosenwasser
Copy link
Member Author

@johnsoft #6062, #6028, and optionally #7642

@DanielRosenwasser
Copy link
Member Author

Also, #186. Duh. Can't believe I forgot about that one.

In any case, I am waiting on some work where we'll be modifying the way we do type guards.

@whatisaphone
Copy link

Great, thanks for the links! I'm really impressed with how the language is evolving.

aluanhaddad referenced this pull request in danturu/systemjs Apr 24, 2016
@aindlq aindlq mentioned this pull request Aug 3, 2016
@vjau
Copy link

vjau commented Aug 28, 2016

When i stumbled on this feature i found it a very welcome addition to the language, but after having played a little bit with it, and getting it not to work for everything it throwed at it, i wonder what this is really good for.
Simple exemple that should intuitively work but doesn't.

const testFunc = (stuff:"bar")=>{};
testFunc("bar"); // OK

const stuff = "bar";
testFunc(stuff);// not OK

After two hours pulling my hair around this (my initial code was more complicated), i finally found out you have to write

const stuff = "bar" as "bar";

Being no TS expert but just a normal programmer trying to gain time with the security of a type checking compiler, i find this overly counter-intuitive and complicated.

Sorry if this not the place to discuss this, in the case would you be kind to point me to the correct place ?

Thank you.

@felixfbecker
Copy link
Contributor

@vjau this is expected behaviour. You are relying on type inference. And the infered type of stuff is string. But testFunc only accepts "bar" as a type, not string. When you cast the string to "bar", it works. Most often you will define a type alias with a union of string literal types, and then you can cast to that type alias.

@joewood
Copy link

joewood commented Aug 28, 2016

This is getting off topic - but yeah, I would expect the type of a const x = "bar" to be "bar". I can't see an open issue covering this through, so may want to open something new.

@kitsonk
Copy link
Contributor

kitsonk commented Aug 28, 2016

@joewood it is discussed under #10195 and somewhat under #9489 which was closed because it would be a significant breaking change.

@vjau there is a better work around (also discussed in #9489) which is to be explicit about the type:

const stuff: 'bar' = 'bar';

There are several types that are "TypeScript only" that don't get inferred, right now, literal types are one of those, tuples are another good example const = [ 'foo', 1 ] will always get inferred as string|number[] instead of [ string, number ].

@joewood
Copy link

joewood commented Aug 29, 2016

@kitsonk The issue in #9489 starts off by talking about the type literal of a const but switches to the issue of implicitly typing React style structures. As @ahejlsberg suggests in that issue, the type of a const could be narrowed to the literal type. I haven't seen a good example of where that would break existing code, unless I'm missing something.

@kitsonk
Copy link
Contributor

kitsonk commented Aug 29, 2016

I haven't seen a good example of where that would break existing code, unless I'm missing something.

I didn't raise or respond to the issue... I am just pointing out that it addresses the bit off topic where you felt it wasn't being discussed elsewhere and might be a better place to continue the discussion.

@joewood
Copy link

joewood commented Aug 29, 2016

Thanks @kitsonk, I posted on that issue you referenced - #9489 (comment)

@DanielRosenwasser
Copy link
Member Author

See my response on the breaking change at #9489 (comment). Just to give some background, I'll write a little bit up here.

After this PR went in, we heavily discussed an alternate approach of using widening to try to stop incorrect comparisons (e.g. "foo" === "bar" is nonsensical, why would we allow it?). Basically the other idea was that all strings start out with a string literal type and then are widened to an appropriate string type at a location where a binding is mutable. This sounds super intuitive.

The discussion started here: #6167
And the PR went out here: #6554

The problem is that too much of the time, the type would vanish too quickly or stick around too long. The behavior was looking generally inappropriate and every time we thought we had the perfect idea, it wasn't good enough for some other case. It became a game of whack-a-use-case.

This original PR was nice because:

  • It didn't break anything.
  • Tuple types already used the same concept (i.e. using the contextual type to "enhance" an expression's type).

The comparability problem was solved with #5517 and #6196 anyway, so the only real issue is that people want it to be easier to infer literal types, which I absolutely get.

@joewood
Copy link

joewood commented Aug 29, 2016

Thanks for the detailed response @DanielRosenwasser.

In my mind a string literal type of one value is an immutable type, and it's paradoxical to assign that type to a mutable variable (at least, semantically). Implicit type inference for literals should therefore always widen when typing a mutable variable or parameter, and likewise an immutable variable should be narrowed as much as possible.

It's a shame the #6554 couldn't be made to work, because it kind of seems counter-intuitive right now that a constant expression's type is different to its implicit type used for immutable binding.

@felixfbecker
Copy link
Contributor

@joewood it is not paradox, as long as they have the same type.

@joewood
Copy link

joewood commented Aug 29, 2016

@felixfbecker right, but I mean it's essentially at that point semantically immutable. You can change its value to another string of the same value.

@coveralls
Copy link

Coverage Status

Changes Unknown when pulling ea4e21d on stringLiteralTypes into ** on master**.

@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Domain: Literal Types Unit types including string literal types, numeric literal types, Boolean literals, null, undefined