Always use literal types #10676

Merged
merged 36 commits into from Sep 11, 2016

Projects

None yet
@ahejlsberg
Member
ahejlsberg commented Sep 1, 2016 edited

This PR switches our approach to literal types from being a concept we sometimes use to a concept we consistently use. Previously we'd only use literal types in "literal type locations", which was not particularly intuitive. With this PR we always use literal types for literals and then widen those literal types as appropriate when they are inferred as types for mutable locations.

First some terminology:

  • A literal type is a type that only has a single value, e.g. true, 1, "abc", undefined.
  • String and numeric literal types come in two flavors, widening and non-widening.
  • A literal union type is a union of only literal types.
  • The widened literal type of a type T is:
    • string, if T is a widening string literal type,
    • number, if T is a widening numeric literal type,
    • boolean, if T is true or false,
    • E, if T is a union enum member type E.X,
    • a union of the widened literal types of each constituent, if T is union type, or
    • T itself otherwise.

Note that the widened literal type of any non-primitive type is simply the type itself. For example, the widened literal type of { kind: true } is that type itself, even though the type contains a property with a literal type. Literal type widening is effectively a "shallow" operation.

The following changes are implemented by this PR:

  • The concept of "literal type locations" is gone.
  • The type of a literal in an expression is always a literal type (e.g. true, 1, "abc").
  • The type of a string or numeric literal occurring in an expression is a widening literal type.
  • The type of a string or numeric literal occurring in a type is a non-widening literal type.
  • The type inferred for a const variable or readonly property without a type annotation is the type of the initializer as-is.
  • The type inferred for a let variable, var variable, parameter, or non-readonly property with an initializer and no type annotation is the widened literal type of the initializer.
  • The type inferred for a property in an object literal is the widened literal type of the expression unless the property has a contextual type that includes literal types.
  • The type inferred for an element in an array literal is the widened literal type of the expression unless the element has a contextual type that includes literal types.
  • In a function with no return type annotation and multiple return statements, the inferred return type is a union of the return expression types. Previously the return expressions were required to have a best common supertype, but this is no longer the case.
  • In a function with no return type annotation, if the inferred return type is a literal type (but not a literal union type) and the function does not have a contextual type with a return type that includes literal types, the return type is widened to its widened literal type.
  • During type argument inference for a call expression, the type inferred for a type parameter T is widened to its widened literal type in certain situations as explained below.

The intuitive way to think of these rules is that immutable locations always have the most specific type inferred for them, whereas mutable locations have a widened type inferred. Some examples:

const c1 = 1;  // Type 1
const c2 = c1;  // Type 1
const c3 = "abc";  // Type "abc"
const c4 = true;  // Type true
const c5 = cond ? 1 : "abc";  // Type 1 | "abc"

let v1 = 1;  // Type number
let v2 = c2;  // Type number
let v3 = c3;  // Type string
let v4 = c4;  // Type boolean
let v5 = c5;  // Type number | string
const a = cond ? "foo" : "bar";  // Type "foo" | "bar"
let b = cond ? "foo" : "bar";  // Type string
let c: "foo" | "bar" = cond ? "foo" : "bar";  // Type "foo" | "bar"
const a1 = [1, 2, 3];   // Type number[], because elements are mutable
const a2: [1, 2, 3] = [1, 2, 3];  // Type [1, 2, 3]
const o1 = { kind: 0 };  // Type { kind: number }, because properties are mutable
const o2: { kind: 0 } = { kind: 0 };  // Type { kind: 0 }

Literal type widening can be controlled through explicit type annotations. Specifically, when an expression of a literal type is inferred for a const location without a type annotation, that const variable gets a widening literal type inferred. But when a const location has an explicit literal type annotation, the const variable gets a non-widening literal type.

const c1 = "hello";  // Widening type "hello"
let v1 = c1;  // Type string
const c2 = c1;  // Widening type "hello"
let v2 = c2;  // Type string
const c3: "hello" = "hello";  // Type "hello"
let v3 = c3;  // Type "hello"
const c4: "hello" = c1;  // Type "hello"
let v4 = c4;  // Type "hello"

For further details on widening vs. non-widening string and numeric literals, see #11126.

In a function with no return type annotation, if the inferred return type is a literal type (but not a literal union type) and the function does not have a contextual type with a return type that includes literal types, the return type is widened to its widened literal type:

