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

Defer union or intersection property type normalization #31486

Merged
merged 3 commits into from May 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
77 changes: 69 additions & 8 deletions src/compiler/checker.ts
Expand Up @@ -5921,7 +5921,20 @@ namespace ts {
return anyType;
}

function getTypeOfSymbolWithDeferredType(symbol: Symbol) {
const links = getSymbolLinks(symbol);
if (!links.type) {
Debug.assertDefined(links.deferralParent);
Debug.assertDefined(links.deferralConstituents);
links.type = links.deferralParent!.flags & TypeFlags.Union ? getUnionType(links.deferralConstituents!) : getIntersectionType(links.deferralConstituents!);
}
return links.type;
}

function getTypeOfSymbol(symbol: Symbol): Type {
if (getCheckFlags(symbol) & CheckFlags.DeferredType) {
return getTypeOfSymbolWithDeferredType(symbol);
}
if (getCheckFlags(symbol) & CheckFlags.Instantiated) {
return getTypeOfInstantiatedSymbol(symbol);
}
Expand Down Expand Up @@ -8061,7 +8074,15 @@ namespace ts {

result.declarations = declarations!;
result.nameType = nameType;
result.type = isUnion ? getUnionType(propTypes) : getIntersectionType(propTypes);
if (propTypes.length > 2) {
// When `propTypes` has the potential to explode in size when normalized, defer normalization until absolutely needed
result.checkFlags |= CheckFlags.DeferredType;
result.deferralParent = containingType;
result.deferralConstituents = propTypes;
}
else {
result.type = isUnion ? getUnionType(propTypes) : getIntersectionType(propTypes);
}
return result;
}

Expand Down Expand Up @@ -12313,8 +12334,8 @@ namespace ts {
return false;
}

function isIgnoredJsxProperty(source: Type, sourceProp: Symbol, targetMemberType: Type | undefined) {
return getObjectFlags(source) & ObjectFlags.JsxAttributes && !(isUnhyphenatedJsxName(sourceProp.escapedName) || targetMemberType);
function isIgnoredJsxProperty(source: Type, sourceProp: Symbol) {
return getObjectFlags(source) & ObjectFlags.JsxAttributes && !isUnhyphenatedJsxName(sourceProp.escapedName);
}

/**
Expand Down Expand Up @@ -13495,6 +13516,49 @@ namespace ts {
return result || properties;
}

function isPropertySymbolTypeRelated(sourceProp: Symbol, targetProp: Symbol, getTypeOfSourceProperty: (sym: Symbol) => Type, reportErrors: boolean): Ternary {
const targetIsOptional = strictNullChecks && !!(getCheckFlags(targetProp) & CheckFlags.Partial);
const source = getTypeOfSourceProperty(sourceProp);
if (getCheckFlags(targetProp) & CheckFlags.DeferredType && !getSymbolLinks(targetProp).type) {
// Rather than resolving (and normalizing) the type, relate constituent-by-constituent without performing normalization or seconadary passes
const links = getSymbolLinks(targetProp);
Debug.assertDefined(links.deferralParent);
Debug.assertDefined(links.deferralConstituents);
const unionParent = !!(links.deferralParent!.flags & TypeFlags.Union);
let result = unionParent ? Ternary.False : Ternary.True;
const targetTypes = links.deferralConstituents!;
for (const targetType of targetTypes) {
const related = isRelatedTo(source, targetType, /*reportErrors*/ false, /*headMessage*/ undefined, /*isIntersectionConstituent*/ !unionParent);
if (!unionParent) {
if (!related) {
// Can't assign to a target individually - have to fallback to assigning to the _whole_ intersection (which forces normalization)
return isRelatedTo(source, addOptionality(getTypeOfSymbol(targetProp), targetIsOptional), reportErrors);
}
result &= related;
}
else {
if (related) {
return related;
}
}
}
if (unionParent && !result && targetIsOptional) {
result = isRelatedTo(source, undefinedType);
}
if (unionParent && !result && reportErrors) {
// The easiest way to get the right errors here is to un-defer (which may be costly)
// If it turns out this is too costly too often, we can replicate the error handling logic within
// typeRelatedToSomeType without the discriminatable type branch (as that requires a manifest union
// type on which to hand discriminable properties, which we are expressly trying to avoid here)
return isRelatedTo(source, addOptionality(getTypeOfSymbol(targetProp), targetIsOptional), reportErrors);
}
return result;
}
else {
return isRelatedTo(source, addOptionality(getTypeOfSymbol(targetProp), targetIsOptional), reportErrors);
}
}

