From f8bfc6f5d6f6af8acecee264ee2b49016e0c29b0 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Mon, 6 Jan 2020 12:53:23 -0800 Subject: [PATCH] Contextually typed binding element initializers (#35855) * Binding element initializers contextually typed by parent initializers * Accept new baselines * Literal type widening should be last step in inference * Accept new baselines * Add tests --- src/compiler/checker.ts | 52 +++++++------ ...InGetTextOfComputedPropertyName.errors.txt | 36 --------- ...crashInGetTextOfComputedPropertyName.types | 6 +- .../reference/literalTypesAndDestructuring.js | 37 +++++++++ .../literalTypesAndDestructuring.symbols | 65 ++++++++++++++++ .../literalTypesAndDestructuring.types | 76 +++++++++++++++++++ .../literalTypesAndTypeAssertions.types | 2 +- ...ForObjectBindingPatternDefaultValues.types | 12 +-- ...bjectBindingPatternWithDefaultValues.types | 6 +- .../literal/literalTypesAndDestructuring.ts | 23 ++++++ 10 files changed, 243 insertions(+), 72 deletions(-) delete mode 100644 tests/baselines/reference/crashInGetTextOfComputedPropertyName.errors.txt create mode 100644 tests/baselines/reference/literalTypesAndDestructuring.js create mode 100644 tests/baselines/reference/literalTypesAndDestructuring.symbols create mode 100644 tests/baselines/reference/literalTypesAndDestructuring.types create mode 100644 tests/cases/conformance/types/literal/literalTypesAndDestructuring.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 065fc617857..9d8c7ed924a 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -6981,7 +6981,7 @@ namespace ts { getTypeWithFacts(type, TypeFacts.NEUndefined) : type; } - return getUnionType([getTypeWithFacts(type, TypeFacts.NEUndefined), checkDeclarationInitializer(declaration)], UnionReduction.Subtype); + return widenTypeInferredFromInitializer(declaration, getUnionType([getTypeWithFacts(type, TypeFacts.NEUndefined), checkDeclarationInitializer(declaration)], UnionReduction.Subtype)); } function getTypeForDeclarationFromJSDocComment(declaration: Node) { @@ -7098,7 +7098,7 @@ namespace ts { // Use the type of the initializer expression if one is present and the declaration is // not a parameter of a contextually typed function if (declaration.initializer && !isParameterOfContextuallyTypedFunction(declaration)) { - const type = checkDeclarationInitializer(declaration); + const type = widenTypeInferredFromInitializer(declaration, checkDeclarationInitializer(declaration)); return addOptionality(type, isOptional); } @@ -7330,7 +7330,11 @@ namespace ts { // pattern. Otherwise, it is the type any. function getTypeFromBindingElement(element: BindingElement, includePatternInType?: boolean, reportErrors?: boolean): Type { if (element.initializer) { - return addOptionality(checkDeclarationInitializer(element)); + // The type implied by a binding pattern is independent of context, so we check the initializer with no + // contextual type or, if the element itself is a binding pattern, with the type implied by that binding + // pattern. + const contextualType = isBindingPattern(element.name) ? getTypeFromBindingPattern(element.name, /*includePatternInType*/ true, /*reportErrors*/ false) : unknownType; + return addOptionality(widenTypeInferredFromInitializer(element, checkDeclarationInitializer(element, contextualType))); } if (isBindingPattern(element.name)) { return getTypeFromBindingPattern(element.name, includePatternInType, reportErrors); @@ -21195,9 +21199,10 @@ namespace ts { } function getContextualTypeForBindingElement(declaration: BindingElement): Type | undefined { - const parentDeclaration = declaration.parent.parent; + const parent = declaration.parent.parent; const name = declaration.propertyName || declaration.name; - const parentType = getContextualTypeForVariableLikeDeclaration(parentDeclaration); + const parentType = getContextualTypeForVariableLikeDeclaration(parent) || + parent.kind !== SyntaxKind.BindingElement && parent.initializer && checkDeclarationInitializer(parent); if (parentType && !isBindingPattern(name) && !isComputedNonLiteralName(name)) { const nameType = getLiteralTypeFromPropertyName(name); if (isTypeUsableAsPropertyName(nameType)) { @@ -27661,27 +27666,13 @@ namespace ts { return node.kind === SyntaxKind.TypeAssertionExpression || node.kind === SyntaxKind.AsExpression; } - function checkDeclarationInitializer(declaration: HasExpressionInitializer) { + function checkDeclarationInitializer(declaration: HasExpressionInitializer, contextualType?: Type | undefined) { const initializer = getEffectiveInitializer(declaration)!; - const type = getQuickTypeOfExpression(initializer) || checkExpressionCached(initializer); - const padded = isParameter(declaration) && declaration.name.kind === SyntaxKind.ArrayBindingPattern && + const type = getQuickTypeOfExpression(initializer) || + (contextualType ? checkExpressionWithContextualType(initializer, contextualType, /*inferenceContext*/ undefined, CheckMode.Normal) : checkExpressionCached(initializer)); + return isParameter(declaration) && declaration.name.kind === SyntaxKind.ArrayBindingPattern && isTupleType(type) && !type.target.hasRestElement && getTypeReferenceArity(type) < declaration.name.elements.length ? padTupleType(type, declaration.name) : type; - const widened = getCombinedNodeFlags(declaration) & NodeFlags.Const || - isDeclarationReadonly(declaration) || - isTypeAssertion(initializer) || - isLiteralOfContextualType(padded, getContextualType(initializer)) ? padded : getWidenedLiteralType(padded); - if (isInJSFile(declaration)) { - if (widened.flags & TypeFlags.Nullable) { - reportImplicitAny(declaration, anyType); - return anyType; - } - else if (isEmptyArrayLiteralType(widened)) { - reportImplicitAny(declaration, anyArrayType); - return anyArrayType; - } - } - return widened; } function padTupleType(type: TupleTypeReference, pattern: ArrayBindingPattern) { @@ -27700,6 +27691,21 @@ namespace ts { return createTupleType(elementTypes, type.target.minLength, /*hasRestElement*/ false, type.target.readonly); } + function widenTypeInferredFromInitializer(declaration: HasExpressionInitializer, type: Type) { + const widened = getCombinedNodeFlags(declaration) & NodeFlags.Const || isDeclarationReadonly(declaration) ? type : getWidenedLiteralType(type); + if (isInJSFile(declaration)) { + if (widened.flags & TypeFlags.Nullable) { + reportImplicitAny(declaration, anyType); + return anyType; + } + else if (isEmptyArrayLiteralType(widened)) { + reportImplicitAny(declaration, anyArrayType); + return anyArrayType; + } + } + return widened; + } + function isLiteralOfContextualType(candidateType: Type, contextualType: Type | undefined): boolean { if (contextualType) { if (contextualType.flags & TypeFlags.UnionOrIntersection) { diff --git a/tests/baselines/reference/crashInGetTextOfComputedPropertyName.errors.txt b/tests/baselines/reference/crashInGetTextOfComputedPropertyName.errors.txt deleted file mode 100644 index 4997400674c..00000000000 --- a/tests/baselines/reference/crashInGetTextOfComputedPropertyName.errors.txt +++ /dev/null @@ -1,36 +0,0 @@ -tests/cases/compiler/crashInGetTextOfComputedPropertyName.ts(23,24): error TS2525: Initializer provides no value for this binding element and the binding element has no default value. - - -==== tests/cases/compiler/crashInGetTextOfComputedPropertyName.ts (1 errors) ==== - // https://github.com/Microsoft/TypeScript/issues/29006 - export interface A { type: 'a' } - export interface B { type: 'b' } - export type AB = A | B - - const itemId = 'some-id' - - // --- test on first level --- - const items: { [id: string]: AB } = {} - const { [itemId]: itemOk1 } = items - typeof itemOk1 // pass - - // --- test on second level --- - interface ObjWithItems { - items: {[s: string]: AB} - } - const objWithItems: ObjWithItems = { items: {}} - - const itemOk2 = objWithItems.items[itemId] - typeof itemOk2 // pass - - const { - items: { [itemId]: itemWithTSError } = {} /*happens when default value is provided*/ - ~~~~~~~~~~~~~~~ -!!! error TS2525: Initializer provides no value for this binding element and the binding element has no default value. - } = objWithItems - - // in order to re-produce the error, uncomment next line: - typeof itemWithTSError // :( - - // will result in: - // Error from compilation: TypeError: Cannot read property 'charCodeAt' of undefined TypeError: Cannot read property 'charCodeAt' of undefined \ No newline at end of file diff --git a/tests/baselines/reference/crashInGetTextOfComputedPropertyName.types b/tests/baselines/reference/crashInGetTextOfComputedPropertyName.types index f27e9cc6e60..7d9ce4aa34b 100644 --- a/tests/baselines/reference/crashInGetTextOfComputedPropertyName.types +++ b/tests/baselines/reference/crashInGetTextOfComputedPropertyName.types @@ -56,8 +56,8 @@ const { items: { [itemId]: itemWithTSError } = {} /*happens when default value is provided*/ >items : any >itemId : "some-id" ->itemWithTSError : any ->{} : { "some-id": any; } +>itemWithTSError : AB +>{} : {} } = objWithItems >objWithItems : ObjWithItems @@ -65,7 +65,7 @@ const { // in order to re-produce the error, uncomment next line: typeof itemWithTSError // :( >typeof itemWithTSError : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" ->itemWithTSError : any +>itemWithTSError : AB // will result in: // Error from compilation: TypeError: Cannot read property 'charCodeAt' of undefined TypeError: Cannot read property 'charCodeAt' of undefined diff --git a/tests/baselines/reference/literalTypesAndDestructuring.js b/tests/baselines/reference/literalTypesAndDestructuring.js new file mode 100644 index 00000000000..195f4ceea1d --- /dev/null +++ b/tests/baselines/reference/literalTypesAndDestructuring.js @@ -0,0 +1,37 @@ +//// [literalTypesAndDestructuring.ts] +declare let x: { a: 0 | 1 | undefined }; + +let { a: a1 } = x; +let { a: a2 = 0 } = x; +let { a: a3 = 2 } = x; +let { a: a4 = 2 as const } = x; + +let b1 = x.a; +let b2 = x.a ?? 0; +let b3 = x.a ?? 2; +let b4 = x.a ?? 2 as const; + +// Repro from #35693 + +interface Foo { + bar: 'yo' | 'ha' | undefined; +} + +let { bar = 'yo' } = {} as Foo; + +bar; // "yo" | "ha" + + +//// [literalTypesAndDestructuring.js] +"use strict"; +var _a, _b, _c; +var a1 = x.a; +var _d = x.a, a2 = _d === void 0 ? 0 : _d; +var _e = x.a, a3 = _e === void 0 ? 2 : _e; +var _f = x.a, a4 = _f === void 0 ? 2 : _f; +var b1 = x.a; +var b2 = (_a = x.a) !== null && _a !== void 0 ? _a : 0; +var b3 = (_b = x.a) !== null && _b !== void 0 ? _b : 2; +var b4 = (_c = x.a) !== null && _c !== void 0 ? _c : 2; +var _g = {}.bar, bar = _g === void 0 ? 'yo' : _g; +bar; // "yo" | "ha" diff --git a/tests/baselines/reference/literalTypesAndDestructuring.symbols b/tests/baselines/reference/literalTypesAndDestructuring.symbols new file mode 100644 index 00000000000..e8baec7979d --- /dev/null +++ b/tests/baselines/reference/literalTypesAndDestructuring.symbols @@ -0,0 +1,65 @@ +=== tests/cases/conformance/types/literal/literalTypesAndDestructuring.ts === +declare let x: { a: 0 | 1 | undefined }; +>x : Symbol(x, Decl(literalTypesAndDestructuring.ts, 0, 11)) +>a : Symbol(a, Decl(literalTypesAndDestructuring.ts, 0, 16)) + +let { a: a1 } = x; +>a : Symbol(a, Decl(literalTypesAndDestructuring.ts, 0, 16)) +>a1 : Symbol(a1, Decl(literalTypesAndDestructuring.ts, 2, 5)) +>x : Symbol(x, Decl(literalTypesAndDestructuring.ts, 0, 11)) + +let { a: a2 = 0 } = x; +>a : Symbol(a, Decl(literalTypesAndDestructuring.ts, 0, 16)) +>a2 : Symbol(a2, Decl(literalTypesAndDestructuring.ts, 3, 5)) +>x : Symbol(x, Decl(literalTypesAndDestructuring.ts, 0, 11)) + +let { a: a3 = 2 } = x; +>a : Symbol(a, Decl(literalTypesAndDestructuring.ts, 0, 16)) +>a3 : Symbol(a3, Decl(literalTypesAndDestructuring.ts, 4, 5)) +>x : Symbol(x, Decl(literalTypesAndDestructuring.ts, 0, 11)) + +let { a: a4 = 2 as const } = x; +>a : Symbol(a, Decl(literalTypesAndDestructuring.ts, 0, 16)) +>a4 : Symbol(a4, Decl(literalTypesAndDestructuring.ts, 5, 5)) +>x : Symbol(x, Decl(literalTypesAndDestructuring.ts, 0, 11)) + +let b1 = x.a; +>b1 : Symbol(b1, Decl(literalTypesAndDestructuring.ts, 7, 3)) +>x.a : Symbol(a, Decl(literalTypesAndDestructuring.ts, 0, 16)) +>x : Symbol(x, Decl(literalTypesAndDestructuring.ts, 0, 11)) +>a : Symbol(a, Decl(literalTypesAndDestructuring.ts, 0, 16)) + +let b2 = x.a ?? 0; +>b2 : Symbol(b2, Decl(literalTypesAndDestructuring.ts, 8, 3)) +>x.a : Symbol(a, Decl(literalTypesAndDestructuring.ts, 0, 16)) +>x : Symbol(x, Decl(literalTypesAndDestructuring.ts, 0, 11)) +>a : Symbol(a, Decl(literalTypesAndDestructuring.ts, 0, 16)) + +let b3 = x.a ?? 2; +>b3 : Symbol(b3, Decl(literalTypesAndDestructuring.ts, 9, 3)) +>x.a : Symbol(a, Decl(literalTypesAndDestructuring.ts, 0, 16)) +>x : Symbol(x, Decl(literalTypesAndDestructuring.ts, 0, 11)) +>a : Symbol(a, Decl(literalTypesAndDestructuring.ts, 0, 16)) + +let b4 = x.a ?? 2 as const; +>b4 : Symbol(b4, Decl(literalTypesAndDestructuring.ts, 10, 3)) +>x.a : Symbol(a, Decl(literalTypesAndDestructuring.ts, 0, 16)) +>x : Symbol(x, Decl(literalTypesAndDestructuring.ts, 0, 11)) +>a : Symbol(a, Decl(literalTypesAndDestructuring.ts, 0, 16)) + +// Repro from #35693 + +interface Foo { +>Foo : Symbol(Foo, Decl(literalTypesAndDestructuring.ts, 10, 27)) + + bar: 'yo' | 'ha' | undefined; +>bar : Symbol(Foo.bar, Decl(literalTypesAndDestructuring.ts, 14, 15)) +} + +let { bar = 'yo' } = {} as Foo; +>bar : Symbol(bar, Decl(literalTypesAndDestructuring.ts, 18, 5)) +>Foo : Symbol(Foo, Decl(literalTypesAndDestructuring.ts, 10, 27)) + +bar; // "yo" | "ha" +>bar : Symbol(bar, Decl(literalTypesAndDestructuring.ts, 18, 5)) + diff --git a/tests/baselines/reference/literalTypesAndDestructuring.types b/tests/baselines/reference/literalTypesAndDestructuring.types new file mode 100644 index 00000000000..a7522eed0b0 --- /dev/null +++ b/tests/baselines/reference/literalTypesAndDestructuring.types @@ -0,0 +1,76 @@ +=== tests/cases/conformance/types/literal/literalTypesAndDestructuring.ts === +declare let x: { a: 0 | 1 | undefined }; +>x : { a: 0 | 1 | undefined; } +>a : 0 | 1 | undefined + +let { a: a1 } = x; +>a : any +>a1 : 0 | 1 | undefined +>x : { a: 0 | 1 | undefined; } + +let { a: a2 = 0 } = x; +>a : any +>a2 : 0 | 1 +>0 : 0 +>x : { a: 0 | 1 | undefined; } + +let { a: a3 = 2 } = x; +>a : any +>a3 : number +>2 : 2 +>x : { a: 0 | 1 | undefined; } + +let { a: a4 = 2 as const } = x; +>a : any +>a4 : 0 | 1 | 2 +>2 as const : 2 +>2 : 2 +>x : { a: 0 | 1 | undefined; } + +let b1 = x.a; +>b1 : 0 | 1 | undefined +>x.a : 0 | 1 | undefined +>x : { a: 0 | 1 | undefined; } +>a : 0 | 1 | undefined + +let b2 = x.a ?? 0; +>b2 : 0 | 1 +>x.a ?? 0 : 0 | 1 +>x.a : 0 | 1 | undefined +>x : { a: 0 | 1 | undefined; } +>a : 0 | 1 | undefined +>0 : 0 + +let b3 = x.a ?? 2; +>b3 : number +>x.a ?? 2 : 0 | 1 | 2 +>x.a : 0 | 1 | undefined +>x : { a: 0 | 1 | undefined; } +>a : 0 | 1 | undefined +>2 : 2 + +let b4 = x.a ?? 2 as const; +>b4 : 0 | 1 | 2 +>x.a ?? 2 as const : 0 | 1 | 2 +>x.a : 0 | 1 | undefined +>x : { a: 0 | 1 | undefined; } +>a : 0 | 1 | undefined +>2 as const : 2 +>2 : 2 + +// Repro from #35693 + +interface Foo { + bar: 'yo' | 'ha' | undefined; +>bar : "yo" | "ha" | undefined +} + +let { bar = 'yo' } = {} as Foo; +>bar : "yo" | "ha" +>'yo' : "yo" +>{} as Foo : Foo +>{} : {} + +bar; // "yo" | "ha" +>bar : "yo" | "ha" + diff --git a/tests/baselines/reference/literalTypesAndTypeAssertions.types b/tests/baselines/reference/literalTypesAndTypeAssertions.types index be204673ad3..d0625aa4978 100644 --- a/tests/baselines/reference/literalTypesAndTypeAssertions.types +++ b/tests/baselines/reference/literalTypesAndTypeAssertions.types @@ -36,7 +36,7 @@ let { a = "foo" } = { a: "foo" }; >"foo" : "foo" let { b = "foo" as "foo" } = { b: "bar" }; ->b : "foo" | "bar" +>b : string >"foo" as "foo" : "foo" >"foo" : "foo" >{ b: "bar" } : { b?: "bar"; } diff --git a/tests/baselines/reference/sourceMapValidationDestructuringForObjectBindingPatternDefaultValues.types b/tests/baselines/reference/sourceMapValidationDestructuringForObjectBindingPatternDefaultValues.types index f5d85525cf4..dbef88b8a1c 100644 --- a/tests/baselines/reference/sourceMapValidationDestructuringForObjectBindingPatternDefaultValues.types +++ b/tests/baselines/reference/sourceMapValidationDestructuringForObjectBindingPatternDefaultValues.types @@ -144,7 +144,7 @@ for (let { >"secondary" : "secondary" } = { primary: "none", secondary: "none" } ->{ primary: "none", secondary: "none" } : { primary?: string; secondary?: string; } +>{ primary: "none", secondary: "none" } : { primary: string; secondary: string; } >primary : string >"none" : "none" >secondary : string @@ -182,7 +182,7 @@ for (let { >"secondary" : "secondary" } = { primary: "none", secondary: "none" } ->{ primary: "none", secondary: "none" } : { primary?: string; secondary?: string; } +>{ primary: "none", secondary: "none" } : { primary: string; secondary: string; } >primary : string >"none" : "none" >secondary : string @@ -221,7 +221,7 @@ for (let { >"secondary" : "secondary" } = { primary: "none", secondary: "none" } ->{ primary: "none", secondary: "none" } : { primary?: string; secondary?: string; } +>{ primary: "none", secondary: "none" } : { primary: string; secondary: string; } >primary : string >"none" : "none" >secondary : string @@ -351,7 +351,7 @@ for (let { >"secondary" : "secondary" } = { primary: "none", secondary: "none" } ->{ primary: "none", secondary: "none" } : { primary?: string; secondary?: string; } +>{ primary: "none", secondary: "none" } : { primary: string; secondary: string; } >primary : string >"none" : "none" >secondary : string @@ -394,7 +394,7 @@ for (let { >"secondary" : "secondary" } = { primary: "none", secondary: "none" } ->{ primary: "none", secondary: "none" } : { primary?: string; secondary?: string; } +>{ primary: "none", secondary: "none" } : { primary: string; secondary: string; } >primary : string >"none" : "none" >secondary : string @@ -438,7 +438,7 @@ for (let { >"secondary" : "secondary" } = { primary: "none", secondary: "none" } ->{ primary: "none", secondary: "none" } : { primary?: string; secondary?: string; } +>{ primary: "none", secondary: "none" } : { primary: string; secondary: string; } >primary : string >"none" : "none" >secondary : string diff --git a/tests/baselines/reference/sourceMapValidationDestructuringVariableStatementNestedObjectBindingPatternWithDefaultValues.types b/tests/baselines/reference/sourceMapValidationDestructuringVariableStatementNestedObjectBindingPatternWithDefaultValues.types index f65a35e1e44..33eb2fd923e 100644 --- a/tests/baselines/reference/sourceMapValidationDestructuringVariableStatementNestedObjectBindingPatternWithDefaultValues.types +++ b/tests/baselines/reference/sourceMapValidationDestructuringVariableStatementNestedObjectBindingPatternWithDefaultValues.types @@ -60,7 +60,7 @@ var { >"noSkill" : "noSkill" } = { primary: "noSkill", secondary: "noSkill" } ->{ primary: "noSkill", secondary: "noSkill" } : { primary?: string; secondary?: string; } +>{ primary: "noSkill", secondary: "noSkill" } : { primary: string; secondary: string; } >primary : string >"noSkill" : "noSkill" >secondary : string @@ -89,7 +89,7 @@ var { >"noSkill" : "noSkill" } = { primary: "noSkill", secondary: "noSkill" } ->{ primary: "noSkill", secondary: "noSkill" } : { primary?: string; secondary?: string; } +>{ primary: "noSkill", secondary: "noSkill" } : { primary: string; secondary: string; } >primary : string >"noSkill" : "noSkill" >secondary : string @@ -118,7 +118,7 @@ var { >"noSkill" : "noSkill" } = { primary: "noSkill", secondary: "noSkill" } ->{ primary: "noSkill", secondary: "noSkill" } : { primary?: string; secondary?: string; } +>{ primary: "noSkill", secondary: "noSkill" } : { primary: string; secondary: string; } >primary : string >"noSkill" : "noSkill" >secondary : string diff --git a/tests/cases/conformance/types/literal/literalTypesAndDestructuring.ts b/tests/cases/conformance/types/literal/literalTypesAndDestructuring.ts new file mode 100644 index 00000000000..354ec39469e --- /dev/null +++ b/tests/cases/conformance/types/literal/literalTypesAndDestructuring.ts @@ -0,0 +1,23 @@ +// @strict: true + +declare let x: { a: 0 | 1 | undefined }; + +let { a: a1 } = x; +let { a: a2 = 0 } = x; +let { a: a3 = 2 } = x; +let { a: a4 = 2 as const } = x; + +let b1 = x.a; +let b2 = x.a ?? 0; +let b3 = x.a ?? 2; +let b4 = x.a ?? 2 as const; + +// Repro from #35693 + +interface Foo { + bar: 'yo' | 'ha' | undefined; +} + +let { bar = 'yo' } = {} as Foo; + +bar; // "yo" | "ha"