function foo() {
    return "hello";
}

function bar() {
    return cond ? "foo" : "bar";
}

const c1 = foo();  // string
const c2 = bar();  // "foo" | "bar"

During type argument inference for a call expression the type inferred for a type parameter T is widened to its widened literal type if:

  • all inferences for T were made to top-level occurrences of T within the particular parameter type, and
  • T has no constraint or its constraint does not include primitive or literal types, and
  • T was fixed during inference or T does not occur at top-level in the return type.

An occurrence of a type X within a type S is said to be top-level if S is X or if S is a union or intersection type and X occurs at top-level within S.

In cases where literal types are preserved in inferences for a type parameter (i.e. when no widening takes place), if all inferences are literal types or literal union types with the same base primitive type, the resulting inferred type is a union of the inferences. Otherwise, the inferred type is the common supertype of the inferences, and an error occurs if there is no such common supertype.

Some examples of type inference involving literal types:

declare function f1<T>(x: T): T;
declare function f2<T>(x: T, y: T): T;
declare function f3<T, U>(x: T, y: U): T | U;
declare function f4<T>(x: T): T[];
declare function f5<T extends number>(x: T, y: T): T[];
declare function f6<T>(x: T[]): T;
declare function f7<T>(x: T[]): T[];

const a: (1 | 2)[] = [1, 2];

const x1 = f1(1);  // Type 1
const x2 = f2(1, 2);  // Type 1 | 2
const x3 = f2(1, "two");  // Error
const x4 = f3(1, "two");  // Type 1 | "two"
const x5 = f4(1);  // Type number[]
const x6 = f5(1, 2);  // Type (1 | 2)[]
const x7 = f6([1, 2]);  // Type number
const x8 = f6(a);  // Type 1 | 2
const x9 = f7(a);  // Type (1 | 2)[]

Note that this PR fixes the often complained about issue of requiring a type annotation on a const to preserve a literal type for the const (e.g. const foo: "foo" = "foo") because const now consistently preserves the most specific type for the expression. However, the PR does not provide a way to infer literal types for mutable locations. That is an orthogonal issue we can address in a seperate PR if need be.

NOTE: This description has been updated to reflect the changes introduced by #11126.

@ahejlsberg
Member

@mhegazy @RyanCavanaugh @DanielRosenwasser I'm sure you'll want to look at this!

@ahejlsberg ahejlsberg referenced this pull request Sep 2, 2016
Closed

Union Types And Strings #9489

@Igorbek
Igorbek commented Sep 2, 2016

In your example

function foo() {
    return "hello";
}
const c1 = foo();  // string

Why c1 inferred as a string? What would be the type of foo then, () => string ? Why not literal type here? Would it be different if it'd be used as const c2: "hello" = foo(); ?

@ahejlsberg
Member
ahejlsberg commented Sep 2, 2016 edited

@Igorbek The issue is that you practically never want the literal type. After all, why write a function that promises to always return the same value? Also, if we infer a literal type this common pattern is broken:

class Base {
    getFoo() {
        return 0;  // Default result is 0
    }
}

class Derived extends Base {
    getFoo() {
        // Compute and return a number
    }
}

If we inferred the type 0 for the return type in Base.getFoo, it would be an error to override it with an implementation that actually computes a number. You can of course add a type annotation if you really want to return a literal type, i.e. getFoo(): 0 { return 0; }.

Of course, we could consider having different rules for functions vs. methods and only do the widening in methods, but I think that would be confusing.

@RyanCavanaugh
Member
RyanCavanaugh commented Sep 2, 2016 edited

Should the expression initializing a readonly class property also retain a literal type?

@joewood
joewood commented Sep 2, 2016

This is great. I'm wondering if this will address the false positive scenario described by @AlexGalays in #6613 (comment).

This is related to using f-bound polymorphism as a solution to "partial types" (e.g. for Object.assign and React setState). I'm guessing not as parameter binding is treated as a mutable bind and widened.

@ahejlsberg
Member

Should the expression initializing a readonly class property should also retain a literal type?

Yes, I think that would make sense.

I'm wondering if this will address the false positive scenario described by @AlexGalays in #6613 (comment).

No, the issue in #6613 is unrelated to this change.

@Igorbek
Igorbek commented Sep 2, 2016

@ahejlsberg thank you, good point. However the same argument can be applied to conditional return as well:

