From 2877ff5b445591761f2ff37663d577d4e377d599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Mon, 8 May 2023 23:25:08 +0200 Subject: [PATCH] Allow intersections to be used as valid types for template literal placeholders --- src/compiler/checker.ts | 16 ++++++--- .../templateLiteralTypesPatterns.errors.txt | 16 +++++++-- .../reference/templateLiteralTypesPatterns.js | 19 ++++++++++- .../templateLiteralTypesPatterns.symbols | 25 ++++++++++++++ .../templateLiteralTypesPatterns.types | 33 +++++++++++++++++++ .../literal/templateLiteralTypesPatterns.ts | 10 ++++++ 6 files changed, 112 insertions(+), 7 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 98cd87923761e..74dd774bb942d 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -16918,8 +16918,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return reduceLeft(types, (n, t) => n + getConstituentCount(t), 0); } - function areIntersectedTypesAvoidingPrimitiveReduction(t1: Type, t2: Type) { - return !!(t1.flags & (TypeFlags.String | TypeFlags.Number | TypeFlags.BigInt)) && t2 === emptyTypeLiteralType; + 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 { @@ -16927,7 +16931,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { if (!links.resolvedType) { const aliasSymbol = getAliasSymbolForTypeNode(node); const types = map(node.types, getTypeFromTypeNode); - const noSupertypeReduction = types.length === 2 && (areIntersectedTypesAvoidingPrimitiveReduction(types[0], types[1]) || areIntersectedTypesAvoidingPrimitiveReduction(types[1], types[0])); + const noSupertypeReduction = areIntersectedTypesAvoidingPrimitiveReduction(types); links.resolvedType = getIntersectionType(types, aliasSymbol, getTypeArgumentsForAliasSymbol(aliasSymbol), noSupertypeReduction); } return links.resolvedType; @@ -24270,12 +24274,16 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { if (source === target || target.flags & (TypeFlags.Any | TypeFlags.String)) { return true; } + if (target.flags & TypeFlags.Intersection) { + return every((target as IntersectionType).types, t => t === emptyTypeLiteralType || isValidTypeForTemplateLiteralPlaceholder(source, t)); + } if (source.flags & TypeFlags.StringLiteral) { const value = (source as StringLiteralType).value; return !!(target.flags & TypeFlags.Number && isValidNumberString(value, /*roundTripOnly*/ false) || target.flags & TypeFlags.BigInt && isValidBigIntString(value, /*roundTripOnly*/ false) || target.flags & (TypeFlags.BooleanLiteral | TypeFlags.Nullable) && value === (target as IntrinsicType).intrinsicName || - target.flags & TypeFlags.StringMapping && isMemberOfStringMapping(getStringLiteralType(value), target)); + target.flags & TypeFlags.StringMapping && isMemberOfStringMapping(getStringLiteralType(value), target) || + target.flags & TypeFlags.TemplateLiteral && isTypeMatchedByTemplateLiteralType(source, target as TemplateLiteralType)); } if (source.flags & TypeFlags.TemplateLiteral) { const texts = (source as TemplateLiteralType).texts; diff --git a/tests/baselines/reference/templateLiteralTypesPatterns.errors.txt b/tests/baselines/reference/templateLiteralTypesPatterns.errors.txt index ee8de62921852..aee86aa8b5444 100644 --- a/tests/baselines/reference/templateLiteralTypesPatterns.errors.txt +++ b/tests/baselines/reference/templateLiteralTypesPatterns.errors.txt @@ -55,9 +55,10 @@ tests/cases/conformance/types/literal/templateLiteralTypesPatterns.ts(129,9): er tests/cases/conformance/types/literal/templateLiteralTypesPatterns.ts(140,1): error TS2322: Type '`a${string}`' is not assignable to type '`a${number}`'. tests/cases/conformance/types/literal/templateLiteralTypesPatterns.ts(141,1): error TS2322: Type '"bno"' is not assignable to type '`a${any}`'. tests/cases/conformance/types/literal/templateLiteralTypesPatterns.ts(160,7): error TS2322: Type '"anything"' is not assignable to type '`${number} ${number}`'. +tests/cases/conformance/types/literal/templateLiteralTypesPatterns.ts(211,5): error TS2345: Argument of type '"abcTest"' is not assignable to parameter of type '`${`a${string}` & `${string}a`}Test`'. -==== tests/cases/conformance/types/literal/templateLiteralTypesPatterns.ts (57 errors) ==== +==== tests/cases/conformance/types/literal/templateLiteralTypesPatterns.ts (58 errors) ==== type RequiresLeadingSlash = `/${string}`; // ok @@ -373,4 +374,15 @@ tests/cases/conformance/types/literal/templateLiteralTypesPatterns.ts(160,7): er this.get(id!); } } - \ No newline at end of file + + // repro from https://github.com/microsoft/TypeScript/issues/54177#issuecomment-1538436654 + function conversionTest(groupName: | "downcast" | "dataDowncast" | "editingDowncast" | `${string & {}}Downcast`) {} + conversionTest("testDowncast"); + function conversionTest2(groupName: | "downcast" | "dataDowncast" | "editingDowncast" | `${{} & string}Downcast`) {} + conversionTest2("testDowncast"); + + function foo(str: `${`a${string}` & `${string}a`}Test`) {} + foo("abaTest"); // ok + foo("abcTest"); // error + ~~~~~~~~~ +!!! error TS2345: Argument of type '"abcTest"' is not assignable to parameter of type '`${`a${string}` & `${string}a`}Test`'. \ No newline at end of file diff --git a/tests/baselines/reference/templateLiteralTypesPatterns.js b/tests/baselines/reference/templateLiteralTypesPatterns.js index c9ffea0e18246..8cb0b36e5c80a 100644 --- a/tests/baselines/reference/templateLiteralTypesPatterns.js +++ b/tests/baselines/reference/templateLiteralTypesPatterns.js @@ -200,7 +200,16 @@ export abstract class BB { this.get(id!); } } - + +// repro from https://github.com/microsoft/TypeScript/issues/54177#issuecomment-1538436654 +function conversionTest(groupName: | "downcast" | "dataDowncast" | "editingDowncast" | `${string & {}}Downcast`) {} +conversionTest("testDowncast"); +function conversionTest2(groupName: | "downcast" | "dataDowncast" | "editingDowncast" | `${{} & string}Downcast`) {} +conversionTest2("testDowncast"); + +function foo(str: `${`a${string}` & `${string}a`}Test`) {} +foo("abaTest"); // ok +foo("abcTest"); // error //// [templateLiteralTypesPatterns.js] "use strict"; @@ -351,3 +360,11 @@ var BB = /** @class */ (function () { return BB; }()); exports.BB = BB; +// repro from https://github.com/microsoft/TypeScript/issues/54177#issuecomment-1538436654 +function conversionTest(groupName) { } +conversionTest("testDowncast"); +function conversionTest2(groupName) { } +conversionTest2("testDowncast"); +function foo(str) { } +foo("abaTest"); // ok +foo("abcTest"); // error diff --git a/tests/baselines/reference/templateLiteralTypesPatterns.symbols b/tests/baselines/reference/templateLiteralTypesPatterns.symbols index 95d25249747b3..7431ae5bdd4f1 100644 --- a/tests/baselines/reference/templateLiteralTypesPatterns.symbols +++ b/tests/baselines/reference/templateLiteralTypesPatterns.symbols @@ -485,3 +485,28 @@ export abstract class BB { } } +// repro from https://github.com/microsoft/TypeScript/issues/54177#issuecomment-1538436654 +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`) {} +>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 foo(str: `${`a${string}` & `${string}a`}Test`) {} +>foo : Symbol(foo, Decl(templateLiteralTypesPatterns.ts, 206, 32)) +>str : Symbol(str, Decl(templateLiteralTypesPatterns.ts, 208, 13)) + +foo("abaTest"); // ok +>foo : Symbol(foo, Decl(templateLiteralTypesPatterns.ts, 206, 32)) + +foo("abcTest"); // error +>foo : Symbol(foo, Decl(templateLiteralTypesPatterns.ts, 206, 32)) + diff --git a/tests/baselines/reference/templateLiteralTypesPatterns.types b/tests/baselines/reference/templateLiteralTypesPatterns.types index 19bae632fd195..3741e2265e5f8 100644 --- a/tests/baselines/reference/templateLiteralTypesPatterns.types +++ b/tests/baselines/reference/templateLiteralTypesPatterns.types @@ -633,3 +633,36 @@ 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" + +conversionTest("testDowncast"); +>conversionTest("testDowncast") : void +>conversionTest : (groupName: `${string & {}}Downcast` | "downcast") => void +>"testDowncast" : "testDowncast" + +function conversionTest2(groupName: | "downcast" | "dataDowncast" | "editingDowncast" | `${{} & string}Downcast`) {} +>conversionTest2 : (groupName: "downcast" | "dataDowncast" | "editingDowncast" | `${{} & string}Downcast`) => void +>groupName : "downcast" | `${{} & string}Downcast` + +conversionTest2("testDowncast"); +>conversionTest2("testDowncast") : void +>conversionTest2 : (groupName: "downcast" | `${{} & string}Downcast`) => void +>"testDowncast" : "testDowncast" + +function foo(str: `${`a${string}` & `${string}a`}Test`) {} +>foo : (str: `${`a${string}` & `${string}a`}Test`) => void +>str : `${`a${string}` & `${string}a`}Test` + +foo("abaTest"); // ok +>foo("abaTest") : void +>foo : (str: `${`a${string}` & `${string}a`}Test`) => void +>"abaTest" : "abaTest" + +foo("abcTest"); // error +>foo("abcTest") : void +>foo : (str: `${`a${string}` & `${string}a`}Test`) => void +>"abcTest" : "abcTest" + diff --git a/tests/cases/conformance/types/literal/templateLiteralTypesPatterns.ts b/tests/cases/conformance/types/literal/templateLiteralTypesPatterns.ts index a98facd4359a3..aeb05ead7ccca 100644 --- a/tests/cases/conformance/types/literal/templateLiteralTypesPatterns.ts +++ b/tests/cases/conformance/types/literal/templateLiteralTypesPatterns.ts @@ -200,3 +200,13 @@ export abstract class BB { this.get(id!); } } + +// repro from https://github.com/microsoft/TypeScript/issues/54177#issuecomment-1538436654 +function conversionTest(groupName: | "downcast" | "dataDowncast" | "editingDowncast" | `${string & {}}Downcast`) {} +conversionTest("testDowncast"); +function conversionTest2(groupName: | "downcast" | "dataDowncast" | "editingDowncast" | `${{} & string}Downcast`) {} +conversionTest2("testDowncast"); + +function foo(str: `${`a${string}` & `${string}a`}Test`) {} +foo("abaTest"); // ok +foo("abcTest"); // error \ No newline at end of file