Skip to content

Introduce introspecing fast path for keyof intersection distribution #23986

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

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 80 additions & 3 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, (<IntersectionType>type).types);
Expand All @@ -8336,7 +8336,12 @@ namespace ts {
!(flags & TypeFlags.Object && (<ObjectType>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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -8468,7 +8545,7 @@ namespace ts {
}

function getIndexType(type: Type, stringsOnly = keyofStringsOnly): Type {
return type.flags & TypeFlags.Union ? getIntersectionType(map((<IntersectionType>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((<IntersectionType>type).types, t => getIndexType(t, stringsOnly))) :
maybeTypeOfKind(type, TypeFlags.InstantiableNonPrimitive) ? getIndexTypeForGenericType(<InstantiableType | UnionOrIntersectionType>type, stringsOnly) :
getObjectFlags(type) & ObjectFlags.Mapped ? getConstraintTypeFromMappedType(<MappedType>type) :
Expand Down
32 changes: 32 additions & 0 deletions tests/baselines/reference/domKeyofUnionNoOOM.errors.txt
Original file line number Diff line number Diff line change
@@ -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];
}
}
47 changes: 47 additions & 0 deletions tests/baselines/reference/domKeyofUnionNoOOM.js
Original file line number Diff line number Diff line change
@@ -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;
92 changes: 92 additions & 0 deletions tests/baselines/reference/domKeyofUnionNoOOM.symbols
Original file line number Diff line number Diff line change
@@ -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))
}
}
Loading