diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index a368a6a8f64aa..d4bee5742c9ff 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -8319,7 +8319,7 @@ namespace ts { return links.resolvedType; } - function addTypeToIntersection(typeSet: Type[], includes: TypeFlags, type: Type) { + function addTypeToIntersection(typeSet: Type[], includes: TypeFlags, type: Type, pos?: number) { const flags = type.flags; if (flags & TypeFlags.Intersection) { return addTypesToIntersection(typeSet, includes, (type).types); @@ -8336,7 +8336,12 @@ namespace ts { !(flags & TypeFlags.Object && (type).objectFlags & ObjectFlags.Anonymous && type.symbol && type.symbol.flags & (SymbolFlags.Function | SymbolFlags.Method) && containsIdenticalType(typeSet, type))) { - typeSet.push(type); + if (pos !== undefined) { + typeSet.splice(pos, 0, type); + } + else { + typeSet.push(type); + } } } return includes; @@ -8382,6 +8387,10 @@ namespace ts { } const typeSet: Type[] = []; const includes = addTypesToIntersection(typeSet, 0, types); + return getIntersectionFromTypeSet(typeSet, includes, aliasSymbol, aliasTypeArguments); + } + + function getIntersectionFromTypeSet(typeSet: Type[], includes: TypeFlags, aliasSymbol?: Symbol, aliasTypeArguments?: Type[]): Type { if (includes & TypeFlags.Never) { return neverType; } @@ -8420,6 +8429,74 @@ namespace ts { return type; } + /** + * This is a fast path replacement for `getIntersectionType` (though it should be interchangable) utilizing common properties of + * `keyof (A | B | C)` types to avoid deeply nesting type creation for all members of the inner union in the common + * case where the inner union members are mostly normal object types. In other scenarios, this is a bit more bookkeeping + * than is usually required. + * + * Essentially, all this does differently than `getIntersectionType` proper is locate the first union within the intersection, + * and the filter any other unions' members through the first union's members. This attempts to avoid recursively calling + * `getIntersectionType` as this process prevents any unions from being added to the intersection right at the start (provided + * they are all literal-membered). + */ + function getIntersectionTypeSpecializedForIndexType(types: Type[]) { + if (types.length === 0) { + return emptyObjectType; + } + if (types.length === 1) { + return types[0]; + } + let maskedUnion: UnionType; + let masked: Type[]; + const result: Type[] = []; + let includes: TypeFlags = 0; + let maskInjectionPosition = 0; + outerLoop: for (let i = 0; i < types.length; i++) { + const keySet = types[i]; + if (keySet.flags & TypeFlags.Union) { + if (!masked) { + maskedUnion = keySet as UnionType; + masked = (keySet as UnionType).types.slice(); + maskInjectionPosition = i; + } + else { + const unfound = []; + for (let i = 0; i < masked.length; i++) { + const key = masked[i]; + if (key.flags & TypeFlags.Literal) { + if (!some((keySet as UnionType).types, t => isTypeIdenticalTo(t, key))) { + unfound.push(i); + } + } + else { + // If a key type in the mask isn't a literal type (so is, eg, `number` or `symbol`) + // We can't easily remove things by checking identity, so we fall back to just adding the + // union normally + includes = addTypeToIntersection(result, includes, getRegularTypeOfLiteralType(keySet)); + continue outerLoop; + } + } + let priors = 0; + for (const index of unfound) { + orderedRemoveItemAt(masked, index - priors); + priors++; + } + } + } + else { + includes = addTypeToIntersection(result, includes, getRegularTypeOfLiteralType(keySet)); + } + } + if (length(masked)) { + includes = addTypeToIntersection(result, includes, maskedUnion.types.length === masked.length ? maskedUnion : getUnionType(masked), maskInjectionPosition); + } + if (result.length === 0 && !includes) { + return neverType; + } + return getIntersectionFromTypeSet(result, includes); + } + function getTypeFromIntersectionTypeNode(node: IntersectionTypeNode): Type { const links = getNodeLinks(node); if (!links.resolvedType) { @@ -8468,7 +8545,7 @@ namespace ts { } function getIndexType(type: Type, stringsOnly = keyofStringsOnly): Type { - return type.flags & TypeFlags.Union ? getIntersectionType(map((type).types, t => getIndexType(t, stringsOnly))) : + return type.flags & TypeFlags.Union ? getIntersectionTypeSpecializedForIndexType(map((type as UnionType).types, t => getIndexType(t, stringsOnly))) : type.flags & TypeFlags.Intersection ? getUnionType(map((type).types, t => getIndexType(t, stringsOnly))) : maybeTypeOfKind(type, TypeFlags.InstantiableNonPrimitive) ? getIndexTypeForGenericType(type, stringsOnly) : getObjectFlags(type) & ObjectFlags.Mapped ? getConstraintTypeFromMappedType(type) : diff --git a/tests/baselines/reference/domKeyofUnionNoOOM.errors.txt b/tests/baselines/reference/domKeyofUnionNoOOM.errors.txt new file mode 100644 index 0000000000000..367a67ee2ea1d --- /dev/null +++ b/tests/baselines/reference/domKeyofUnionNoOOM.errors.txt @@ -0,0 +1,32 @@ +tests/cases/compiler/domKeyofUnionNoOOM.ts(19,12): error TS2536: Type 'T' cannot be used to index type 'HTMLElementTagNameMap'. +tests/cases/compiler/domKeyofUnionNoOOM.ts(19,12): error TS2536: Type 'P' cannot be used to index type 'HTMLElementTagNameMap[T]'. + + +==== tests/cases/compiler/domKeyofUnionNoOOM.ts (2 errors) ==== + export function assertIsElement(node: Node | null): node is Element { + let nodeType = node === null ? null : node.nodeType; + return nodeType === 1; + } + + export function assertNodeTagName< + T extends keyof ElementTagNameMap, + U extends ElementTagNameMap[T]>(node: Node | null, tagName: T): node is U { + if (assertIsElement(node)) { + const nodeTagName = node.tagName.toLowerCase(); + return nodeTagName === tagName; + } + return false; + } + + export function assertNodeProperty< + T extends keyof ElementTagNameMap, + P extends keyof ElementTagNameMap[T], + V extends HTMLElementTagNameMap[T][P]>(node: Node | null, tagName: T, prop: P, value: V) { + ~~~~~~~~~~~~~~~~~~~~~~~~ +!!! error TS2536: Type 'T' cannot be used to index type 'HTMLElementTagNameMap'. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ +!!! error TS2536: Type 'P' cannot be used to index type 'HTMLElementTagNameMap[T]'. + if (assertNodeTagName(node, tagName)) { + node[prop]; + } + } \ No newline at end of file diff --git a/tests/baselines/reference/domKeyofUnionNoOOM.js b/tests/baselines/reference/domKeyofUnionNoOOM.js new file mode 100644 index 0000000000000..73168e0265cb5 --- /dev/null +++ b/tests/baselines/reference/domKeyofUnionNoOOM.js @@ -0,0 +1,47 @@ +//// [domKeyofUnionNoOOM.ts] +export function assertIsElement(node: Node | null): node is Element { + let nodeType = node === null ? null : node.nodeType; + return nodeType === 1; +} + +export function assertNodeTagName< +T extends keyof ElementTagNameMap, +U extends ElementTagNameMap[T]>(node: Node | null, tagName: T): node is U { + if (assertIsElement(node)) { + const nodeTagName = node.tagName.toLowerCase(); + return nodeTagName === tagName; + } + return false; +} + +export function assertNodeProperty< + T extends keyof ElementTagNameMap, + P extends keyof ElementTagNameMap[T], + V extends HTMLElementTagNameMap[T][P]>(node: Node | null, tagName: T, prop: P, value: V) { + if (assertNodeTagName(node, tagName)) { + node[prop]; + } +} + +//// [domKeyofUnionNoOOM.js] +"use strict"; +exports.__esModule = true; +function assertIsElement(node) { + var nodeType = node === null ? null : node.nodeType; + return nodeType === 1; +} +exports.assertIsElement = assertIsElement; +function assertNodeTagName(node, tagName) { + if (assertIsElement(node)) { + var nodeTagName = node.tagName.toLowerCase(); + return nodeTagName === tagName; + } + return false; +} +exports.assertNodeTagName = assertNodeTagName; +function assertNodeProperty(node, tagName, prop, value) { + if (assertNodeTagName(node, tagName)) { + node[prop]; + } +} +exports.assertNodeProperty = assertNodeProperty; diff --git a/tests/baselines/reference/domKeyofUnionNoOOM.symbols b/tests/baselines/reference/domKeyofUnionNoOOM.symbols new file mode 100644 index 0000000000000..26a5b364e70d4 --- /dev/null +++ b/tests/baselines/reference/domKeyofUnionNoOOM.symbols @@ -0,0 +1,92 @@ +=== tests/cases/compiler/domKeyofUnionNoOOM.ts === +export function assertIsElement(node: Node | null): node is Element { +>assertIsElement : Symbol(assertIsElement, Decl(domKeyofUnionNoOOM.ts, 0, 0)) +>node : Symbol(node, Decl(domKeyofUnionNoOOM.ts, 0, 32)) +>Node : Symbol(Node, Decl(lib.dom.d.ts, --, --), Decl(lib.dom.d.ts, --, --)) +>node : Symbol(node, Decl(domKeyofUnionNoOOM.ts, 0, 32)) +>Element : Symbol(Element, Decl(lib.dom.d.ts, --, --), Decl(lib.dom.d.ts, --, --)) + + let nodeType = node === null ? null : node.nodeType; +>nodeType : Symbol(nodeType, Decl(domKeyofUnionNoOOM.ts, 1, 4)) +>node : Symbol(node, Decl(domKeyofUnionNoOOM.ts, 0, 32)) +>node.nodeType : Symbol(Node.nodeType, Decl(lib.dom.d.ts, --, --)) +>node : Symbol(node, Decl(domKeyofUnionNoOOM.ts, 0, 32)) +>nodeType : Symbol(Node.nodeType, Decl(lib.dom.d.ts, --, --)) + + return nodeType === 1; +>nodeType : Symbol(nodeType, Decl(domKeyofUnionNoOOM.ts, 1, 4)) +} + +export function assertNodeTagName< +>assertNodeTagName : Symbol(assertNodeTagName, Decl(domKeyofUnionNoOOM.ts, 3, 1)) + +T extends keyof ElementTagNameMap, +>T : Symbol(T, Decl(domKeyofUnionNoOOM.ts, 5, 34)) +>ElementTagNameMap : Symbol(ElementTagNameMap, Decl(lib.dom.d.ts, --, --)) + +U extends ElementTagNameMap[T]>(node: Node | null, tagName: T): node is U { +>U : Symbol(U, Decl(domKeyofUnionNoOOM.ts, 6, 34)) +>ElementTagNameMap : Symbol(ElementTagNameMap, Decl(lib.dom.d.ts, --, --)) +>T : Symbol(T, Decl(domKeyofUnionNoOOM.ts, 5, 34)) +>node : Symbol(node, Decl(domKeyofUnionNoOOM.ts, 7, 32)) +>Node : Symbol(Node, Decl(lib.dom.d.ts, --, --), Decl(lib.dom.d.ts, --, --)) +>tagName : Symbol(tagName, Decl(domKeyofUnionNoOOM.ts, 7, 50)) +>T : Symbol(T, Decl(domKeyofUnionNoOOM.ts, 5, 34)) +>node : Symbol(node, Decl(domKeyofUnionNoOOM.ts, 7, 32)) +>U : Symbol(U, Decl(domKeyofUnionNoOOM.ts, 6, 34)) + + if (assertIsElement(node)) { +>assertIsElement : Symbol(assertIsElement, Decl(domKeyofUnionNoOOM.ts, 0, 0)) +>node : Symbol(node, Decl(domKeyofUnionNoOOM.ts, 7, 32)) + + const nodeTagName = node.tagName.toLowerCase(); +>nodeTagName : Symbol(nodeTagName, Decl(domKeyofUnionNoOOM.ts, 9, 7)) +>node.tagName.toLowerCase : Symbol(String.toLowerCase, Decl(lib.es5.d.ts, --, --)) +>node.tagName : Symbol(Element.tagName, Decl(lib.dom.d.ts, --, --)) +>node : Symbol(node, Decl(domKeyofUnionNoOOM.ts, 7, 32)) +>tagName : Symbol(Element.tagName, Decl(lib.dom.d.ts, --, --)) +>toLowerCase : Symbol(String.toLowerCase, Decl(lib.es5.d.ts, --, --)) + + return nodeTagName === tagName; +>nodeTagName : Symbol(nodeTagName, Decl(domKeyofUnionNoOOM.ts, 9, 7)) +>tagName : Symbol(tagName, Decl(domKeyofUnionNoOOM.ts, 7, 50)) + } + return false; +} + +export function assertNodeProperty< +>assertNodeProperty : Symbol(assertNodeProperty, Decl(domKeyofUnionNoOOM.ts, 13, 1)) + + T extends keyof ElementTagNameMap, +>T : Symbol(T, Decl(domKeyofUnionNoOOM.ts, 15, 35)) +>ElementTagNameMap : Symbol(ElementTagNameMap, Decl(lib.dom.d.ts, --, --)) + + P extends keyof ElementTagNameMap[T], +>P : Symbol(P, Decl(domKeyofUnionNoOOM.ts, 16, 35)) +>ElementTagNameMap : Symbol(ElementTagNameMap, Decl(lib.dom.d.ts, --, --)) +>T : Symbol(T, Decl(domKeyofUnionNoOOM.ts, 15, 35)) + + V extends HTMLElementTagNameMap[T][P]>(node: Node | null, tagName: T, prop: P, value: V) { +>V : Symbol(V, Decl(domKeyofUnionNoOOM.ts, 17, 38)) +>HTMLElementTagNameMap : Symbol(HTMLElementTagNameMap, Decl(lib.dom.d.ts, --, --)) +>T : Symbol(T, Decl(domKeyofUnionNoOOM.ts, 15, 35)) +>P : Symbol(P, Decl(domKeyofUnionNoOOM.ts, 16, 35)) +>node : Symbol(node, Decl(domKeyofUnionNoOOM.ts, 18, 40)) +>Node : Symbol(Node, Decl(lib.dom.d.ts, --, --), Decl(lib.dom.d.ts, --, --)) +>tagName : Symbol(tagName, Decl(domKeyofUnionNoOOM.ts, 18, 58)) +>T : Symbol(T, Decl(domKeyofUnionNoOOM.ts, 15, 35)) +>prop : Symbol(prop, Decl(domKeyofUnionNoOOM.ts, 18, 70)) +>P : Symbol(P, Decl(domKeyofUnionNoOOM.ts, 16, 35)) +>value : Symbol(value, Decl(domKeyofUnionNoOOM.ts, 18, 79)) +>V : Symbol(V, Decl(domKeyofUnionNoOOM.ts, 17, 38)) + + if (assertNodeTagName(node, tagName)) { +>assertNodeTagName : Symbol(assertNodeTagName, Decl(domKeyofUnionNoOOM.ts, 3, 1)) +>node : Symbol(node, Decl(domKeyofUnionNoOOM.ts, 18, 40)) +>tagName : Symbol(tagName, Decl(domKeyofUnionNoOOM.ts, 18, 58)) + + node[prop]; +>node : Symbol(node, Decl(domKeyofUnionNoOOM.ts, 18, 40)) +>prop : Symbol(prop, Decl(domKeyofUnionNoOOM.ts, 18, 70)) + } +} diff --git a/tests/baselines/reference/domKeyofUnionNoOOM.types b/tests/baselines/reference/domKeyofUnionNoOOM.types new file mode 100644 index 0000000000000..e0e93b5b74d0d --- /dev/null +++ b/tests/baselines/reference/domKeyofUnionNoOOM.types @@ -0,0 +1,107 @@ +=== tests/cases/compiler/domKeyofUnionNoOOM.ts === +export function assertIsElement(node: Node | null): node is Element { +>assertIsElement : (node: Node) => node is Element +>node : Node +>Node : Node +>null : null +>node : any +>Element : Element + + let nodeType = node === null ? null : node.nodeType; +>nodeType : number +>node === null ? null : node.nodeType : number +>node === null : boolean +>node : Node +>null : null +>null : null +>node.nodeType : number +>node : Node +>nodeType : number + + return nodeType === 1; +>nodeType === 1 : boolean +>nodeType : number +>1 : 1 +} + +export function assertNodeTagName< +>assertNodeTagName : (node: Node, tagName: T) => node is U + +T extends keyof ElementTagNameMap, +>T : T +>ElementTagNameMap : ElementTagNameMap + +U extends ElementTagNameMap[T]>(node: Node | null, tagName: T): node is U { +>U : U +>ElementTagNameMap : ElementTagNameMap +>T : T +>node : Node +>Node : Node +>null : null +>tagName : T +>T : T +>node : any +>U : U + + if (assertIsElement(node)) { +>assertIsElement(node) : boolean +>assertIsElement : (node: Node) => node is Element +>node : Node + + const nodeTagName = node.tagName.toLowerCase(); +>nodeTagName : string +>node.tagName.toLowerCase() : string +>node.tagName.toLowerCase : () => string +>node.tagName : string +>node : Element +>tagName : string +>toLowerCase : () => string + + return nodeTagName === tagName; +>nodeTagName === tagName : boolean +>nodeTagName : string +>tagName : T + } + return false; +>false : false +} + +export function assertNodeProperty< +>assertNodeProperty : (node: Node, tagName: T, prop: P, value: V) => void + + T extends keyof ElementTagNameMap, +>T : T +>ElementTagNameMap : ElementTagNameMap + + P extends keyof ElementTagNameMap[T], +>P : P +>ElementTagNameMap : ElementTagNameMap +>T : T + + V extends HTMLElementTagNameMap[T][P]>(node: Node | null, tagName: T, prop: P, value: V) { +>V : V +>HTMLElementTagNameMap : HTMLElementTagNameMap +>T : T +>P : P +>node : Node +>Node : Node +>null : null +>tagName : T +>T : T +>prop : P +>P : P +>value : V +>V : V + + if (assertNodeTagName(node, tagName)) { +>assertNodeTagName(node, tagName) : boolean +>assertNodeTagName : (node: Node, tagName: T) => node is U +>node : Node +>tagName : T + + node[prop]; +>node[prop] : ElementTagNameMap[T][P] +>node : ElementTagNameMap[T] +>prop : P + } +} diff --git a/tests/cases/compiler/domKeyofUnionNoOOM.ts b/tests/cases/compiler/domKeyofUnionNoOOM.ts new file mode 100644 index 0000000000000..f22aa24e071f7 --- /dev/null +++ b/tests/cases/compiler/domKeyofUnionNoOOM.ts @@ -0,0 +1,24 @@ +// @lib: dom,es6 +export function assertIsElement(node: Node | null): node is Element { + let nodeType = node === null ? null : node.nodeType; + return nodeType === 1; +} + +export function assertNodeTagName< +T extends keyof ElementTagNameMap, +U extends ElementTagNameMap[T]>(node: Node | null, tagName: T): node is U { + if (assertIsElement(node)) { + const nodeTagName = node.tagName.toLowerCase(); + return nodeTagName === tagName; + } + return false; +} + +export function assertNodeProperty< + T extends keyof ElementTagNameMap, + P extends keyof ElementTagNameMap[T], + V extends HTMLElementTagNameMap[T][P]>(node: Node | null, tagName: T, prop: P, value: V) { + if (assertNodeTagName(node, tagName)) { + node[prop]; + } +} \ No newline at end of file