class Base {
    startWithOne = true;
    getFoo() {
        return startWithOne ? 1 : 0; // now it will be typed as 1 | 0 ?
    }
}

class Derived extends Base {
    getFoo() {
        // Compute and return a number
    }
}

Technically speaking, class methods cannot be considered immutable locations. And even plain functions cannot be:

function foo() { return cond ? 1 : 2; }
foo = function() { return 3; } // error?
const foo2 = function() { return 1; } // now it's really immutable
@Igorbek
Igorbek commented Sep 2, 2016 edited

I would say that function declaration

function foo() { return 1; }

is equivalent (with hoisting in mind) to

let foo = function () { return 1; };

where function () { return 1; } is an expression (immutable location) of type () => 1 and foo is a mutable location so that it must be of base type () => number.

function () { return 1; }; // () => 1
let foo = function () { return 1; }; // () => number
function foo() { return 1; }; // () => number
const boo = function () { return 1; }; // () => 1
@alitaheri

This is really great ❤️ ❤️

Just a small detail:

The issue is that you practically never want the literal type. After all, why write a function that promises to always return the same value?

How about a function from which we want to return a finite set of literal types:

function foo(n: 1 | 2 | 3) {
  switch (n) {
    case 1: return 'one';
    case 2: return 'two';
    case 3: return 'three';
    default: return 'none';
  }
}

Although it's a better practice to explicitly type the return value, but this might a good example of inferring the return type as a literal.

To avoid it, the developer can simply annotate the return type as string. But to make it return literal union another interface will need to be declared and maintained with the function body.

@ahejlsberg
Member

@alitaheri The function in your example has a literal union type inferred as its return type:

function foo(n: 1 | 2 | 3): "one" | "two" | "three" | "none";

Widening occurs only if the inferred return type is a singleton literal type.

@ahejlsberg
Member

@Igorbek In the case of return cond ? 0 : 1 the inferred return type would be 0 | 1. I think in this case it is not at all clear we should widen to the base primitive type. After all, with a return type of 0 | 1 there is actually meaningful information being conveyed out of the function. It's like the difference between a function that always returns false vs. a function that returns true | false.

@ahejlsberg
Member

I have updated the introduction to include the changes in the latest commits, plus I have added some more details and examples.

@trevorsg
Member
trevorsg commented Sep 4, 2016

This is great. If I understand everything correctly, the following scenario should now work.

namespace MyDomUtils {
  export function elem<T extends string>(type: T, ...classList: string[]) {
    const element = document.createElement(type);
    element.classList.add(...classList);
    return element;
  }
}

const ul = MyDomUtils.elem("ul"); // HTMLUListElement (before this was HTMLElement)
const li = MyDomUtils("li"); // HTMLLIElement (before this was HTMLElement)
@ahejlsberg
Member

@trevorsg No, that wouldn't be affected by this PR. You need to add a list of overloaded signatures to your elem function to get more specific return types.

@DanielRosenwasser DanielRosenwasser commented on the diff Sep 4, 2016
src/compiler/checker.ts
case SyntaxKind.TrueKeyword:
case SyntaxKind.FalseKeyword:
- return isLiteralContextForType(node, booleanType) ? node.kind === SyntaxKind.TrueKeyword ? trueType : falseType : booleanType;
+ return node.kind === SyntaxKind.TrueKeyword ? trueType : falseType;
@DanielRosenwasser
DanielRosenwasser Sep 4, 2016 Member

Just return trueType in the TrueKeyword branch.

@thorn0
thorn0 commented Sep 4, 2016 edited

What about this case? Will it start working? (playground)

interface InputMaskOptions {
    // this API is a real-world example from the popular jquery.inputmask plugin
    digits: number | '*';
}

declare function defaultTo<T, U>(value: T, defaultValue: U): T | U;
declare var decimalDigits: number;

let options: InputMaskOptions = {
    digits: defaultTo(decimalDigits, '*')
    // this works:
    // digits: defaultTo(decimalDigits, '*' as '*')
};

Update. Okay. I understood that it won't work and found the explanation in #10685,

@thorn0
thorn0 commented Sep 4, 2016 edited

But still... What exactly can break if we always infer type arguments to be literal types? From the explanation in #10685, I derived this example.

declare function f<T>(value: T): T;
var a1 = f('one');
var a2 = f('two');
a1 = a2; // ERROR