function propertyRelatedTo(source: Type, target: Type, sourceProp: Symbol, targetProp: Symbol, getTypeOfSourceProperty: (sym: Symbol) => Type, reportErrors: boolean): Ternary {
const sourcePropFlags = getDeclarationModifierFlagsFromSymbol(sourceProp);
const targetPropFlags = getDeclarationModifierFlagsFromSymbol(targetProp);
Expand Down Expand Up @@ -13537,7 +13601,7 @@ namespace ts {
return Ternary.False;
}
// If the target comes from a partial union prop, allow `undefined` in the target type
const related = isRelatedTo(getTypeOfSourceProperty(sourceProp), addOptionality(getTypeOfSymbol(targetProp), !!(getCheckFlags(targetProp) & CheckFlags.Partial)), reportErrors);
const related = isPropertySymbolTypeRelated(sourceProp, targetProp, getTypeOfSourceProperty, reportErrors);
if (!related) {
if (reportErrors) {
reportError(Diagnostics.Types_of_property_0_are_incompatible, symbolToString(targetProp));
Expand Down Expand Up @@ -13649,9 +13713,6 @@ namespace ts {
if (!(targetProp.flags & SymbolFlags.Prototype)) {
const sourceProp = getPropertyOfType(source, targetProp.escapedName);
if (sourceProp && sourceProp !== targetProp) {
if (isIgnoredJsxProperty(source, sourceProp, getTypeOfSymbol(targetProp))) {
continue;
}
const related = propertyRelatedTo(source, target, sourceProp, targetProp, getTypeOfSymbol, reportErrors);
if (!related) {
return Ternary.False;
Expand Down Expand Up @@ -13797,7 +13858,7 @@ namespace ts {
function eachPropertyRelatedTo(source: Type, target: Type, kind: IndexKind, reportErrors: boolean): Ternary {
let result = Ternary.True;
for (const prop of getPropertiesOfObjectType(source)) {
if (isIgnoredJsxProperty(source, prop, /*targetMemberType*/ undefined)) {
if (isIgnoredJsxProperty(source, prop)) {
continue;
}
// Skip over symbol-named members
Expand Down
3 changes: 3 additions & 0 deletions src/compiler/types.ts
Expand Up @@ -3752,6 +3752,8 @@ namespace ts {
extendedContainers?: Symbol[]; // Containers (other than the parent) which this symbol is aliased in
extendedContainersByFile?: Map<Symbol[]>; // Containers (other than the parent) which this symbol is aliased in
variances?: VarianceFlags[]; // Alias symbol type argument variance cache
deferralConstituents?: Type[]; // Calculated list of constituents for a deferred type
deferralParent?: Type; // Source union/intersection of a deferred type
}

/* @internal */
Expand All @@ -3778,6 +3780,7 @@ namespace ts {
ReverseMapped = 1 << 13, // Property of reverse-inferred homomorphic mapped type
OptionalParameter = 1 << 14, // Optional parameter
RestParameter = 1 << 15, // Rest parameter
DeferredType = 1 << 16, // Calculation of the type of this symbol is deferred due to processing costs, should be fetched with `getTypeOfSymbolWithDeferredType`
Synthetic = SyntheticProperty | SyntheticMethod,
Discriminant = HasNonUniformType | HasLiteralType,
Partial = ReadPartial | WritePartial
Expand Down
34 changes: 34 additions & 0 deletions tests/baselines/reference/intersectionTypeMembers.js
Expand Up @@ -43,6 +43,28 @@ const de: D & E = {
other: { g: 101 }
}
}

// Additional test case with >2 doubly nested members so fix for #31441 is tested w/ excess props
interface F {
nested: { doublyNested: { g: string; } }
}

interface G {
nested: { doublyNested: { h: string; } }
}

const defg: D & E & F & G = {
nested: {
doublyNested: {
d: 'yes',
f: 'no',
g: 'ok',
h: 'affirmative'
},
different: { e: 12 },
other: { g: 101 }
}
}


//// [intersectionTypeMembers.js]
Expand All @@ -69,3 +91,15 @@ var de = {
other: { g: 101 }
}
};
var defg = {
nested: {
doublyNested: {
d: 'yes',
f: 'no',
g: 'ok',
h: 'affirmative'
},
different: { e: 12 },
other: { g: 101 }
}
};
55 changes: 55 additions & 0 deletions tests/baselines/reference/intersectionTypeMembers.symbols
Expand Up @@ -146,3 +146,58 @@ const de: D & E = {
}
}

// Additional test case with >2 doubly nested members so fix for #31441 is tested w/ excess props
interface F {
>F : Symbol(F, Decl(intersectionTypeMembers.ts, 43, 1))

nested: { doublyNested: { g: string; } }
>nested : Symbol(F.nested, Decl(intersectionTypeMembers.ts, 46, 13))
>doublyNested : Symbol(doublyNested, Decl(intersectionTypeMembers.ts, 47, 13))
>g : Symbol(g, Decl(intersectionTypeMembers.ts, 47, 29))
}

interface G {
>G : Symbol(G, Decl(intersectionTypeMembers.ts, 48, 1))

nested: { doublyNested: { h: string; } }
>nested : Symbol(G.nested, Decl(intersectionTypeMembers.ts, 50, 13))
>doublyNested : Symbol(doublyNested, Decl(intersectionTypeMembers.ts, 51, 13))
>h : Symbol(h, Decl(intersectionTypeMembers.ts, 51, 29))
}

const defg: D & E & F & G = {
>defg : Symbol(defg, Decl(intersectionTypeMembers.ts, 54, 5))
>D : Symbol(D, Decl(intersectionTypeMembers.ts, 26, 14))
>E : Symbol(E, Decl(intersectionTypeMembers.ts, 30, 1))
>F : Symbol(F, Decl(intersectionTypeMembers.ts, 43, 1))
>G : Symbol(G, Decl(intersectionTypeMembers.ts, 48, 1))

nested: {
>nested : Symbol(nested, Decl(intersectionTypeMembers.ts, 54, 29))

doublyNested: {
>doublyNested : Symbol(doublyNested, Decl(intersectionTypeMembers.ts, 55, 13))

d: 'yes',
>d : Symbol(d, Decl(intersectionTypeMembers.ts, 56, 23))

f: 'no',
>f : Symbol(f, Decl(intersectionTypeMembers.ts, 57, 21))

g: 'ok',
>g : Symbol(g, Decl(intersectionTypeMembers.ts, 58, 20))

h: 'affirmative'
>h : Symbol(h, Decl(intersectionTypeMembers.ts, 59, 20))

},
different: { e: 12 },
>different : Symbol(different, Decl(intersectionTypeMembers.ts, 61, 10))
>e : Symbol(e, Decl(intersectionTypeMembers.ts, 62, 20))

other: { g: 101 }
>other : Symbol(other, Decl(intersectionTypeMembers.ts, 62, 29))
>g : Symbol(g, Decl(intersectionTypeMembers.ts, 63, 16))
}
}

58 changes: 58 additions & 0 deletions tests/baselines/reference/intersectionTypeMembers.types
Expand Up @@ -148,3 +148,61 @@ const de: D & E = {
}
}

// Additional test case with >2 doubly nested members so fix for #31441 is tested w/ excess props
interface F {
nested: { doublyNested: { g: string; } }
>nested : { doublyNested: { g: string; }; }
>doublyNested : { g: string; }
>g : string
}

interface G {
nested: { doublyNested: { h: string; } }
>nested : { doublyNested: { h: string; }; }
>doublyNested : { h: string; }
>h : string
}

const defg: D & E & F & G = {
>defg : D & E & F & G
>{ nested: { doublyNested: { d: 'yes', f: 'no', g: 'ok', h: 'affirmative' }, different: { e: 12 }, other: { g: 101 } }} : { nested: { doublyNested: { d: string; f: string; g: string; h: string; }; different: { e: number; }; other: { g: number; }; }; }

nested: {
>nested : { doublyNested: { d: string; f: string; g: string; h: string; }; different: { e: number; }; other: { g: number; }; }
>{ doublyNested: { d: 'yes', f: 'no', g: 'ok', h: 'affirmative' }, different: { e: 12 }, other: { g: 101 } } : { doublyNested: { d: string; f: string; g: string; h: string; }; different: { e: number; }; other: { g: number; }; }

doublyNested: {
>doublyNested : { d: string; f: string; g: string; h: string; }
>{ d: 'yes', f: 'no', g: 'ok', h: 'affirmative' } : { d: string; f: string; g: string; h: string; }

d: 'yes',
>d : string
>'yes' : "yes"

f: 'no',
>f : string
>'no' : "no"

g: 'ok',
>g : string
>'ok' : "ok"

h: 'affirmative'
>h : string
>'affirmative' : "affirmative"

},
different: { e: 12 },
>different : { e: number; }
>{ e: 12 } : { e: number; }
>e : number
>12 : 12

other: { g: 101 }
>other : { g: number; }
>{ g: 101 } : { g: number; }
>g : number
>101 : 101
}
}

@@ -1,5 +1,5 @@
tests/cases/compiler/normalizedIntersectionTooComplex.ts(36,14): error TS2590: Expression produces a union type that is too complex to represent.
tests/cases/compiler/normalizedIntersectionTooComplex.ts(36,40): error TS7006: Parameter 'x' implicitly has an 'any' type.
tests/cases/compiler/normalizedIntersectionTooComplex.ts(36,40): error TS2590: Expression produces a union type that is too complex to represent.


==== tests/cases/compiler/normalizedIntersectionTooComplex.ts (2 errors) ====
Expand Down Expand Up @@ -39,8 +39,8 @@ tests/cases/compiler/normalizedIntersectionTooComplex.ts(36,40): error TS7006: P
declare var all: keyof Big;
const ctor = getCtor(all);
const comp = ctor({ common: "ok", ref: x => console.log(x) });
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
!!! error TS2590: Expression produces a union type that is too complex to represent.
~
!!! error TS7006: Parameter 'x' implicitly has an 'any' type.
~~~~~~~~~~~~~~~~~~~
!!! error TS2590: Expression produces a union type that is too complex to represent.

33 changes: 33 additions & 0 deletions tests/baselines/reference/reactTagNameComponentWithPropsNoOOM.js
@@ -0,0 +1,33 @@
//// [reactTagNameComponentWithPropsNoOOM.tsx]
/// <reference path="/.lib/react16.d.ts" />

import * as React from "react";
declare const Tag: keyof React.ReactHTML;

const classes = "";
const rest: {} = {};
const children: any[] = [];
<Tag className={classes} {...rest}>
{children}
</Tag>

//// [reactTagNameComponentWithPropsNoOOM.js]
"use strict";
/// <reference path="react16.d.ts" />
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
exports.__esModule = true;
var React = require("react");
var classes = "";
var rest = {};
var children = [];
React.createElement(Tag, __assign({ className: classes }, rest), children);
@@ -0,0 +1,32 @@
=== tests/cases/compiler/reactTagNameComponentWithPropsNoOOM.tsx ===
/// <reference path="react16.d.ts" />

import * as React from "react";
>React : Symbol(React, Decl(reactTagNameComponentWithPropsNoOOM.tsx, 2, 6))

declare const Tag: keyof React.ReactHTML;
>Tag : Symbol(Tag, Decl(reactTagNameComponentWithPropsNoOOM.tsx, 3, 13))
>React : Symbol(React, Decl(reactTagNameComponentWithPropsNoOOM.tsx, 2, 6))
>ReactHTML : Symbol(React.ReactHTML, Decl(react16.d.ts, 2089, 9))

const classes = "";
>classes : Symbol(classes, Decl(reactTagNameComponentWithPropsNoOOM.tsx, 5, 5))

const rest: {} = {};
>rest : Symbol(rest, Decl(reactTagNameComponentWithPropsNoOOM.tsx, 6, 5))

const children: any[] = [];
>children : Symbol(children, Decl(reactTagNameComponentWithPropsNoOOM.tsx, 7, 5))

<Tag className={classes} {...rest}>
>Tag : Symbol(Tag, Decl(reactTagNameComponentWithPropsNoOOM.tsx, 3, 13))
>className : Symbol(className, Decl(reactTagNameComponentWithPropsNoOOM.tsx, 8, 4))
>classes : Symbol(classes, Decl(reactTagNameComponentWithPropsNoOOM.tsx, 5, 5))
>rest : Symbol(rest, Decl(reactTagNameComponentWithPropsNoOOM.tsx, 6, 5))

{children}
>children : Symbol(children, Decl(reactTagNameComponentWithPropsNoOOM.tsx, 7, 5))

</Tag>
>Tag : Symbol(Tag, Decl(reactTagNameComponentWithPropsNoOOM.tsx, 3, 13))