Skip to content

Commit 6b0cf9a

Browse files
committed
Allow pattern literal types like http://${string} to exist and be reasoned about
1 parent a5babe1 commit 6b0cf9a

File tree

6 files changed

+207
-26
lines changed

6 files changed

+207
-26
lines changed

src/compiler/checker.ts

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13489,7 +13489,7 @@ namespace ts {
1348913489
text += s;
1349013490
text += texts[i + 1];
1349113491
}
13492-
else if (isGenericIndexType(t)) {
13492+
else if (isGenericIndexType(t) || t === stringType) {
1349313493
newTypes.push(t);
1349413494
newCasings.push(casings[i]);
1349513495
newTexts.push(text);
@@ -13753,6 +13753,10 @@ namespace ts {
1375313753
accessNode;
1375413754
}
1375513755

13756+
function isPatternLiteralType(type: Type) {
13757+
return !!(type.flags & TypeFlags.TemplateLiteral) && every((type as TemplateLiteralType).types, t => t === stringType);
13758+
}
13759+
1375613760
function isGenericObjectType(type: Type): boolean {
1375713761
if (type.flags & TypeFlags.UnionOrIntersection) {
1375813762
if (!((<UnionOrIntersectionType>type).objectFlags & ObjectFlags.IsGenericObjectTypeComputed)) {
@@ -13772,7 +13776,7 @@ namespace ts {
1377213776
}
1377313777
return !!((<UnionOrIntersectionType>type).objectFlags & ObjectFlags.IsGenericIndexType);
1377413778
}
13775-
return !!(type.flags & (TypeFlags.InstantiableNonPrimitive | TypeFlags.Index | TypeFlags.TemplateLiteral));
13779+
return !!(type.flags & (TypeFlags.InstantiableNonPrimitive | TypeFlags.Index | TypeFlags.TemplateLiteral)) && !isPatternLiteralType(type);
1377613780
}
1377713781

1377813782
function isThisTypeParameter(type: Type): boolean {
@@ -17277,6 +17281,15 @@ namespace ts {
1727717281
}
1727817282
}
1727917283
}
17284+
else if (target.flags & TypeFlags.TemplateLiteral && source.flags & TypeFlags.StringLiteral) {
17285+
if (isPatternLiteralType(target)) {
17286+
// match all non-`string` segemnts
17287+
const result = inferLiteralsFromTemplateLiteralType(source as StringLiteralType, target as TemplateLiteralType);
17288+
if (result) {
17289+
return Ternary.True;
17290+
}
17291+
}
17292+
}
1728017293

1728117294
if (source.flags & TypeFlags.TypeVariable) {
1728217295
if (source.flags & TypeFlags.IndexedAccess && target.flags & TypeFlags.IndexedAccess) {
@@ -18224,12 +18237,12 @@ namespace ts {
1822418237

1822518238
if (type.flags & TypeFlags.Instantiable) {
1822618239
const constraint = getConstraintOfType(type);
18227-
if (constraint) {
18240+
if (constraint && constraint !== type) {
1822818241
return typeCouldHaveTopLevelSingletonTypes(constraint);
1822918242
}
1823018243
}
1823118244

18232-
return isUnitType(type);
18245+
return isUnitType(type) || !!(type.flags & TypeFlags.TemplateLiteral);
1823318246
}
1823418247

1823518248
function getBestMatchingType(source: Type, target: UnionOrIntersectionType, isRelatedTo = compareTypesAssignable) {
@@ -19599,6 +19612,27 @@ namespace ts {
1959919612
return !!(type.symbol && some(type.symbol.declarations, hasSkipDirectInferenceFlag));
1960019613
}
1960119614

19615+
function inferLiteralsFromTemplateLiteralType(source: StringLiteralType, target: TemplateLiteralType): Type[] | undefined {
19616+
const value = source.value;
19617+
const texts = target.texts;
19618+
const lastIndex = texts.length - 1;
19619+
const startText = texts[0];
19620+
const endText = texts[lastIndex];
19621+
if (!(value.startsWith(startText) && value.endsWith(endText))) return undefined;
19622+
const matches = [];
19623+
const str = value.slice(startText.length, value.length - endText.length);
19624+
let pos = 0;
19625+
for (let i = 1; i < lastIndex; i++) {
19626+
const delim = texts[i];
19627+
const delimPos = delim.length > 0 ? str.indexOf(delim, pos) : pos < str.length ? pos + 1 : -1;
19628+
if (delimPos < 0) return undefined;
19629+
matches.push(getLiteralType(str.slice(pos, delimPos)));
19630+
pos = delimPos + delim.length;
19631+
}
19632+
matches.push(getLiteralType(str.slice(pos)));
19633+
return matches;
19634+
}
19635+
1960219636
function inferTypes(inferences: InferenceInfo[], originalSource: Type, originalTarget: Type, priority: InferencePriority = 0, contravariant = false) {
1960319637
let bivariant = false;
1960419638
let propagationType: Type;
@@ -20071,27 +20105,6 @@ namespace ts {
2007120105
}
2007220106
}
2007320107

20074-
function inferLiteralsFromTemplateLiteralType(source: StringLiteralType, target: TemplateLiteralType): Type[] | undefined {
20075-
const value = source.value;
20076-
const texts = target.texts;
20077-
const lastIndex = texts.length - 1;
20078-
const startText = texts[0];
20079-
const endText = texts[lastIndex];
20080-
if (!(value.startsWith(startText) && value.endsWith(endText))) return undefined;
20081-
const matches = [];
20082-
const str = value.slice(startText.length, value.length - endText.length);
20083-
let pos = 0;
20084-
for (let i = 1; i < lastIndex; i++) {
20085-
const delim = texts[i];
20086-
const delimPos = delim.length > 0 ? str.indexOf(delim, pos) : pos < str.length ? pos + 1 : -1;
20087-
if (delimPos < 0) return undefined;
20088-
matches.push(getLiteralType(str.slice(pos, delimPos)));
20089-
pos = delimPos + delim.length;
20090-
}
20091-
matches.push(getLiteralType(str.slice(pos)));
20092-
return matches;
20093-
}
20094-
2009520108
function inferFromObjectTypes(source: Type, target: Type) {
2009620109
if (getObjectFlags(source) & ObjectFlags.Reference && getObjectFlags(target) & ObjectFlags.Reference && (
2009720110
(<TypeReference>source).target === (<TypeReference>target).target || isArrayType(source) && isArrayType(target))) {
@@ -31580,7 +31593,7 @@ namespace ts {
3158031593
checkSourceElement(span.type);
3158131594
const type = getTypeFromTypeNode(span.type);
3158231595
checkTypeAssignableTo(type, templateConstraintType, span.type);
31583-
if (!everyType(type, t => !!(t.flags & TypeFlags.Literal) || isGenericIndexType(t))) {
31596+
if (!everyType(type, t => !!(t.flags & TypeFlags.Literal) || isGenericIndexType(t) || t === stringType)) {
3158431597
error(span.type, Diagnostics.Template_literal_type_argument_0_is_not_literal_type_or_a_generic_type, typeToString(type));
3158531598
}
3158631599
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
tests/cases/conformance/types/literal/templateLiteralTypesPatterns.ts(7,7): error TS2322: Type '"no slash"' is not assignable to type '`/${string}`'.
2+
tests/cases/conformance/types/literal/templateLiteralTypesPatterns.ts(14,10): error TS2345: Argument of type '"example.com/noprotocol"' is not assignable to parameter of type '`http://${string}` | `https://${string}` | `ftp://${string}`'.
3+
tests/cases/conformance/types/literal/templateLiteralTypesPatterns.ts(16,10): error TS2345: Argument of type '"gopher://example.com/protocol"' is not assignable to parameter of type '`http://${string}` | `https://${string}` | `ftp://${string}`'.
4+
5+
6+
==== tests/cases/conformance/types/literal/templateLiteralTypesPatterns.ts (3 errors) ====
7+
type RequiresLeadingSlash = `/${string}`;
8+
9+
// ok
10+
const a: RequiresLeadingSlash = "/bin";
11+
12+
// not ok
13+
const b: RequiresLeadingSlash = "no slash";
14+
~
15+
!!! error TS2322: Type '"no slash"' is not assignable to type '`/${string}`'.
16+
17+
type Protocol<T extends string, U extends string> = `${T}://${U}`;
18+
function download(hostSpec: Protocol<"http" | "https" | "ftp", string>) { }
19+
// ok, has protocol
20+
download("http://example.com/protocol");
21+
// issues error - no protocol
22+
download("example.com/noprotocol");
23+
~~~~~~~~~~~~~~~~~~~~~~~~
24+
!!! error TS2345: Argument of type '"example.com/noprotocol"' is not assignable to parameter of type '`http://${string}` | `https://${string}` | `ftp://${string}`'.
25+
// issues error, incorrect protocol
26+
download("gopher://example.com/protocol");
27+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
28+
!!! error TS2345: Argument of type '"gopher://example.com/protocol"' is not assignable to parameter of type '`http://${string}` | `https://${string}` | `ftp://${string}`'.
29+
30+
const q: RequiresLeadingSlash extends string ? true : false = true;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//// [templateLiteralTypesPatterns.ts]
2+
type RequiresLeadingSlash = `/${string}`;
3+
4+
// ok
5+
const a: RequiresLeadingSlash = "/bin";
6+
7+
// not ok
8+
const b: RequiresLeadingSlash = "no slash";
9+
10+
type Protocol<T extends string, U extends string> = `${T}://${U}`;
11+
function download(hostSpec: Protocol<"http" | "https" | "ftp", string>) { }
12+
// ok, has protocol
13+
download("http://example.com/protocol");
14+
// issues error - no protocol
15+
download("example.com/noprotocol");
16+
// issues error, incorrect protocol
17+
download("gopher://example.com/protocol");
18+
19+
const q: RequiresLeadingSlash extends string ? true : false = true;
20+
21+
//// [templateLiteralTypesPatterns.js]
22+
// ok
23+
var a = "/bin";
24+
// not ok
25+
var b = "no slash";
26+
function download(hostSpec) { }
27+
// ok, has protocol
28+
download("http://example.com/protocol");
29+
// issues error - no protocol
30+
download("example.com/noprotocol");
31+
// issues error, incorrect protocol
32+
download("gopher://example.com/protocol");
33+
var q = true;
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
=== tests/cases/conformance/types/literal/templateLiteralTypesPatterns.ts ===
2+
type RequiresLeadingSlash = `/${string}`;
3+
>RequiresLeadingSlash : Symbol(RequiresLeadingSlash, Decl(templateLiteralTypesPatterns.ts, 0, 0))
4+
5+
// ok
6+
const a: RequiresLeadingSlash = "/bin";
7+
>a : Symbol(a, Decl(templateLiteralTypesPatterns.ts, 3, 5))
8+
>RequiresLeadingSlash : Symbol(RequiresLeadingSlash, Decl(templateLiteralTypesPatterns.ts, 0, 0))
9+
10+
// not ok
11+
const b: RequiresLeadingSlash = "no slash";
12+
>b : Symbol(b, Decl(templateLiteralTypesPatterns.ts, 6, 5))
13+
>RequiresLeadingSlash : Symbol(RequiresLeadingSlash, Decl(templateLiteralTypesPatterns.ts, 0, 0))
14+
15+
type Protocol<T extends string, U extends string> = `${T}://${U}`;
16+
>Protocol : Symbol(Protocol, Decl(templateLiteralTypesPatterns.ts, 6, 43))
17+
>T : Symbol(T, Decl(templateLiteralTypesPatterns.ts, 8, 14))
18+
>U : Symbol(U, Decl(templateLiteralTypesPatterns.ts, 8, 31))
19+
>T : Symbol(T, Decl(templateLiteralTypesPatterns.ts, 8, 14))
20+
>U : Symbol(U, Decl(templateLiteralTypesPatterns.ts, 8, 31))
21+
22+
function download(hostSpec: Protocol<"http" | "https" | "ftp", string>) { }
23+
>download : Symbol(download, Decl(templateLiteralTypesPatterns.ts, 8, 66))
24+
>hostSpec : Symbol(hostSpec, Decl(templateLiteralTypesPatterns.ts, 9, 18))
25+
>Protocol : Symbol(Protocol, Decl(templateLiteralTypesPatterns.ts, 6, 43))
26+
27+
// ok, has protocol
28+
download("http://example.com/protocol");
29+
>download : Symbol(download, Decl(templateLiteralTypesPatterns.ts, 8, 66))
30+
31+
// issues error - no protocol
32+
download("example.com/noprotocol");
33+
>download : Symbol(download, Decl(templateLiteralTypesPatterns.ts, 8, 66))
34+
35+
// issues error, incorrect protocol
36+
download("gopher://example.com/protocol");
37+
>download : Symbol(download, Decl(templateLiteralTypesPatterns.ts, 8, 66))
38+
39+
const q: RequiresLeadingSlash extends string ? true : false = true;
40+
>q : Symbol(q, Decl(templateLiteralTypesPatterns.ts, 17, 5))
41+
>RequiresLeadingSlash : Symbol(RequiresLeadingSlash, Decl(templateLiteralTypesPatterns.ts, 0, 0))
42+
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
=== tests/cases/conformance/types/literal/templateLiteralTypesPatterns.ts ===
2+
type RequiresLeadingSlash = `/${string}`;
3+
>RequiresLeadingSlash : `/${string}`
4+
5+
// ok
6+
const a: RequiresLeadingSlash = "/bin";
7+
>a : `/${string}`
8+
>"/bin" : "/bin"
9+
10+
// not ok
11+
const b: RequiresLeadingSlash = "no slash";
12+
>b : `/${string}`
13+
>"no slash" : "no slash"
14+
15+
type Protocol<T extends string, U extends string> = `${T}://${U}`;
16+
>Protocol : `${T}://${U}`
17+
18+
function download(hostSpec: Protocol<"http" | "https" | "ftp", string>) { }
19+
>download : (hostSpec: Protocol<"http" | "https" | "ftp", string>) => void
20+
>hostSpec : `http://${string}` | `https://${string}` | `ftp://${string}`
21+
22+
// ok, has protocol
23+
download("http://example.com/protocol");
24+
>download("http://example.com/protocol") : void
25+
>download : (hostSpec: `http://${string}` | `https://${string}` | `ftp://${string}`) => void
26+
>"http://example.com/protocol" : "http://example.com/protocol"
27+
28+
// issues error - no protocol
29+
download("example.com/noprotocol");
30+
>download("example.com/noprotocol") : void
31+
>download : (hostSpec: `http://${string}` | `https://${string}` | `ftp://${string}`) => void
32+
>"example.com/noprotocol" : "example.com/noprotocol"
33+
34+
// issues error, incorrect protocol
35+
download("gopher://example.com/protocol");
36+
>download("gopher://example.com/protocol") : void
37+
>download : (hostSpec: `http://${string}` | `https://${string}` | `ftp://${string}`) => void
38+
>"gopher://example.com/protocol" : "gopher://example.com/protocol"
39+
40+
const q: RequiresLeadingSlash extends string ? true : false = true;
41+
>q : true
42+
>true : true
43+
>false : false
44+
>true : true
45+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
type RequiresLeadingSlash = `/${string}`;
2+
3+
// ok
4+
const a: RequiresLeadingSlash = "/bin";
5+
6+
// not ok
7+
const b: RequiresLeadingSlash = "no slash";
8+
9+
type Protocol<T extends string, U extends string> = `${T}://${U}`;
10+
function download(hostSpec: Protocol<"http" | "https" | "ftp", string>) { }
11+
// ok, has protocol
12+
download("http://example.com/protocol");
13+
// issues error - no protocol
14+
download("example.com/noprotocol");
15+
// issues error, incorrect protocol
16+
download("gopher://example.com/protocol");
17+
18+
const q: RequiresLeadingSlash extends string ? true : false = true;

0 commit comments

Comments
 (0)