However, type inference for variables and type inference for type arguments are separate processes, aren't they? Can't we infer the type of f('one') to be 'one' and at the same time the type of var a1 to be string?

@ahejlsberg
Member

@thorn0 The reason we need to widen is that a type parameter may be used as the type of a mutable location inside the function:

function makeArray<T>(x: T): T[] {
    return [x];
}

var a = makeArray("one");  // Type of a would be "one"[]
a.push("two");  // Would be an error
@thorn0
thorn0 commented Sep 4, 2016

But that's what I'm asking/proposing. Let makeArray("one") be 'one'[]. Why not? In this case it'll be possible to use it where either string[] or 'one'[] is needed. However, the type of var a should be inferred to be string[], not 'one'[]. It's similar to what you're doing in this PR when literal expressions are used to init a variable:

let v1 = 1;  // Type number
@thorn0
thorn0 commented Sep 4, 2016

Okay, looks like converting 'one'[] to string[] isn't a good idea if we try to solve it in general case. But what about special-casing functions that return just T (not T[], { a: T }, etc)? Is the widening really needed for them?

@ahejlsberg
Member

@thorn0 You have a point about not always widening during type inference. I'm thinking we need to track where inferences came from and then only widen when all inferences came from "unwidened" locations. For example:

function makeArray<T>(x: T): T[] {
    return [x];
}

function append<T>(a: T[], x: T): T[] {
    let result = a.slice();
    a.push(x);
    return result;
}

type Bit = 0 | 1;

let a = makeArray<Bit>(0);  // Bit[]
let b = append(a, 1);  // Should return Bit[]

Here, in the call to makeArray we only have inferences from "unwidened" locations, so we need to widen. But in the call to append the inference we make from the array itself is from a location that was already widened, so we should keep that type.

Now, to your point, in cases where a type parameter is only used in "unwidened" locations in the return type (effectively, when the return type is just T) there is never a need to widen. So, the simple f<T>(x: T): T should always infer the exact type of the argument.

@thorn0
thorn0 commented Sep 6, 2016

in cases where a type parameter is only used in "unwidened" locations in the return type (effectively, when the return type is just T)

What about my first example?

declare function defaultTo<T, U>(value: T, defaultValue: U): T | U;

It seems like widening isn't needed here.

ahejlsberg added some commits Sep 7, 2016
@ahejlsberg ahejlsberg Less widening of literal types in type inference ff3b627
@ahejlsberg ahejlsberg Accept new baselines da2efa0
@ahejlsberg ahejlsberg Remove failing fourslash tests (may need to be restored and fixed) 2f9c9c9
@ahejlsberg ahejlsberg Merge branch 'master' into literalTypesAlways
# Conflicts:
#	tests/baselines/reference/awaitBinaryExpression1_es6.types
#	tests/baselines/reference/awaitBinaryExpression2_es6.types
#	tests/baselines/reference/awaitBinaryExpression3_es6.types
#	tests/baselines/reference/awaitBinaryExpression4_es6.types
#	tests/baselines/reference/awaitBinaryExpression5_es6.types
#	tests/baselines/reference/awaitCallExpression1_es6.types
#	tests/baselines/reference/awaitCallExpression2_es6.types
#	tests/baselines/reference/awaitCallExpression3_es6.types
#	tests/baselines/reference/awaitCallExpression4_es6.types
#	tests/baselines/reference/awaitCallExpression5_es6.types
#	tests/baselines/reference/awaitCallExpression6_es6.types
#	tests/baselines/reference/awaitCallExpression7_es6.types
#	tests/baselines/reference/awaitCallExpression8_es6.types
#	tests/baselines/reference/classExpressionWithStaticProperties1.types
#	tests/baselines/reference/classExpressionWithStaticProperties2.types
b33e499
@ahejlsberg ahejlsberg Fix merge issue ad1c9b9
@ahejlsberg ahejlsberg Fixing the fix b9fa0af
@ahejlsberg ahejlsberg Accept new baselines 737867e
@ahejlsberg ahejlsberg Cleaning up InferenceContext 31a94fc
@ahejlsberg ahejlsberg Simplify tracking of top-level type inferences 6f06d06
@ahejlsberg
Member

Latest commits revise the rules for widening literal types during type inference based on feedback from @thorn0. I have updated the OP with description and examples.

