Skip to content
Open
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
73 changes: 68 additions & 5 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1058,6 +1058,7 @@ import {
SyntheticExpression,
TaggedTemplateExpression,
TemplateExpression,
TemplateLiteralTrieNode,
TemplateLiteralType,
TemplateLiteralTypeNode,
Ternary,
Expand Down Expand Up @@ -18219,21 +18220,31 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
function removeStringLiteralsMatchedByTemplateLiterals(types: Type[]) {
const templates = filter(types, isPatternLiteralType) as (TemplateLiteralType | StringMappingType)[];
if (templates.length) {
const templateLiterals = filter(templates, t => !!(t.flags & TypeFlags.TemplateLiteral)) as TemplateLiteralType[];
const stringMappings = filter(templates, t => !!(t.flags & TypeFlags.StringMapping)) as StringMappingType[];
const trie = templateLiterals.length >= 2 ? buildTemplateLiteralTrieFromTypes(templateLiterals) : undefined;
Comment on lines +18223 to +18225
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trie is built whenever there are ≥2 TemplateLiteralTypes, but if most/all templates have an empty texts[0] (e.g. patterns starting with a placeholder like ${string}...), they all end up in root.types and the trie provides little/no pruning while still paying the build + allocation cost. Consider gating trie construction on having enough non-empty prefixes (or splitting empty-prefix templates into a separate array checked linearly) so this optimization doesn't regress cases dominated by empty-prefix templates.

Copilot uses AI. Check for mistakes.
let i = types.length;
while (i > 0) {
i--;
const t = types[i];
if (t.flags & TypeFlags.StringLiteral && some(templates, template => isTypeMatchedByTemplateLiteralOrStringMapping(t, template))) {
if (t.flags & TypeFlags.StringLiteral && isStringLiteralMatchedByTemplates(t as StringLiteralType, trie, templateLiterals, stringMappings)) {
orderedRemoveItemAt(types, i);
}
}
}
}

function isTypeMatchedByTemplateLiteralOrStringMapping(type: Type, template: TemplateLiteralType | StringMappingType) {
return template.flags & TypeFlags.TemplateLiteral ?
isTypeMatchedByTemplateLiteralType(type, template as TemplateLiteralType) :
isMemberOfStringMapping(type, template);
function isStringLiteralMatchedByTemplates(source: StringLiteralType, trie: TemplateLiteralTrieNode | undefined, templateLiterals: readonly TemplateLiteralType[], stringMappings: readonly StringMappingType[]): boolean {
if (trie) {
if (findMatchingTemplateLiteralInTrie(trie, source)) return true;
}
else if (templateLiterals.length) {
if (some(templateLiterals, tl => isTypeMatchedByTemplateLiteralType(source, tl))) return true;
}
if (stringMappings.length) {
if (some(stringMappings, sm => isMemberOfStringMapping(source, sm))) return true;
}
return false;
}

function removeConstrainedTypeVariables(types: Type[]) {
Expand Down Expand Up @@ -28062,6 +28073,58 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return propType && getConstituentTypeForKeyType(unionType, propType);
}

function buildTemplateLiteralTrieFromTypes(templateTypes: readonly TemplateLiteralType[]): TemplateLiteralTrieNode {
const root: TemplateLiteralTrieNode = {};
for (const templateType of templateTypes) {
const prefix = templateType.texts[0];
let node = root;
for (let i = 0; i < prefix.length; i++) {
const ch = prefix.charCodeAt(i);
if (!node.children) {
node.children = new Map();
}
let child = node.children.get(ch);
if (!child) {
child = {};
node.children.set(ch, child);
}
node = child;
}
if (!node.types) {
node.types = [];
}
node.types.push(templateType);
}
return root;
}

function findMatchingTemplateLiteralInTrie(trie: TemplateLiteralTrieNode, source: StringLiteralType): TemplateLiteralType | undefined {
const value = source.value;
let node: TemplateLiteralTrieNode | undefined = trie;
// Check root candidates (empty-prefix templates like `${string}`)
if (node.types) {
for (const type of node.types) {
if (isTypeMatchedByTemplateLiteralType(source, type)) {
return type;
}
}
}
for (let i = 0; i < value.length; i++) {
node = node.children?.get(value.charCodeAt(i));
if (!node) {
return undefined;
}
if (node.types) {
for (const type of node.types) {
if (isTypeMatchedByTemplateLiteralType(source, type)) {
return type;
}
}
}
}
return undefined;
}

function getMatchingUnionConstituentForObjectLiteral(unionType: UnionType, node: ObjectLiteralExpression) {
const keyPropertyName = getKeyPropertyName(unionType);
const propNode = keyPropertyName && find(node.properties, p =>
Expand Down
6 changes: 6 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6960,6 +6960,12 @@ export interface TemplateLiteralType extends InstantiableType {
types: readonly Type[]; // Always at least one element
}

/** @internal */
export interface TemplateLiteralTrieNode {
children?: Map<number, TemplateLiteralTrieNode>; // char code -> child
types?: TemplateLiteralType[]; // template literals whose prefix ends at this node
}

export interface StringMappingType extends InstantiableType {
symbol: Symbol;
type: Type;
Expand Down
Loading