diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 364f0de7f642f..40fd8c8d56df6 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -12029,7 +12029,7 @@ namespace ts { else if (type !== firstType) { checkFlags |= CheckFlags.HasNonUniformType; } - if (isLiteralType(type)) { + if (isLiteralType(type) || isPatternLiteralType(type)) { checkFlags |= CheckFlags.HasLiteralType; } if (type.flags & TypeFlags.Never) { @@ -18935,6 +18935,9 @@ namespace ts { } else if (target.flags & TypeFlags.TemplateLiteral) { if (source.flags & TypeFlags.TemplateLiteral) { + if (relation === comparableRelation) { + return templateLiteralTypesDefinitelyUnrelated(source as TemplateLiteralType, target as TemplateLiteralType) ? Ternary.False : Ternary.True; + } // Report unreliable variance for type variables referenced in template literal type placeholders. // For example, `foo-${number}` is related to `foo-${string}` even though number isn't related to string. instantiateType(source, makeFunctionTypeMapper(reportUnreliableMarkers)); @@ -18974,11 +18977,10 @@ namespace ts { return result; } } - else if (source.flags & TypeFlags.TemplateLiteral) { + else if (source.flags & TypeFlags.TemplateLiteral && !(target.flags & TypeFlags.Object)) { if (!(target.flags & TypeFlags.TemplateLiteral)) { - const baseConstraint = getBaseConstraintOfType(source); - const constraint = baseConstraint && baseConstraint !== source ? baseConstraint : stringType; - if (result = isRelatedTo(constraint, target, reportErrors)) { + const constraint = getBaseConstraintOfType(source); + if (constraint && constraint !== source && (result = isRelatedTo(constraint, target, reportErrors))) { resetErrorInfo(saveErrorInfo); return result; } @@ -21349,6 +21351,18 @@ namespace ts { return !!(type.symbol && some(type.symbol.declarations, hasSkipDirectInferenceFlag)); } + function templateLiteralTypesDefinitelyUnrelated(source: TemplateLiteralType, target: TemplateLiteralType) { + // Two template literal types with diffences in their starting or ending text spans are definitely unrelated. + const sourceStart = source.texts[0]; + const targetStart = target.texts[0]; + const sourceEnd = source.texts[source.texts.length - 1]; + const targetEnd = target.texts[target.texts.length - 1]; + const startLen = Math.min(sourceStart.length, targetStart.length); + const endLen = Math.min(sourceEnd.length, targetEnd.length); + return sourceStart.slice(0, startLen) !== targetStart.slice(0, startLen) || + sourceEnd.slice(sourceEnd.length - endLen) !== targetEnd.slice(targetEnd.length - endLen); + } + function isValidBigIntString(s: string): boolean { const scanner = createScanner(ScriptTarget.ESNext, /*skipTrivia*/ false); let success = true; @@ -22449,7 +22463,7 @@ namespace ts { if ((prop as TransientSymbol).isDiscriminantProperty === undefined) { (prop as TransientSymbol).isDiscriminantProperty = ((prop as TransientSymbol).checkFlags & CheckFlags.Discriminant) === CheckFlags.Discriminant && - !maybeTypeOfKind(getTypeOfSymbol(prop), TypeFlags.Instantiable & ~TypeFlags.TemplateLiteral); + !isGenericType(getTypeOfSymbol(prop)); } return !!(prop as TransientSymbol).isDiscriminantProperty; } @@ -23008,15 +23022,17 @@ namespace ts { return filterType(type, t => (t.flags & kind) !== 0); } - // Return a new type in which occurrences of the string and number primitive types in - // typeWithPrimitives have been replaced with occurrences of string literals and numeric - // literals in typeWithLiterals, respectively. + // Return a new type in which occurrences of the string, number and bigint primitives and placeholder template + // literal types in typeWithPrimitives have been replaced with occurrences of compatible and more specific types + // from typeWithLiterals. This is essentially a limited form of intersection between the two types. We avoid a + // true intersection because it is more costly and, when applied to union types, generates a large number of + // types we don't actually care about. function replacePrimitivesWithLiterals(typeWithPrimitives: Type, typeWithLiterals: Type) { - if (isTypeSubsetOf(stringType, typeWithPrimitives) && maybeTypeOfKind(typeWithLiterals, TypeFlags.StringLiteral) || - isTypeSubsetOf(numberType, typeWithPrimitives) && maybeTypeOfKind(typeWithLiterals, TypeFlags.NumberLiteral) || - isTypeSubsetOf(bigintType, typeWithPrimitives) && maybeTypeOfKind(typeWithLiterals, TypeFlags.BigIntLiteral)) { + if (maybeTypeOfKind(typeWithPrimitives, TypeFlags.String | TypeFlags.TemplateLiteral | TypeFlags.Number | TypeFlags.BigInt) && + maybeTypeOfKind(typeWithLiterals, TypeFlags.StringLiteral | TypeFlags.TemplateLiteral | TypeFlags.StringMapping | TypeFlags.NumberLiteral | TypeFlags.BigIntLiteral)) { return mapType(typeWithPrimitives, t => - t.flags & TypeFlags.String ? extractTypesOfKind(typeWithLiterals, TypeFlags.String | TypeFlags.StringLiteral) : + t.flags & TypeFlags.String ? extractTypesOfKind(typeWithLiterals, TypeFlags.String | TypeFlags.StringLiteral | TypeFlags.TemplateLiteral | TypeFlags.StringMapping) : + isPatternLiteralType(t) && !maybeTypeOfKind(typeWithLiterals, TypeFlags.String | TypeFlags.TemplateLiteral | TypeFlags.StringMapping) ? extractTypesOfKind(typeWithLiterals, TypeFlags.StringLiteral) : t.flags & TypeFlags.Number ? extractTypesOfKind(typeWithLiterals, TypeFlags.Number | TypeFlags.NumberLiteral) : t.flags & TypeFlags.BigInt ? extractTypesOfKind(typeWithLiterals, TypeFlags.BigInt | TypeFlags.BigIntLiteral) : t); } @@ -23859,7 +23875,7 @@ namespace ts { const narrowedPropType = narrowType(propType); return filterType(type, t => { const discriminantType = getTypeOfPropertyOrIndexSignature(t, propName); - return !(discriminantType.flags & TypeFlags.Never) && isTypeComparableTo(discriminantType, narrowedPropType); + return !(narrowedPropType.flags & TypeFlags.Never) && isTypeComparableTo(narrowedPropType, discriminantType); }); } diff --git a/tests/baselines/reference/templateLiteralTypes3.errors.txt b/tests/baselines/reference/templateLiteralTypes3.errors.txt index 81865b5dba5dc..4c6e98d93653a 100644 --- a/tests/baselines/reference/templateLiteralTypes3.errors.txt +++ b/tests/baselines/reference/templateLiteralTypes3.errors.txt @@ -5,9 +5,11 @@ tests/cases/conformance/types/literal/templateLiteralTypes3.ts(71,5): error TS23 tests/cases/conformance/types/literal/templateLiteralTypes3.ts(72,5): error TS2322: Type '`*${string}*`' is not assignable to type '`*${number}*`'. tests/cases/conformance/types/literal/templateLiteralTypes3.ts(74,5): error TS2322: Type '"*false*" | "*true*"' is not assignable to type '`*${number}*`'. Type '"*false*"' is not assignable to type '`*${number}*`'. +tests/cases/conformance/types/literal/templateLiteralTypes3.ts(133,9): error TS2367: This condition will always return 'false' since the types '`foo-${string}`' and '`baz-${string}`' have no overlap. +tests/cases/conformance/types/literal/templateLiteralTypes3.ts(141,9): error TS2367: This condition will always return 'false' since the types '`foo-${T}`' and '`baz-${T}`' have no overlap. -==== tests/cases/conformance/types/literal/templateLiteralTypes3.ts (6 errors) ==== +==== tests/cases/conformance/types/literal/templateLiteralTypes3.ts (8 errors) ==== // Inference from template literal type to template literal type type Foo1 = T extends `*${infer U}*` ? U : never; @@ -146,4 +148,54 @@ tests/cases/conformance/types/literal/templateLiteralTypes3.ts(74,5): error TS23 declare function chain(field: F | `${F}.${F}`): void; chain("a"); + + // Repro from #46125 + + function ff1(x: `foo-${string}`, y: `${string}-bar`, z: `baz-${string}`) { + if (x === y) { + x; // `foo-${string}` + } + if (x === z) { // Error + ~~~~~~~ +!!! error TS2367: This condition will always return 'false' since the types '`foo-${string}`' and '`baz-${string}`' have no overlap. + } + } + + function ff2(x: `foo-${T}`, y: `${T}-bar`, z: `baz-${T}`) { + if (x === y) { + x; // `foo-${T}` + } + if (x === z) { // Error + ~~~~~~~ +!!! error TS2367: This condition will always return 'false' since the types '`foo-${T}`' and '`baz-${T}`' have no overlap. + } + } + + function ff3(x: string, y: `foo-${string}` | 'bar') { + if (x === y) { + x; // `foo-${string}` | 'bar' + } + } + + function ff4(x: string, y: `foo-${string}`) { + if (x === 'foo-test') { + x; // 'foo-test' + } + if (y === 'foo-test') { + y; // 'foo-test' + } + } + + // Repro from #46045 + + type Action = + | { type: `${string}_REQUEST` } + | { type: `${string}_SUCCESS`, response: string }; + + function reducer(action: Action) { + if (action.type === 'FOO_SUCCESS') { + action.type; + action.response; + } + } \ No newline at end of file diff --git a/tests/baselines/reference/templateLiteralTypes3.js b/tests/baselines/reference/templateLiteralTypes3.js index 07a3e669db098..7122f13da6dad 100644 --- a/tests/baselines/reference/templateLiteralTypes3.js +++ b/tests/baselines/reference/templateLiteralTypes3.js @@ -124,6 +124,52 @@ type Schema = { a: { b: { c: number } } }; declare function chain(field: F | `${F}.${F}`): void; chain("a"); + +// Repro from #46125 + +function ff1(x: `foo-${string}`, y: `${string}-bar`, z: `baz-${string}`) { + if (x === y) { + x; // `foo-${string}` + } + if (x === z) { // Error + } +} + +function ff2(x: `foo-${T}`, y: `${T}-bar`, z: `baz-${T}`) { + if (x === y) { + x; // `foo-${T}` + } + if (x === z) { // Error + } +} + +function ff3(x: string, y: `foo-${string}` | 'bar') { + if (x === y) { + x; // `foo-${string}` | 'bar' + } +} + +function ff4(x: string, y: `foo-${string}`) { + if (x === 'foo-test') { + x; // 'foo-test' + } + if (y === 'foo-test') { + y; // 'foo-test' + } +} + +// Repro from #46045 + +type Action = + | { type: `${string}_REQUEST` } + | { type: `${string}_SUCCESS`, response: string }; + +function reducer(action: Action) { + if (action.type === 'FOO_SUCCESS') { + action.type; + action.response; + } +} //// [templateLiteralTypes3.js] @@ -177,6 +223,40 @@ var templated1 = value1 + " abc"; var value2 = "abc"; var templated2 = value2 + " abc"; chain("a"); +// Repro from #46125 +function ff1(x, y, z) { + if (x === y) { + x; // `foo-${string}` + } + if (x === z) { // Error + } +} +function ff2(x, y, z) { + if (x === y) { + x; // `foo-${T}` + } + if (x === z) { // Error + } +} +function ff3(x, y) { + if (x === y) { + x; // `foo-${string}` | 'bar' + } +} +function ff4(x, y) { + if (x === 'foo-test') { + x; // 'foo-test' + } + if (y === 'foo-test') { + y; // 'foo-test' + } +} +function reducer(action) { + if (action.type === 'FOO_SUCCESS') { + action.type; + action.response; + } +} //// [templateLiteralTypes3.d.ts] @@ -233,3 +313,14 @@ declare type Schema = { }; }; declare function chain(field: F | `${F}.${F}`): void; +declare function ff1(x: `foo-${string}`, y: `${string}-bar`, z: `baz-${string}`): void; +declare function ff2(x: `foo-${T}`, y: `${T}-bar`, z: `baz-${T}`): void; +declare function ff3(x: string, y: `foo-${string}` | 'bar'): void; +declare function ff4(x: string, y: `foo-${string}`): void; +declare type Action = { + type: `${string}_REQUEST`; +} | { + type: `${string}_SUCCESS`; + response: string; +}; +declare function reducer(action: Action): void; diff --git a/tests/baselines/reference/templateLiteralTypes3.symbols b/tests/baselines/reference/templateLiteralTypes3.symbols index 509a9a36c373e..570a85a809dd8 100644 --- a/tests/baselines/reference/templateLiteralTypes3.symbols +++ b/tests/baselines/reference/templateLiteralTypes3.symbols @@ -405,3 +405,114 @@ declare function chain(field: F | `${F}.${F}`): void; chain("a"); >chain : Symbol(chain, Decl(templateLiteralTypes3.ts, 120, 42)) +// Repro from #46125 + +function ff1(x: `foo-${string}`, y: `${string}-bar`, z: `baz-${string}`) { +>ff1 : Symbol(ff1, Decl(templateLiteralTypes3.ts, 124, 11)) +>x : Symbol(x, Decl(templateLiteralTypes3.ts, 128, 13)) +>y : Symbol(y, Decl(templateLiteralTypes3.ts, 128, 32)) +>z : Symbol(z, Decl(templateLiteralTypes3.ts, 128, 52)) + + if (x === y) { +>x : Symbol(x, Decl(templateLiteralTypes3.ts, 128, 13)) +>y : Symbol(y, Decl(templateLiteralTypes3.ts, 128, 32)) + + x; // `foo-${string}` +>x : Symbol(x, Decl(templateLiteralTypes3.ts, 128, 13)) + } + if (x === z) { // Error +>x : Symbol(x, Decl(templateLiteralTypes3.ts, 128, 13)) +>z : Symbol(z, Decl(templateLiteralTypes3.ts, 128, 52)) + } +} + +function ff2(x: `foo-${T}`, y: `${T}-bar`, z: `baz-${T}`) { +>ff2 : Symbol(ff2, Decl(templateLiteralTypes3.ts, 134, 1)) +>T : Symbol(T, Decl(templateLiteralTypes3.ts, 136, 13)) +>x : Symbol(x, Decl(templateLiteralTypes3.ts, 136, 31)) +>T : Symbol(T, Decl(templateLiteralTypes3.ts, 136, 13)) +>y : Symbol(y, Decl(templateLiteralTypes3.ts, 136, 45)) +>T : Symbol(T, Decl(templateLiteralTypes3.ts, 136, 13)) +>z : Symbol(z, Decl(templateLiteralTypes3.ts, 136, 60)) +>T : Symbol(T, Decl(templateLiteralTypes3.ts, 136, 13)) + + if (x === y) { +>x : Symbol(x, Decl(templateLiteralTypes3.ts, 136, 31)) +>y : Symbol(y, Decl(templateLiteralTypes3.ts, 136, 45)) + + x; // `foo-${T}` +>x : Symbol(x, Decl(templateLiteralTypes3.ts, 136, 31)) + } + if (x === z) { // Error +>x : Symbol(x, Decl(templateLiteralTypes3.ts, 136, 31)) +>z : Symbol(z, Decl(templateLiteralTypes3.ts, 136, 60)) + } +} + +function ff3(x: string, y: `foo-${string}` | 'bar') { +>ff3 : Symbol(ff3, Decl(templateLiteralTypes3.ts, 142, 1)) +>x : Symbol(x, Decl(templateLiteralTypes3.ts, 144, 13)) +>y : Symbol(y, Decl(templateLiteralTypes3.ts, 144, 23)) + + if (x === y) { +>x : Symbol(x, Decl(templateLiteralTypes3.ts, 144, 13)) +>y : Symbol(y, Decl(templateLiteralTypes3.ts, 144, 23)) + + x; // `foo-${string}` | 'bar' +>x : Symbol(x, Decl(templateLiteralTypes3.ts, 144, 13)) + } +} + +function ff4(x: string, y: `foo-${string}`) { +>ff4 : Symbol(ff4, Decl(templateLiteralTypes3.ts, 148, 1)) +>x : Symbol(x, Decl(templateLiteralTypes3.ts, 150, 13)) +>y : Symbol(y, Decl(templateLiteralTypes3.ts, 150, 23)) + + if (x === 'foo-test') { +>x : Symbol(x, Decl(templateLiteralTypes3.ts, 150, 13)) + + x; // 'foo-test' +>x : Symbol(x, Decl(templateLiteralTypes3.ts, 150, 13)) + } + if (y === 'foo-test') { +>y : Symbol(y, Decl(templateLiteralTypes3.ts, 150, 23)) + + y; // 'foo-test' +>y : Symbol(y, Decl(templateLiteralTypes3.ts, 150, 23)) + } +} + +// Repro from #46045 + +type Action = +>Action : Symbol(Action, Decl(templateLiteralTypes3.ts, 157, 1)) + + | { type: `${string}_REQUEST` } +>type : Symbol(type, Decl(templateLiteralTypes3.ts, 162, 7)) + + | { type: `${string}_SUCCESS`, response: string }; +>type : Symbol(type, Decl(templateLiteralTypes3.ts, 163, 7)) +>response : Symbol(response, Decl(templateLiteralTypes3.ts, 163, 34)) + +function reducer(action: Action) { +>reducer : Symbol(reducer, Decl(templateLiteralTypes3.ts, 163, 54)) +>action : Symbol(action, Decl(templateLiteralTypes3.ts, 165, 17)) +>Action : Symbol(Action, Decl(templateLiteralTypes3.ts, 157, 1)) + + if (action.type === 'FOO_SUCCESS') { +>action.type : Symbol(type, Decl(templateLiteralTypes3.ts, 162, 7), Decl(templateLiteralTypes3.ts, 163, 7)) +>action : Symbol(action, Decl(templateLiteralTypes3.ts, 165, 17)) +>type : Symbol(type, Decl(templateLiteralTypes3.ts, 162, 7), Decl(templateLiteralTypes3.ts, 163, 7)) + + action.type; +>action.type : Symbol(type, Decl(templateLiteralTypes3.ts, 163, 7)) +>action : Symbol(action, Decl(templateLiteralTypes3.ts, 165, 17)) +>type : Symbol(type, Decl(templateLiteralTypes3.ts, 163, 7)) + + action.response; +>action.response : Symbol(response, Decl(templateLiteralTypes3.ts, 163, 34)) +>action : Symbol(action, Decl(templateLiteralTypes3.ts, 165, 17)) +>response : Symbol(response, Decl(templateLiteralTypes3.ts, 163, 34)) + } +} + diff --git a/tests/baselines/reference/templateLiteralTypes3.types b/tests/baselines/reference/templateLiteralTypes3.types index 934fa60857f9e..5a8f15f27fbab 100644 --- a/tests/baselines/reference/templateLiteralTypes3.types +++ b/tests/baselines/reference/templateLiteralTypes3.types @@ -396,3 +396,120 @@ chain("a"); >chain : (field: F | `${F}.${F}`) => void >"a" : "a" +// Repro from #46125 + +function ff1(x: `foo-${string}`, y: `${string}-bar`, z: `baz-${string}`) { +>ff1 : (x: `foo-${string}`, y: `${string}-bar`, z: `baz-${string}`) => void +>x : `foo-${string}` +>y : `${string}-bar` +>z : `baz-${string}` + + if (x === y) { +>x === y : boolean +>x : `foo-${string}` +>y : `${string}-bar` + + x; // `foo-${string}` +>x : `foo-${string}` + } + if (x === z) { // Error +>x === z : boolean +>x : `foo-${string}` +>z : `baz-${string}` + } +} + +function ff2(x: `foo-${T}`, y: `${T}-bar`, z: `baz-${T}`) { +>ff2 : (x: `foo-${T}`, y: `${T}-bar`, z: `baz-${T}`) => void +>x : `foo-${T}` +>y : `${T}-bar` +>z : `baz-${T}` + + if (x === y) { +>x === y : boolean +>x : `foo-${T}` +>y : `${T}-bar` + + x; // `foo-${T}` +>x : `foo-${T}` + } + if (x === z) { // Error +>x === z : boolean +>x : `foo-${T}` +>z : `baz-${T}` + } +} + +function ff3(x: string, y: `foo-${string}` | 'bar') { +>ff3 : (x: string, y: `foo-${string}` | 'bar') => void +>x : string +>y : "bar" | `foo-${string}` + + if (x === y) { +>x === y : boolean +>x : string +>y : "bar" | `foo-${string}` + + x; // `foo-${string}` | 'bar' +>x : "bar" | `foo-${string}` + } +} + +function ff4(x: string, y: `foo-${string}`) { +>ff4 : (x: string, y: `foo-${string}`) => void +>x : string +>y : `foo-${string}` + + if (x === 'foo-test') { +>x === 'foo-test' : boolean +>x : string +>'foo-test' : "foo-test" + + x; // 'foo-test' +>x : "foo-test" + } + if (y === 'foo-test') { +>y === 'foo-test' : boolean +>y : `foo-${string}` +>'foo-test' : "foo-test" + + y; // 'foo-test' +>y : "foo-test" + } +} + +// Repro from #46045 + +type Action = +>Action : Action + + | { type: `${string}_REQUEST` } +>type : `${string}_REQUEST` + + | { type: `${string}_SUCCESS`, response: string }; +>type : `${string}_SUCCESS` +>response : string + +function reducer(action: Action) { +>reducer : (action: Action) => void +>action : Action + + if (action.type === 'FOO_SUCCESS') { +>action.type === 'FOO_SUCCESS' : boolean +>action.type : `${string}_REQUEST` | `${string}_SUCCESS` +>action : Action +>type : `${string}_REQUEST` | `${string}_SUCCESS` +>'FOO_SUCCESS' : "FOO_SUCCESS" + + action.type; +>action.type : "FOO_SUCCESS" +>action : { type: `${string}_SUCCESS`; response: string; } +>type : "FOO_SUCCESS" + + action.response; +>action.response : string +>action : { type: `${string}_SUCCESS`; response: string; } +>response : string + } +} + diff --git a/tests/cases/conformance/types/literal/templateLiteralTypes3.ts b/tests/cases/conformance/types/literal/templateLiteralTypes3.ts index b5fc39d32ef3c..fbeae4b3f6545 100644 --- a/tests/cases/conformance/types/literal/templateLiteralTypes3.ts +++ b/tests/cases/conformance/types/literal/templateLiteralTypes3.ts @@ -126,3 +126,49 @@ type Schema = { a: { b: { c: number } } }; declare function chain(field: F | `${F}.${F}`): void; chain("a"); + +// Repro from #46125 + +function ff1(x: `foo-${string}`, y: `${string}-bar`, z: `baz-${string}`) { + if (x === y) { + x; // `foo-${string}` + } + if (x === z) { // Error + } +} + +function ff2(x: `foo-${T}`, y: `${T}-bar`, z: `baz-${T}`) { + if (x === y) { + x; // `foo-${T}` + } + if (x === z) { // Error + } +} + +function ff3(x: string, y: `foo-${string}` | 'bar') { + if (x === y) { + x; // `foo-${string}` | 'bar' + } +} + +function ff4(x: string, y: `foo-${string}`) { + if (x === 'foo-test') { + x; // 'foo-test' + } + if (y === 'foo-test') { + y; // 'foo-test' + } +} + +// Repro from #46045 + +type Action = + | { type: `${string}_REQUEST` } + | { type: `${string}_SUCCESS`, response: string }; + +function reducer(action: Action) { + if (action.type === 'FOO_SUCCESS') { + action.type; + action.response; + } +}