@JsonFreeman
Contributor
JsonFreeman commented Sep 7, 2016 edited

I think this PR is a great idea. It's interesting that widening was originally intended to replace null and undefined with any, and now it is used for a much more general purpose. It seems like you could think of null and undefined as literal types whose base primitive type is any. Would that analogy work?

Also, with regard to the issue @thorn0 raises, we have always had this question about widening with type argument inference. We had some old discussions of it at #1436 and #531.

@thorn0
thorn0 commented Sep 7, 2016 edited

@ahejlsberg I'm reading and rereading your updated description and I can't understand it:

... the type inferred for a type parameter T is widened to its base primitive type if:

  • all inferences for T were made to top-level occurrences of T within the particular parameter type, and...

I may be wrong, but isn't "not" missing somewhere here? It sounds to me more like a condition for the widening not to happen.

Also what does "T was fixed during inference" mean? Where can I read about this? Found it.

@JsonFreeman
Contributor

I am likewise confused about the intent for the various type argument inference policies.

@thorn0
thorn0 commented Sep 7, 2016

The intent is simple: not to widen the type when there is no real need to do so. It's difficult for me to understand the wording of these rules, but when I look at the code examples, the results of the inference there look really logical and expected. There will be no need to think about these rules when writing code.

@JsonFreeman
Contributor

@thorn0 In terms of the code examples, I understand all of them except for x7. It looks like those type parameters are not top level within the parameter type, so I am not sure why they get widened per the first requirement listed. This may be related to your comment about the requirement being off-by-negation.

@ahejlsberg
Member

@thorn0 No, there isn't a "not" missing, but notice that the last bullet of the three rules will cause widening not to happen when the type parameter occurs at top level in the return type. Intuitively, we only widen if you infer to top-level type parameters (and therefore from possibly unwidened locations) and subsequently ignore the inferred type in the result or wrap an object type around it.

@JsonFreeman The x7 example produces type number because the array literal [1, 2] has type number[] because there is no contextual type that guides us to use literal types. The number[] is produced before we do type inference and is really independent of the type argument inference.

@JsonFreeman
Contributor

@ahejlsberg Oh, I had missed the part where you said "unless the property has a contextual type that includes literal types." That makes sense.

When you say that a top level type parameter corresponds to a possibly unwidened location, why is that the case? Or rather, why is it that a non-top level type parameter is expected to be already widened? Is it mostly because object literals and array literals are widened in the absence of contextual types?

@ahejlsberg
Member

When you say that a top level type parameter corresponds to a possibly unwidened location, why is that the case? Or rather, why is it that a non-top level type parameter is expected to be already widened? Is it mostly because object literals and array literals are widened in the absence of contextual types?

Yes, it is because properties in object literals and elements in array literals have already been subject to widening (or no widening if they had a contextual type). We don't want to do the widening again.

BTW, note that the literal type widening is more eager (i.e. happens sooner) than the widening we do for null and undefined in classic type checking mode. We have to defer the null and undefined widening such that an any produced by the widening doesn't "poison the pot" for type inference. For example

var x = [{ x: undefined, y: "foo"}, { x: 5, y: "bar" }];

We want to infer { x: number, y: string }[] for this example, but we'd infer any for x if we eagerly widened. So, in that sense you can't just think of null and undefined as literal types that have any as their base primitive type.

@danielearwicker

Playing with this version:

declare function f<T>(x: T): T;
const c = f("FOO");
let v: typeof c = "FEE";

As expected, Type '"FEE"' is not assignable to type '"FOO"'.

But:

declare function f<T>(x: T): { readonly prop: T };
const c = f("FOO");
let v: typeof c.prop = "FEE";

That produces no error. Is that expected? Seems "morally similar" given prop is readonly but I guess there's a subtlety I'm missing.

@ahejlsberg
Member
ahejlsberg commented Sep 8, 2016 edited

