Skip to content

Commit

Permalink
Fix support for intersections in template literal placeholder types (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
ahejlsberg committed Nov 21, 2023
1 parent 38ef79e commit c266e47
Show file tree
Hide file tree
Showing 8 changed files with 101 additions and 49 deletions.
50 changes: 28 additions & 22 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16925,10 +16925,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

function removeStringLiteralsMatchedByTemplateLiterals(types: Type[]) {
const templates = filter(types, t =>
!!(t.flags & TypeFlags.TemplateLiteral) &&
isPatternLiteralType(t) &&
(t as TemplateLiteralType).types.every(t => !(t.flags & TypeFlags.Intersection) || !areIntersectedTypesAvoidingPrimitiveReduction((t as IntersectionType).types))) as TemplateLiteralType[];
const templates = filter(types, t => !!(t.flags & TypeFlags.TemplateLiteral) && isPatternLiteralType(t)) as TemplateLiteralType[];
if (templates.length) {
let i = types.length;
while (i > 0) {
Expand Down Expand Up @@ -17439,20 +17436,17 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return reduceLeft(types, (n, t) => n + getConstituentCount(t), 0);
}

function areIntersectedTypesAvoidingPrimitiveReduction(types: Type[], primitiveFlags = TypeFlags.String | TypeFlags.Number | TypeFlags.BigInt): boolean {
if (types.length !== 2) {
return false;
}
const [t1, t2] = types;
return !!(t1.flags & primitiveFlags) && t2 === emptyTypeLiteralType || !!(t2.flags & primitiveFlags) && t1 === emptyTypeLiteralType;
}

function getTypeFromIntersectionTypeNode(node: IntersectionTypeNode): Type {
const links = getNodeLinks(node);
if (!links.resolvedType) {
const aliasSymbol = getAliasSymbolForTypeNode(node);
const types = map(node.types, getTypeFromTypeNode);
const noSupertypeReduction = areIntersectedTypesAvoidingPrimitiveReduction(types);
// We perform no supertype reduction for X & {} or {} & X, where X is one of string, number, bigint,
// or a pattern literal template type. This enables union types like "a" | "b" | string & {} or
// "aa" | "ab" | `a${string}` which preserve the literal types for purposes of statement completion.
const emptyIndex = types.length === 2 ? types.indexOf(emptyTypeLiteralType) : -1;
const t = emptyIndex >= 0 ? types[1 - emptyIndex] : unknownType;
const noSupertypeReduction = !!(t.flags & (TypeFlags.String | TypeFlags.Number | TypeFlags.BigInt) || t.flags & TypeFlags.TemplateLiteral && isPatternLiteralType(t));
links.resolvedType = getIntersectionType(types, aliasSymbol, getTypeArgumentsForAliasSymbol(aliasSymbol), noSupertypeReduction);
}
return links.resolvedType;
Expand Down Expand Up @@ -17732,7 +17726,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {

function createTemplateLiteralType(texts: readonly string[], types: readonly Type[]) {
const type = createType(TypeFlags.TemplateLiteral) as TemplateLiteralType;
type.objectFlags = getPropagatingFlagsOfTypes(types, /*excludeKinds*/ TypeFlags.Nullable);
type.texts = texts;
type.types = types;
return type;
Expand Down Expand Up @@ -18057,12 +18050,25 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {

function isPatternLiteralPlaceholderType(type: Type): boolean {
if (type.flags & TypeFlags.Intersection) {
return !isGenericType(type) && some((type as IntersectionType).types, t => !!(t.flags & (TypeFlags.Literal | TypeFlags.Nullable)) || isPatternLiteralPlaceholderType(t));
// Return true if the intersection consists of one or more placeholders and zero or
// more object type tags.
let seenPlaceholder = false;
for (const t of (type as IntersectionType).types) {
if (t.flags & (TypeFlags.Literal | TypeFlags.Nullable) || isPatternLiteralPlaceholderType(t)) {
seenPlaceholder = true;
}
else if (!(t.flags & TypeFlags.Object)) {
return false;
}
}
return seenPlaceholder;
}
return !!(type.flags & (TypeFlags.Any | TypeFlags.String | TypeFlags.Number | TypeFlags.BigInt)) || isPatternLiteralType(type);
}

function isPatternLiteralType(type: Type) {
// A pattern literal type is a template literal or a string mapping type that contains only
// non-generic pattern literal placeholders.
return !!(type.flags & TypeFlags.TemplateLiteral) && every((type as TemplateLiteralType).types, isPatternLiteralPlaceholderType) ||
!!(type.flags & TypeFlags.StringMapping) && isPatternLiteralPlaceholderType((type as StringMappingType).type);
}
Expand All @@ -18080,12 +18086,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

function getGenericObjectFlags(type: Type): ObjectFlags {
if (type.flags & (TypeFlags.UnionOrIntersection | TypeFlags.TemplateLiteral)) {
if (!((type as UnionOrIntersectionType | TemplateLiteralType).objectFlags & ObjectFlags.IsGenericTypeComputed)) {
(type as UnionOrIntersectionType | TemplateLiteralType).objectFlags |= ObjectFlags.IsGenericTypeComputed |
reduceLeft((type as UnionOrIntersectionType | TemplateLiteralType).types, (flags, t) => flags | getGenericObjectFlags(t), 0);
if (type.flags & (TypeFlags.UnionOrIntersection)) {
if (!((type as UnionOrIntersectionType).objectFlags & ObjectFlags.IsGenericTypeComputed)) {
(type as UnionOrIntersectionType).objectFlags |= ObjectFlags.IsGenericTypeComputed |
reduceLeft((type as UnionOrIntersectionType).types, (flags, t) => flags | getGenericObjectFlags(t), 0);
}
return (type as UnionOrIntersectionType | TemplateLiteralType).objectFlags & ObjectFlags.IsGenericType;
return (type as UnionOrIntersectionType).objectFlags & ObjectFlags.IsGenericType;
}
if (type.flags & TypeFlags.Substitution) {
if (!((type as SubstitutionType).objectFlags & ObjectFlags.IsGenericTypeComputed)) {
Expand All @@ -18095,7 +18101,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return (type as SubstitutionType).objectFlags & ObjectFlags.IsGenericType;
}
return (type.flags & TypeFlags.InstantiableNonPrimitive || isGenericMappedType(type) || isGenericTupleType(type) ? ObjectFlags.IsGenericObjectType : 0) |
(type.flags & (TypeFlags.InstantiableNonPrimitive | TypeFlags.Index | TypeFlags.StringMapping) && !isPatternLiteralType(type) ? ObjectFlags.IsGenericIndexType : 0);
(type.flags & (TypeFlags.InstantiableNonPrimitive | TypeFlags.Index | TypeFlags.TemplateLiteral | TypeFlags.StringMapping) && !isPatternLiteralType(type) ? ObjectFlags.IsGenericIndexType : 0);
}

function getSimplifiedType(type: Type, writing: boolean): Type {
Expand Down Expand Up @@ -24767,7 +24773,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
objectFlags & ObjectFlags.Anonymous && type.symbol && type.symbol.flags & (SymbolFlags.Function | SymbolFlags.Method | SymbolFlags.Class | SymbolFlags.TypeLiteral | SymbolFlags.ObjectLiteral) && type.symbol.declarations ||
objectFlags & (ObjectFlags.Mapped | ObjectFlags.ReverseMapped | ObjectFlags.ObjectRestType | ObjectFlags.InstantiationExpressionType)
) ||
type.flags & (TypeFlags.UnionOrIntersection | TypeFlags.TemplateLiteral) && !(type.flags & TypeFlags.EnumLiteral) && !isNonGenericTopLevelType(type) && some((type as UnionOrIntersectionType | TemplateLiteralType).types, couldContainTypeVariables));
type.flags & TypeFlags.UnionOrIntersection && !(type.flags & TypeFlags.EnumLiteral) && !isNonGenericTopLevelType(type) && some((type as UnionOrIntersectionType).types, couldContainTypeVariables));
if (type.flags & TypeFlags.ObjectFlagsType) {
(type as ObjectFlagsType).objectFlags |= ObjectFlags.CouldContainTypeVariablesComputed | (result ? ObjectFlags.CouldContainTypeVariables : 0);
}
Expand Down
8 changes: 3 additions & 5 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6130,7 +6130,7 @@ export const enum TypeFlags {
Instantiable = InstantiableNonPrimitive | InstantiablePrimitive,
StructuredOrInstantiable = StructuredType | Instantiable,
/** @internal */
ObjectFlagsType = Any | Nullable | Never | Object | Union | Intersection | TemplateLiteral,
ObjectFlagsType = Any | Nullable | Never | Object | Union | Intersection,
/** @internal */
Simplifiable = IndexedAccess | Conditional,
/** @internal */
Expand Down Expand Up @@ -6289,7 +6289,7 @@ export const enum ObjectFlags {
/** @internal */
IdenticalBaseTypeExists = 1 << 26, // has a defined cachedEquivalentBaseType member

// Flags that require TypeFlags.UnionOrIntersection, TypeFlags.Substitution, or TypeFlags.TemplateLiteral
// Flags that require TypeFlags.UnionOrIntersection or TypeFlags.Substitution
/** @internal */
IsGenericTypeComputed = 1 << 21, // IsGenericObjectType flag has been computed
/** @internal */
Expand All @@ -6316,7 +6316,7 @@ export const enum ObjectFlags {
}

/** @internal */
export type ObjectFlagsType = NullableType | ObjectType | UnionType | IntersectionType | TemplateLiteralType;
export type ObjectFlagsType = NullableType | ObjectType | UnionType | IntersectionType;

// Object types (TypeFlags.ObjectType)
// dprint-ignore
Expand Down Expand Up @@ -6675,8 +6675,6 @@ export interface ConditionalType extends InstantiableType {
}

export interface TemplateLiteralType extends InstantiableType {
/** @internal */
objectFlags: ObjectFlags;
texts: readonly string[]; // Always one element longer than types
types: readonly Type[]; // Always at least one element
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ templateLiteralTypesPatterns.ts(129,9): error TS2345: Argument of type '"1.1e-10
templateLiteralTypesPatterns.ts(140,1): error TS2322: Type '`a${string}`' is not assignable to type '`a${number}`'.
templateLiteralTypesPatterns.ts(141,1): error TS2322: Type '"bno"' is not assignable to type '`a${any}`'.
templateLiteralTypesPatterns.ts(160,7): error TS2322: Type '"anything"' is not assignable to type '`${number} ${number}`'.
templateLiteralTypesPatterns.ts(211,5): error TS2345: Argument of type '"abcTest"' is not assignable to parameter of type '`${`a${string}` & `${string}a`}Test`'.
templateLiteralTypesPatterns.ts(215,5): error TS2345: Argument of type '"abcTest"' is not assignable to parameter of type '`${`a${string}` & `${string}a`}Test`'.


==== templateLiteralTypesPatterns.ts (58 errors) ====
Expand Down Expand Up @@ -376,10 +376,14 @@ templateLiteralTypesPatterns.ts(211,5): error TS2345: Argument of type '"abcTest
}

// repro from https://github.com/microsoft/TypeScript/issues/54177#issuecomment-1538436654
function conversionTest(groupName: | "downcast" | "dataDowncast" | "editingDowncast" | `${string & {}}Downcast`) {}
function conversionTest(groupName: | "downcast" | "dataDowncast" | "editingDowncast" | `${string}Downcast` & {}) {}
conversionTest("testDowncast");
function conversionTest2(groupName: | "downcast" | "dataDowncast" | "editingDowncast" | `${{} & string}Downcast`) {}
function conversionTest2(groupName: | "downcast" | "dataDowncast" | "editingDowncast" | {} & `${string}Downcast`) {}
conversionTest2("testDowncast");
function conversionTest3(groupName: | "downcast" | "dataDowncast" | "editingDowncast" | `${string & {}}Downcast`) {}
conversionTest3("testDowncast");
function conversionTest4(groupName: | "downcast" | "dataDowncast" | "editingDowncast" | `${{} & string}Downcast`) {}
conversionTest4("testDowncast");

function foo(str: `${`a${string}` & `${string}a`}Test`) {}
foo("abaTest"); // ok
Expand Down
12 changes: 10 additions & 2 deletions tests/baselines/reference/templateLiteralTypesPatterns.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,10 +204,14 @@ export abstract class BB {
}

// repro from https://github.com/microsoft/TypeScript/issues/54177#issuecomment-1538436654
function conversionTest(groupName: | "downcast" | "dataDowncast" | "editingDowncast" | `${string & {}}Downcast`) {}
function conversionTest(groupName: | "downcast" | "dataDowncast" | "editingDowncast" | `${string}Downcast` & {}) {}
conversionTest("testDowncast");
function conversionTest2(groupName: | "downcast" | "dataDowncast" | "editingDowncast" | `${{} & string}Downcast`) {}
function conversionTest2(groupName: | "downcast" | "dataDowncast" | "editingDowncast" | {} & `${string}Downcast`) {}
conversionTest2("testDowncast");
function conversionTest3(groupName: | "downcast" | "dataDowncast" | "editingDowncast" | `${string & {}}Downcast`) {}
conversionTest3("testDowncast");
function conversionTest4(groupName: | "downcast" | "dataDowncast" | "editingDowncast" | `${{} & string}Downcast`) {}
conversionTest4("testDowncast");

function foo(str: `${`a${string}` & `${string}a`}Test`) {}
foo("abaTest"); // ok
Expand Down Expand Up @@ -367,6 +371,10 @@ function conversionTest(groupName) { }
conversionTest("testDowncast");
function conversionTest2(groupName) { }
conversionTest2("testDowncast");
function conversionTest3(groupName) { }
conversionTest3("testDowncast");
function conversionTest4(groupName) { }
conversionTest4("testDowncast");
function foo(str) { }
foo("abaTest"); // ok
foo("abcTest"); // error
26 changes: 20 additions & 6 deletions tests/baselines/reference/templateLiteralTypesPatterns.symbols
Original file line number Diff line number Diff line change
Expand Up @@ -488,27 +488,41 @@ export abstract class BB {
}

// repro from https://github.com/microsoft/TypeScript/issues/54177#issuecomment-1538436654
function conversionTest(groupName: | "downcast" | "dataDowncast" | "editingDowncast" | `${string & {}}Downcast`) {}
function conversionTest(groupName: | "downcast" | "dataDowncast" | "editingDowncast" | `${string}Downcast` & {}) {}
>conversionTest : Symbol(conversionTest, Decl(templateLiteralTypesPatterns.ts, 200, 1))
>groupName : Symbol(groupName, Decl(templateLiteralTypesPatterns.ts, 203, 24))

conversionTest("testDowncast");
>conversionTest : Symbol(conversionTest, Decl(templateLiteralTypesPatterns.ts, 200, 1))

function conversionTest2(groupName: | "downcast" | "dataDowncast" | "editingDowncast" | `${{} & string}Downcast`) {}
function conversionTest2(groupName: | "downcast" | "dataDowncast" | "editingDowncast" | {} & `${string}Downcast`) {}
>conversionTest2 : Symbol(conversionTest2, Decl(templateLiteralTypesPatterns.ts, 204, 31))
>groupName : Symbol(groupName, Decl(templateLiteralTypesPatterns.ts, 205, 25))

conversionTest2("testDowncast");
>conversionTest2 : Symbol(conversionTest2, Decl(templateLiteralTypesPatterns.ts, 204, 31))

function conversionTest3(groupName: | "downcast" | "dataDowncast" | "editingDowncast" | `${string & {}}Downcast`) {}
>conversionTest3 : Symbol(conversionTest3, Decl(templateLiteralTypesPatterns.ts, 206, 32))
>groupName : Symbol(groupName, Decl(templateLiteralTypesPatterns.ts, 207, 25))

conversionTest3("testDowncast");
>conversionTest3 : Symbol(conversionTest3, Decl(templateLiteralTypesPatterns.ts, 206, 32))

function conversionTest4(groupName: | "downcast" | "dataDowncast" | "editingDowncast" | `${{} & string}Downcast`) {}
>conversionTest4 : Symbol(conversionTest4, Decl(templateLiteralTypesPatterns.ts, 208, 32))
>groupName : Symbol(groupName, Decl(templateLiteralTypesPatterns.ts, 209, 25))

conversionTest4("testDowncast");
>conversionTest4 : Symbol(conversionTest4, Decl(templateLiteralTypesPatterns.ts, 208, 32))

function foo(str: `${`a${string}` & `${string}a`}Test`) {}
>foo : Symbol(foo, Decl(templateLiteralTypesPatterns.ts, 206, 32))
>str : Symbol(str, Decl(templateLiteralTypesPatterns.ts, 208, 13))
>foo : Symbol(foo, Decl(templateLiteralTypesPatterns.ts, 210, 32))
>str : Symbol(str, Decl(templateLiteralTypesPatterns.ts, 212, 13))

foo("abaTest"); // ok
>foo : Symbol(foo, Decl(templateLiteralTypesPatterns.ts, 206, 32))
>foo : Symbol(foo, Decl(templateLiteralTypesPatterns.ts, 210, 32))

foo("abcTest"); // error
>foo : Symbol(foo, Decl(templateLiteralTypesPatterns.ts, 206, 32))
>foo : Symbol(foo, Decl(templateLiteralTypesPatterns.ts, 210, 32))

34 changes: 26 additions & 8 deletions tests/baselines/reference/templateLiteralTypesPatterns.types
Original file line number Diff line number Diff line change
Expand Up @@ -636,22 +636,40 @@ export abstract class BB {
}

// repro from https://github.com/microsoft/TypeScript/issues/54177#issuecomment-1538436654
function conversionTest(groupName: | "downcast" | "dataDowncast" | "editingDowncast" | `${string & {}}Downcast`) {}
>conversionTest : (groupName: "downcast" | "dataDowncast" | "editingDowncast" | `${string & {}}Downcast`) => void
>groupName : `${string & {}}Downcast` | "downcast" | "dataDowncast" | "editingDowncast"
function conversionTest(groupName: | "downcast" | "dataDowncast" | "editingDowncast" | `${string}Downcast` & {}) {}
>conversionTest : (groupName: "downcast" | "dataDowncast" | "editingDowncast" | `${string}Downcast` & {}) => void
>groupName : (`${string}Downcast` & {}) | "downcast" | "dataDowncast" | "editingDowncast"

conversionTest("testDowncast");
>conversionTest("testDowncast") : void
>conversionTest : (groupName: `${string & {}}Downcast` | "downcast" | "dataDowncast" | "editingDowncast") => void
>conversionTest : (groupName: (`${string}Downcast` & {}) | "downcast" | "dataDowncast" | "editingDowncast") => void
>"testDowncast" : "testDowncast"

function conversionTest2(groupName: | "downcast" | "dataDowncast" | "editingDowncast" | `${{} & string}Downcast`) {}
>conversionTest2 : (groupName: "downcast" | "dataDowncast" | "editingDowncast" | `${{} & string}Downcast`) => void
>groupName : "downcast" | "dataDowncast" | "editingDowncast" | `${{} & string}Downcast`
function conversionTest2(groupName: | "downcast" | "dataDowncast" | "editingDowncast" | {} & `${string}Downcast`) {}
>conversionTest2 : (groupName: "downcast" | "dataDowncast" | "editingDowncast" | {} & `${string}Downcast`) => void
>groupName : "downcast" | "dataDowncast" | "editingDowncast" | ({} & `${string}Downcast`)

conversionTest2("testDowncast");
>conversionTest2("testDowncast") : void
>conversionTest2 : (groupName: "downcast" | "dataDowncast" | "editingDowncast" | `${{} & string}Downcast`) => void
>conversionTest2 : (groupName: "downcast" | "dataDowncast" | "editingDowncast" | ({} & `${string}Downcast`)) => void
>"testDowncast" : "testDowncast"

function conversionTest3(groupName: | "downcast" | "dataDowncast" | "editingDowncast" | `${string & {}}Downcast`) {}
>conversionTest3 : (groupName: "downcast" | "dataDowncast" | "editingDowncast" | `${string & {}}Downcast`) => void
>groupName : "downcast" | `${string & {}}Downcast`

conversionTest3("testDowncast");
>conversionTest3("testDowncast") : void
>conversionTest3 : (groupName: "downcast" | `${string & {}}Downcast`) => void
>"testDowncast" : "testDowncast"

function conversionTest4(groupName: | "downcast" | "dataDowncast" | "editingDowncast" | `${{} & string}Downcast`) {}
>conversionTest4 : (groupName: "downcast" | "dataDowncast" | "editingDowncast" | `${{} & string}Downcast`) => void
>groupName : "downcast" | `${{} & string}Downcast`

conversionTest4("testDowncast");
>conversionTest4("testDowncast") : void
>conversionTest4 : (groupName: "downcast" | `${{} & string}Downcast`) => void
>"testDowncast" : "testDowncast"

function foo(str: `${`a${string}` & `${string}a`}Test`) {}
Expand Down

0 comments on commit c266e47

Please sign in to comment.