Skip to content

Commit 5ec836d

Browse files
authored
Fix issues + Support template literal types as discriminants (#46137)
* Fix issues + Support template literal types in discriminants * Add tests * Address CR feedback
1 parent f09c5e8 commit 5ec836d

File tree

6 files changed

+448
-15
lines changed

6 files changed

+448
-15
lines changed

src/compiler/checker.ts

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12040,7 +12040,7 @@ namespace ts {
1204012040
else if (type !== firstType) {
1204112041
checkFlags |= CheckFlags.HasNonUniformType;
1204212042
}
12043-
if (isLiteralType(type)) {
12043+
if (isLiteralType(type) || isPatternLiteralType(type)) {
1204412044
checkFlags |= CheckFlags.HasLiteralType;
1204512045
}
1204612046
if (type.flags & TypeFlags.Never) {
@@ -19014,6 +19014,9 @@ namespace ts {
1901419014
}
1901519015
else if (target.flags & TypeFlags.TemplateLiteral) {
1901619016
if (source.flags & TypeFlags.TemplateLiteral) {
19017+
if (relation === comparableRelation) {
19018+
return templateLiteralTypesDefinitelyUnrelated(source as TemplateLiteralType, target as TemplateLiteralType) ? Ternary.False : Ternary.True;
19019+
}
1901719020
// Report unreliable variance for type variables referenced in template literal type placeholders.
1901819021
// For example, `foo-${number}` is related to `foo-${string}` even though number isn't related to string.
1901919022
instantiateType(source, makeFunctionTypeMapper(reportUnreliableMarkers));
@@ -19053,11 +19056,10 @@ namespace ts {
1905319056
return result;
1905419057
}
1905519058
}
19056-
else if (source.flags & TypeFlags.TemplateLiteral) {
19059+
else if (source.flags & TypeFlags.TemplateLiteral && !(target.flags & TypeFlags.Object)) {
1905719060
if (!(target.flags & TypeFlags.TemplateLiteral)) {
19058-
const baseConstraint = getBaseConstraintOfType(source);
19059-
const constraint = baseConstraint && baseConstraint !== source ? baseConstraint : stringType;
19060-
if (result = isRelatedTo(constraint, target, reportErrors)) {
19061+
const constraint = getBaseConstraintOfType(source);
19062+
if (constraint && constraint !== source && (result = isRelatedTo(constraint, target, reportErrors))) {
1906119063
resetErrorInfo(saveErrorInfo);
1906219064
return result;
1906319065
}
@@ -21428,6 +21430,18 @@ namespace ts {
2142821430
return !!(type.symbol && some(type.symbol.declarations, hasSkipDirectInferenceFlag));
2142921431
}
2143021432

21433+
function templateLiteralTypesDefinitelyUnrelated(source: TemplateLiteralType, target: TemplateLiteralType) {
21434+
// Two template literal types with diffences in their starting or ending text spans are definitely unrelated.
21435+
const sourceStart = source.texts[0];
21436+
const targetStart = target.texts[0];
21437+
const sourceEnd = source.texts[source.texts.length - 1];
21438+
const targetEnd = target.texts[target.texts.length - 1];
21439+
const startLen = Math.min(sourceStart.length, targetStart.length);
21440+
const endLen = Math.min(sourceEnd.length, targetEnd.length);
21441+
return sourceStart.slice(0, startLen) !== targetStart.slice(0, startLen) ||
21442+
sourceEnd.slice(sourceEnd.length - endLen) !== targetEnd.slice(targetEnd.length - endLen);
21443+
}
21444+
2143121445
function isValidBigIntString(s: string): boolean {
2143221446
const scanner = createScanner(ScriptTarget.ESNext, /*skipTrivia*/ false);
2143321447
let success = true;
@@ -22528,7 +22542,7 @@ namespace ts {
2252822542
if ((prop as TransientSymbol).isDiscriminantProperty === undefined) {
2252922543
(prop as TransientSymbol).isDiscriminantProperty =
2253022544
((prop as TransientSymbol).checkFlags & CheckFlags.Discriminant) === CheckFlags.Discriminant &&
22531-
!maybeTypeOfKind(getTypeOfSymbol(prop), TypeFlags.Instantiable & ~TypeFlags.TemplateLiteral);
22545+
!isGenericType(getTypeOfSymbol(prop));
2253222546
}
2253322547
return !!(prop as TransientSymbol).isDiscriminantProperty;
2253422548
}
@@ -23087,15 +23101,17 @@ namespace ts {
2308723101
return filterType(type, t => (t.flags & kind) !== 0);
2308823102
}
2308923103

23090-
// Return a new type in which occurrences of the string and number primitive types in
23091-
// typeWithPrimitives have been replaced with occurrences of string literals and numeric
23092-
// literals in typeWithLiterals, respectively.
23104+
// Return a new type in which occurrences of the string, number and bigint primitives and placeholder template
23105+
// literal types in typeWithPrimitives have been replaced with occurrences of compatible and more specific types
23106+
// from typeWithLiterals. This is essentially a limited form of intersection between the two types. We avoid a
23107+
// true intersection because it is more costly and, when applied to union types, generates a large number of
23108+
// types we don't actually care about.
2309323109
function replacePrimitivesWithLiterals(typeWithPrimitives: Type, typeWithLiterals: Type) {
23094-
if (isTypeSubsetOf(stringType, typeWithPrimitives) && maybeTypeOfKind(typeWithLiterals, TypeFlags.StringLiteral) ||
23095-
isTypeSubsetOf(numberType, typeWithPrimitives) && maybeTypeOfKind(typeWithLiterals, TypeFlags.NumberLiteral) ||
23096-
isTypeSubsetOf(bigintType, typeWithPrimitives) && maybeTypeOfKind(typeWithLiterals, TypeFlags.BigIntLiteral)) {
23110+
if (maybeTypeOfKind(typeWithPrimitives, TypeFlags.String | TypeFlags.TemplateLiteral | TypeFlags.Number | TypeFlags.BigInt) &&
23111+
maybeTypeOfKind(typeWithLiterals, TypeFlags.StringLiteral | TypeFlags.TemplateLiteral | TypeFlags.StringMapping | TypeFlags.NumberLiteral | TypeFlags.BigIntLiteral)) {
2309723112
return mapType(typeWithPrimitives, t =>
23098-
t.flags & TypeFlags.String ? extractTypesOfKind(typeWithLiterals, TypeFlags.String | TypeFlags.StringLiteral) :
23113+
t.flags & TypeFlags.String ? extractTypesOfKind(typeWithLiterals, TypeFlags.String | TypeFlags.StringLiteral | TypeFlags.TemplateLiteral | TypeFlags.StringMapping) :
23114+
isPatternLiteralType(t) && !maybeTypeOfKind(typeWithLiterals, TypeFlags.String | TypeFlags.TemplateLiteral | TypeFlags.StringMapping) ? extractTypesOfKind(typeWithLiterals, TypeFlags.StringLiteral) :
2309923115
t.flags & TypeFlags.Number ? extractTypesOfKind(typeWithLiterals, TypeFlags.Number | TypeFlags.NumberLiteral) :
2310023116
t.flags & TypeFlags.BigInt ? extractTypesOfKind(typeWithLiterals, TypeFlags.BigInt | TypeFlags.BigIntLiteral) : t);
2310123117
}
@@ -23938,7 +23954,7 @@ namespace ts {
2393823954
const narrowedPropType = narrowType(propType);
2393923955
return filterType(type, t => {
2394023956
const discriminantType = getTypeOfPropertyOrIndexSignature(t, propName);
23941-
return !(discriminantType.flags & TypeFlags.Never) && isTypeComparableTo(discriminantType, narrowedPropType);
23957+
return !(narrowedPropType.flags & TypeFlags.Never) && isTypeComparableTo(narrowedPropType, discriminantType);
2394223958
});
2394323959
}
2394423960

tests/baselines/reference/templateLiteralTypes3.errors.txt

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ tests/cases/conformance/types/literal/templateLiteralTypes3.ts(71,5): error TS23
55
tests/cases/conformance/types/literal/templateLiteralTypes3.ts(72,5): error TS2322: Type '`*${string}*`' is not assignable to type '`*${number}*`'.
66
tests/cases/conformance/types/literal/templateLiteralTypes3.ts(74,5): error TS2322: Type '"*false*" | "*true*"' is not assignable to type '`*${number}*`'.
77
Type '"*false*"' is not assignable to type '`*${number}*`'.
8+
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.
9+
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.
810

911

10-
==== tests/cases/conformance/types/literal/templateLiteralTypes3.ts (6 errors) ====
12+
==== tests/cases/conformance/types/literal/templateLiteralTypes3.ts (8 errors) ====
1113
// Inference from template literal type to template literal type
1214

1315
type Foo1<T> = T extends `*${infer U}*` ? U : never;
@@ -146,4 +148,54 @@ tests/cases/conformance/types/literal/templateLiteralTypes3.ts(74,5): error TS23
146148
declare function chain<F extends keyof Schema>(field: F | `${F}.${F}`): void;
147149

148150
chain("a");
151+
152+
// Repro from #46125
153+
154+
function ff1(x: `foo-${string}`, y: `${string}-bar`, z: `baz-${string}`) {
155+
if (x === y) {
156+
x; // `foo-${string}`
157+
}
158+
if (x === z) { // Error
159+
~~~~~~~
160+
!!! error TS2367: This condition will always return 'false' since the types '`foo-${string}`' and '`baz-${string}`' have no overlap.
161+
}
162+
}
163+
164+
function ff2<T extends string>(x: `foo-${T}`, y: `${T}-bar`, z: `baz-${T}`) {
165+
if (x === y) {
166+
x; // `foo-${T}`
167+
}
168+
if (x === z) { // Error
169+
~~~~~~~
170+
!!! error TS2367: This condition will always return 'false' since the types '`foo-${T}`' and '`baz-${T}`' have no overlap.
171+
}
172+
}
173+
174+
function ff3(x: string, y: `foo-${string}` | 'bar') {
175+
if (x === y) {
176+
x; // `foo-${string}` | 'bar'
177+
}
178+
}
179+
180+
function ff4(x: string, y: `foo-${string}`) {
181+
if (x === 'foo-test') {
182+
x; // 'foo-test'
183+
}
184+
if (y === 'foo-test') {
185+
y; // 'foo-test'
186+
}
187+
}
188+
189+
// Repro from #46045
190+
191+
type Action =
192+
| { type: `${string}_REQUEST` }
193+
| { type: `${string}_SUCCESS`, response: string };
194+
195+
function reducer(action: Action) {
196+
if (action.type === 'FOO_SUCCESS') {
197+
action.type;
198+
action.response;
199+
}
200+
}
149201

tests/baselines/reference/templateLiteralTypes3.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,52 @@ type Schema = { a: { b: { c: number } } };
124124
declare function chain<F extends keyof Schema>(field: F | `${F}.${F}`): void;
125125

126126
chain("a");
127+
128+
// Repro from #46125
129+
130+
function ff1(x: `foo-${string}`, y: `${string}-bar`, z: `baz-${string}`) {
131+
if (x === y) {
132+
x; // `foo-${string}`
133+
}
134+
if (x === z) { // Error
135+
}
136+
}
137+
138+
function ff2<T extends string>(x: `foo-${T}`, y: `${T}-bar`, z: `baz-${T}`) {
139+
if (x === y) {
140+
x; // `foo-${T}`
141+
}
142+
if (x === z) { // Error
143+
}
144+
}
145+
146+
function ff3(x: string, y: `foo-${string}` | 'bar') {
147+
if (x === y) {
148+
x; // `foo-${string}` | 'bar'
149+
}
150+
}
151+
152+
function ff4(x: string, y: `foo-${string}`) {
153+
if (x === 'foo-test') {
154+
x; // 'foo-test'
155+
}
156+
if (y === 'foo-test') {
157+
y; // 'foo-test'
158+
}
159+
}
160+
161+
// Repro from #46045
162+
163+
type Action =
164+
| { type: `${string}_REQUEST` }
165+
| { type: `${string}_SUCCESS`, response: string };
166+
167+
function reducer(action: Action) {
168+
if (action.type === 'FOO_SUCCESS') {
169+
action.type;
170+
action.response;
171+
}
172+
}
127173

128174

129175
//// [templateLiteralTypes3.js]
@@ -177,6 +223,40 @@ var templated1 = value1 + " abc";
177223
var value2 = "abc";
178224
var templated2 = value2 + " abc";
179225
chain("a");
226+
// Repro from #46125
227+
function ff1(x, y, z) {
228+
if (x === y) {
229+
x; // `foo-${string}`
230+
}
231+
if (x === z) { // Error
232+
}
233+
}
234+
function ff2(x, y, z) {
235+
if (x === y) {
236+
x; // `foo-${T}`
237+
}
238+
if (x === z) { // Error
239+
}
240+
}
241+
function ff3(x, y) {
242+
if (x === y) {
243+
x; // `foo-${string}` | 'bar'
244+
}
245+
}
246+
function ff4(x, y) {
247+
if (x === 'foo-test') {
248+
x; // 'foo-test'
249+
}
250+
if (y === 'foo-test') {
251+
y; // 'foo-test'
252+
}
253+
}
254+
function reducer(action) {
255+
if (action.type === 'FOO_SUCCESS') {
256+
action.type;
257+
action.response;
258+
}
259+
}
180260

181261

182262
//// [templateLiteralTypes3.d.ts]
@@ -233,3 +313,14 @@ declare type Schema = {
233313
};
234314
};
235315
declare function chain<F extends keyof Schema>(field: F | `${F}.${F}`): void;
316+
declare function ff1(x: `foo-${string}`, y: `${string}-bar`, z: `baz-${string}`): void;
317+
declare function ff2<T extends string>(x: `foo-${T}`, y: `${T}-bar`, z: `baz-${T}`): void;
318+
declare function ff3(x: string, y: `foo-${string}` | 'bar'): void;
319+
declare function ff4(x: string, y: `foo-${string}`): void;
320+
declare type Action = {
321+
type: `${string}_REQUEST`;
322+
} | {
323+
type: `${string}_SUCCESS`;
324+
response: string;
325+
};
326+
declare function reducer(action: Action): void;

tests/baselines/reference/templateLiteralTypes3.symbols

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,3 +405,114 @@ declare function chain<F extends keyof Schema>(field: F | `${F}.${F}`): void;
405405
chain("a");
406406
>chain : Symbol(chain, Decl(templateLiteralTypes3.ts, 120, 42))
407407

408+
// Repro from #46125
409+
410+
function ff1(x: `foo-${string}`, y: `${string}-bar`, z: `baz-${string}`) {
411+
>ff1 : Symbol(ff1, Decl(templateLiteralTypes3.ts, 124, 11))
412+
>x : Symbol(x, Decl(templateLiteralTypes3.ts, 128, 13))
413+
>y : Symbol(y, Decl(templateLiteralTypes3.ts, 128, 32))
414+
>z : Symbol(z, Decl(templateLiteralTypes3.ts, 128, 52))
415+
416+
if (x === y) {
417+
>x : Symbol(x, Decl(templateLiteralTypes3.ts, 128, 13))
418+
>y : Symbol(y, Decl(templateLiteralTypes3.ts, 128, 32))
419+
420+
x; // `foo-${string}`
421+
>x : Symbol(x, Decl(templateLiteralTypes3.ts, 128, 13))
422+
}
423+
if (x === z) { // Error
424+
>x : Symbol(x, Decl(templateLiteralTypes3.ts, 128, 13))
425+
>z : Symbol(z, Decl(templateLiteralTypes3.ts, 128, 52))
426+
}
427+
}
428+
429+
function ff2<T extends string>(x: `foo-${T}`, y: `${T}-bar`, z: `baz-${T}`) {
430+
>ff2 : Symbol(ff2, Decl(templateLiteralTypes3.ts, 134, 1))
431+
>T : Symbol(T, Decl(templateLiteralTypes3.ts, 136, 13))
432+
>x : Symbol(x, Decl(templateLiteralTypes3.ts, 136, 31))
433+
>T : Symbol(T, Decl(templateLiteralTypes3.ts, 136, 13))
434+
>y : Symbol(y, Decl(templateLiteralTypes3.ts, 136, 45))
435+
>T : Symbol(T, Decl(templateLiteralTypes3.ts, 136, 13))
436+
>z : Symbol(z, Decl(templateLiteralTypes3.ts, 136, 60))
437+
>T : Symbol(T, Decl(templateLiteralTypes3.ts, 136, 13))
438+
439+
if (x === y) {
440+
>x : Symbol(x, Decl(templateLiteralTypes3.ts, 136, 31))
441+
>y : Symbol(y, Decl(templateLiteralTypes3.ts, 136, 45))
442+
443+
x; // `foo-${T}`
444+
>x : Symbol(x, Decl(templateLiteralTypes3.ts, 136, 31))
445+
}
446+
if (x === z) { // Error
447+
>x : Symbol(x, Decl(templateLiteralTypes3.ts, 136, 31))
448+
>z : Symbol(z, Decl(templateLiteralTypes3.ts, 136, 60))
449+
}
450+
}
451+
452+
function ff3(x: string, y: `foo-${string}` | 'bar') {
453+
>ff3 : Symbol(ff3, Decl(templateLiteralTypes3.ts, 142, 1))
454+
>x : Symbol(x, Decl(templateLiteralTypes3.ts, 144, 13))
455+
>y : Symbol(y, Decl(templateLiteralTypes3.ts, 144, 23))
456+
457+
if (x === y) {
458+
>x : Symbol(x, Decl(templateLiteralTypes3.ts, 144, 13))
459+
>y : Symbol(y, Decl(templateLiteralTypes3.ts, 144, 23))
460+
461+
x; // `foo-${string}` | 'bar'
462+
>x : Symbol(x, Decl(templateLiteralTypes3.ts, 144, 13))
463+
}
464+
}
465+
466+
function ff4(x: string, y: `foo-${string}`) {
467+
>ff4 : Symbol(ff4, Decl(templateLiteralTypes3.ts, 148, 1))
468+
>x : Symbol(x, Decl(templateLiteralTypes3.ts, 150, 13))
469+
>y : Symbol(y, Decl(templateLiteralTypes3.ts, 150, 23))
470+
471+
if (x === 'foo-test') {
472+
>x : Symbol(x, Decl(templateLiteralTypes3.ts, 150, 13))
473+
474+
x; // 'foo-test'
475+
>x : Symbol(x, Decl(templateLiteralTypes3.ts, 150, 13))
476+
}
477+
if (y === 'foo-test') {
478+
>y : Symbol(y, Decl(templateLiteralTypes3.ts, 150, 23))
479+
480+
y; // 'foo-test'
481+
>y : Symbol(y, Decl(templateLiteralTypes3.ts, 150, 23))
482+
}
483+
}
484+
485+
// Repro from #46045
486+
487+
type Action =
488+
>Action : Symbol(Action, Decl(templateLiteralTypes3.ts, 157, 1))
489+
490+
| { type: `${string}_REQUEST` }
491+
>type : Symbol(type, Decl(templateLiteralTypes3.ts, 162, 7))
492+
493+
| { type: `${string}_SUCCESS`, response: string };
494+
>type : Symbol(type, Decl(templateLiteralTypes3.ts, 163, 7))
495+
>response : Symbol(response, Decl(templateLiteralTypes3.ts, 163, 34))
496+
497+
function reducer(action: Action) {
498+
>reducer : Symbol(reducer, Decl(templateLiteralTypes3.ts, 163, 54))
499+
>action : Symbol(action, Decl(templateLiteralTypes3.ts, 165, 17))
500+
>Action : Symbol(Action, Decl(templateLiteralTypes3.ts, 157, 1))
501+
502+
if (action.type === 'FOO_SUCCESS') {
503+
>action.type : Symbol(type, Decl(templateLiteralTypes3.ts, 162, 7), Decl(templateLiteralTypes3.ts, 163, 7))
504+
>action : Symbol(action, Decl(templateLiteralTypes3.ts, 165, 17))
505+
>type : Symbol(type, Decl(templateLiteralTypes3.ts, 162, 7), Decl(templateLiteralTypes3.ts, 163, 7))
506+
507+
action.type;
508+
>action.type : Symbol(type, Decl(templateLiteralTypes3.ts, 163, 7))
509+
>action : Symbol(action, Decl(templateLiteralTypes3.ts, 165, 17))
510+
>type : Symbol(type, Decl(templateLiteralTypes3.ts, 163, 7))
511+
512+
action.response;
513+
>action.response : Symbol(response, Decl(templateLiteralTypes3.ts, 163, 34))
514+
>action : Symbol(action, Decl(templateLiteralTypes3.ts, 165, 17))
515+
>response : Symbol(response, Decl(templateLiteralTypes3.ts, 163, 34))
516+
}
517+
}
518+

0 commit comments

Comments
 (0)