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

To/from assignability is too strict of a check for comparison and other operations #5300

Closed
DanielRosenwasser opened this issue Oct 17, 2015 · 8 comments
Labels
Fixed A PR has been merged for this issue Suggestion An idea for TypeScript

Comments

@DanielRosenwasser
Copy link
Member

This can be seen as the dual of #5156 and an extension of #1447. While the former seeked to limit what could be compared, this issue seeks to be more permissive.

Currently, the following causes an error:

interface I1 {
    p1: number
}

interface I2 extends I1 {
    p2: number;
}

var x = { p1: 10, p2: 20 };
var y: number | I2 = x;
var z: I1 = x;

// Error: Operator '===' cannot be applied to types 'number | I2' and 'I1'.
if (y === z) {
}

So while y and z are both I2s at runtime, we restrict the two from comparison.

This is especially troublesome with my work on string literal types. In trying to use string literal types on our own codebase, I encountered something like the following:

interface Option {
    type: Map<number> | "string" | "boolean" | "number";
}
declare var opt: Option;
// ...

// Error: Operator '!==' cannot be applied to types 'Map<number> | "string" | "boolean" | "number"' and 'string'.
if (opt.type !== "boolean") {
    //...
}
switch (opt.type) {
    // Error: Type 'string' is not assignable to type 'Map<number> | "string" | "boolean" | "number"'.
    case "string":
    // ...
}

Of note: because I use the contextual type to inform string literal types, "string" in the case clause remains having the string type.

@DanielRosenwasser DanielRosenwasser added the Suggestion An idea for TypeScript label Oct 17, 2015
@mhegazy
Copy link
Contributor

mhegazy commented Oct 17, 2015

so what is the proposal? for logical operators to allow operands with union types if at least one of their constituents is assignable to the other operand's type?

@DanielRosenwasser
Copy link
Member Author

@mhegazy yup, that's more or less what I had in mind.

@mhegazy mhegazy added the In Discussion Not yet reached consensus label Oct 21, 2015
@DanielRosenwasser DanielRosenwasser changed the title To/from assignability is too strict for union types. To/from assignability is too strict of a check for comparison and other operations Oct 21, 2015
@JsonFreeman
Copy link
Contributor

Is it possible that contextual typing is not a sufficient trigger for string literal types? In other words, it seems like the string "boolean" should intuitively be a string literal type and then it would just be assignable to the union type. This is an argument in favor of the widening approach for deciding whether to type a string literal as a string literal versus as string.

@DanielRosenwasser
Copy link
Member Author

It's still a tough call. As I mentioned in the pull request, widening the way we do now with null/undefined is not appropriate because it relies on the fact that any is still assignable to anything else, and that null/undefined are not types that you can describe. You don't have that behavior with string literal types.

On the other hand, contextual typing is very close to what you want. Maybe what we want is something that's a step above contextual typing - call it contextually relevant typing. The idea would be "right now, I am talking about this domain of types", as opposed to contextual typing which originally was just meant to say "this must be the type". If a string literal type is contextually relevant for a string literal, then its type is a string literal type.

You could say a switch statement makes the type of its expression contextually relevant to its case clauses, or that somehow each side of a comparison operator's type is contextually relevant for the other side (though I don't know how you prioritize one side over the other).

Honestly, this is me extremely exhausted and brainstorming at 1 in the morning. 😄

Anyhow, I think the shortcomings described here are part of a larger issue than string literal types though.

@JsonFreeman
Copy link
Contributor

I think contextual typing is pretty much what you are calling "contextually relevant typing". It does not say, "this must be the type". That is assignability saying that.

I don't think the null / undefined properties are an issue here. If I understand correctly, you are saying that the following will fail:

var s: "hello";
var s1 = "hello";
s = s1; // not assignable

But that failure happens with contextual typing as well.

I think the biggest issue with the widening approach is that types are widened after type argument inference, which is an issue we have discussed before. It makes it so that any string literals that get inferred are automatically widened to string, which is a little unfortunate.

@JsonFreeman
Copy link
Contributor

We have seen this with tuples, and to some extent with optional properties. For every kind of expression in the language that can have multiple types in different situations (array literals, string literals, null and undefined), the question always comes up about how to distinguish the two possible types. And the candidate answers are always contextual typing and widening. And there does not seem to be a pattern as to which one is picked.

Ideally, the language would have a duality, where contextual typing and widening had mutually exclusive syntactic contexts. So the following two properties are desirable:

  • An expression context that produces widening can never produce contextual typing.
  • An expression context that produces contextual typing can never produce widening.

If these were true, then the decision of contextual typing versus widening would only matter in contexts where there is no contextual typing and no widening (like comparison operators). And you could always make the decision based on what you want in these cases, rather than having to worry about breaking type argument inference or function return types.

Another thought: The widening approach is weird because of cases like this:

var s: "hello";
var s1 = s;

What is the type of s1 with the widening approach? It is string I guess. Widening is an operation defined on types, though in spirit we often want to apply it to expressions (except in type argument inference). To get the type "hello" for s1 here with the widening approach, it would really require separating the notion of widening types from widening expressions. And that would be an annoying complexity.

So I think it would be a very nice long term goal to achieve the two properties I mentioned above. In the mean time, I think your heuristic about the union type constituents will be good in order to capture the notion of "related enough for comparison".

@RyanCavanaugh
Copy link
Member

@DanielRosenwasser is this fixed by the comparability relation?

@DanielRosenwasser
Copy link
Member Author

@RyanCavanaugh yes, it is fixed by #5517 and #12202

@DanielRosenwasser DanielRosenwasser added Fixed A PR has been merged for this issue and removed In Discussion Not yet reached consensus labels Apr 30, 2017
@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
Fixed A PR has been merged for this issue Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

4 participants