@danielearwicker Yes, I've been thinking about that issue too. Unfortunately I don't think there is any conclusive analysis we can do that will always yield a correct answer on whether a type parameter is used in a mutable fashion within a generic type. And, even if there was, it is very common to use the same type for both read/write and read-only purposes. We already did a lot of this analysis when we introduced the readonly modifier (see #12 and #6532).

In the end I think the best we can do is to provide an explicit way of indicating whether a type parameter should have literal type widening applied to its inferences. You can already do it by constraining a type parameter to a primitive type, but of course that also constrains type arguments to that primitive type. I'm thinking we should just say that any constraint on a type parameter disables literal type widening for that type parameter. Then you be able to say:

declare function f<T extends {}>(x: T): { readonly prop: T };
const c = f("FOO");  // Type { readonly prop: "FOO" }
let v: typeof c.prop = "FEE";  // Error

It's a bit subtle, but at least it provides some way of overriding the default heuristics.

@JsonFreeman
Contributor

@ahejlsberg thanks for explaining about null and undefined versus literal widening.

With regard to the readonly issue, I always felt rather uneasy about using extends {} as an idiom for altering the behavior of type argument inference, particularly when it does not have to do with constraining the type parameter to the given constraint type. It feels like a very roundabout way to opt in to inferring a literal type. Plus it is a breaking change for users who used a non-primitive constraint, and previously did not have a literal type inferred. I do like the rule that if the constraint includes literals, that hints to the compiler that a literal type is allowed. I would be inclined to leave the issue as it is for now, and revisit if it becomes an issue, despite the apparent inconsistency. That's just my hunch.

@ahejlsberg
Member

@JsonFreeman No, it shouldn't be a breaking change. If you have a non-primitive constraint (e.g. an interface type), then a literal type wouldn't meet the constraint anyway and you'd get an error. The one other reason I think it makes sense to have any constraint disable literal type widening is that the constraint might be another type parameter, but we can't know whether that will be a primitive type until we finish type inference. However, we do know that a constraint exists.

@JsonFreeman
Contributor

That is certainly true, if the constraint is another type parameter, it is hard to know whether it will be a primitive type.

If you have a non-primitive constraint (e.g. an interface type), then a literal type wouldn't meet the constraint anyway and you'd get an error.

declare function foo<T extends {}>(x: T): T;
foo("arg");

Is this an error today? If it is, are you suggesting making an exception for {} as a constraint? If it's not, then wouldn't the inferred type change from string to "arg"?

@danielearwicker

@ahejlsberg Interesting, thanks. The use cases I've been thinking about are for specific base types (a string literal) so I'm already a lot happier with typescript@next (compared to typescript@beta) in this area.

@Igorbek
Igorbek commented Sep 9, 2016 edited

And, even if there was, it is very common to use the same type for both read/write and read-only purposes.

@ahejlsberg Would it be useful for that purposes if the generic types are annotated with use-site variance? See my proposal #10717. Covariance usually means read-only usage of the type.
So it wouldn't give any additional knowledge in case of function f<T>(v: T): T; since there's no generic types but would in case of function findSmth<T>(arr: out T[]): out T[].

@ahejlsberg
Member

@danielearwicker @JsonFreeman Based on your feedback I'm going to hold off on changing inference to infer literal types in the presence of any constraint. We'll keep the original design for now.

@Igorbek It's possible variance annotations would help, but the question of whether we want variance annotations and the associated complexity is definitely orthogonal to this PR.

@ahejlsberg ahejlsberg merged commit 3cca17e into master Sep 11, 2016

1 check passed

continuous-integration/travis-ci/pr The Travis CI build passed
Details
@ahejlsberg ahejlsberg deleted the literalTypesAlways branch Sep 11, 2016
@yahiko00

Litteral types are becoming strong!
Even though I would need to read carefully a second time this PR and all the comments, this sounds really nice. Thinking aloud, non widening types for immutables could be a good incentive to write safer code...

@aluanhaddad
Contributor

This is great. Literal types are very expressive and this makes them even more concise and intuitive. I think the distinctions of where and where not literal types are inferred are very well thought out. Specifically I think the decision to widen a singleton literal return type but not a union literal return type is just brilliant.

@ethanresnick
Contributor

This is awesome! Is it going to be part of 2.0 or 2.1?

@RyanCavanaugh
Member

This will be in 2.1.

@aleksey-bykov
aleksey-bykov commented Sep 15, 2016 edited

sorry for crashing the party, we got 200+ something errors after trying this out, is this how it is supposed to be now?

#10938

@ahejlsberg
Member

@aleksey-bykov Can you share some of those error scenarios?

@aleksey-bykov

yes please #10938

@ahejlsberg
Member

I have updated the OP to reflect the changes made in #11126.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment