Skip to content

Commit 5d6cce5

Browse files
authored
Const contexts for template literals (#40707)
* Support const assertions with template literal expressions * Add tests * Accept new baselines
1 parent c5a28fc commit 5d6cce5

File tree

6 files changed

+517
-14
lines changed

6 files changed

+517
-14
lines changed

src/compiler/checker.ts

+12-12
Original file line numberDiff line numberDiff line change
@@ -28212,6 +28212,7 @@ namespace ts {
2821228212
case SyntaxKind.FalseKeyword:
2821328213
case SyntaxKind.ArrayLiteralExpression:
2821428214
case SyntaxKind.ObjectLiteralExpression:
28215+
case SyntaxKind.TemplateExpression:
2821528216
return true;
2821628217
case SyntaxKind.ParenthesizedExpression:
2821728218
return isValidConstAssertionArgument((<ParenthesizedExpression>node).expression);
@@ -30284,18 +30285,17 @@ namespace ts {
3028430285
}
3028530286

3028630287
function checkTemplateExpression(node: TemplateExpression): Type {
30287-
// We just want to check each expressions, but we are unconcerned with
30288-
// the type of each expression, as any value may be coerced into a string.
30289-
// It is worth asking whether this is what we really want though.
30290-
// A place where we actually *are* concerned with the expressions' types are
30291-
// in tagged templates.
30292-
forEach(node.templateSpans, templateSpan => {
30293-
if (maybeTypeOfKind(checkExpression(templateSpan.expression), TypeFlags.ESSymbolLike)) {
30294-
error(templateSpan.expression, Diagnostics.Implicit_conversion_of_a_symbol_to_a_string_will_fail_at_runtime_Consider_wrapping_this_expression_in_String);
30288+
const texts = [node.head.text];
30289+
const types = [];
30290+
for (const span of node.templateSpans) {
30291+
const type = checkExpression(span.expression);
30292+
if (maybeTypeOfKind(type, TypeFlags.ESSymbolLike)) {
30293+
error(span.expression, Diagnostics.Implicit_conversion_of_a_symbol_to_a_string_will_fail_at_runtime_Consider_wrapping_this_expression_in_String);
3029530294
}
30296-
});
30297-
30298-
return stringType;
30295+
texts.push(span.literal.text);
30296+
types.push(isTypeAssignableTo(type, templateConstraintType) ? type : stringType);
30297+
}
30298+
return isConstContext(node) ? getTemplateLiteralType(texts, types) : stringType;
3029930299
}
3030030300

3030130301
function getContextNode(node: Expression): Node {
@@ -30427,7 +30427,7 @@ namespace ts {
3042730427
const parent = node.parent;
3042830428
return isAssertionExpression(parent) && isConstTypeReference(parent.type) ||
3042930429
(isParenthesizedExpression(parent) || isArrayLiteralExpression(parent) || isSpreadElement(parent)) && isConstContext(parent) ||
30430-
(isPropertyAssignment(parent) || isShorthandPropertyAssignment(parent)) && isConstContext(parent.parent);
30430+
(isPropertyAssignment(parent) || isShorthandPropertyAssignment(parent) || isTemplateSpan(parent)) && isConstContext(parent.parent);
3043130431
}
3043230432

3043330433
function checkExpressionForMutableLocation(node: Expression, checkMode: CheckMode | undefined, contextualType?: Type, forceTuple?: boolean): Type {

tests/baselines/reference/constAssertions.errors.txt

+45-1
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,48 @@ tests/cases/conformance/expressions/typeAssertions/constAssertions.ts(63,10): er
7676
let e3 = id(1) as const; // Error
7777
~~~~~
7878
!!! error TS1355: A 'const' assertions can only be applied to references to enum members, or string, number, boolean, array, or object literals.
79-
79+
80+
let t1 = 'foo' as const;
81+
let t2 = 'bar' as const;
82+
let t3 = `${t1}-${t2}` as const;
83+
let t4 = `${`(${t1})`}-${`(${t2})`}` as const;
84+
85+
function ff1(x: 'foo' | 'bar', y: 1 | 2) {
86+
return `${x}-${y}` as const;
87+
}
88+
89+
function ff2<T extends string, U extends string>(x: T, y: U) {
90+
return `${x}-${y}` as const;
91+
}
92+
93+
const ts1 = ff2('foo', 'bar');
94+
const ts2 = ff2('foo', !!true ? '0' : '1');
95+
const ts3 = ff2(!!true ? 'top' : 'bottom', !!true ? 'left' : 'right');
96+
97+
function ff3(x: 'foo' | 'bar', y: object) {
98+
return `${x}${y}` as const;
99+
}
100+
101+
type Action = "verify" | "write";
102+
type ContentMatch = "match" | "nonMatch";
103+
type Outcome = `${Action}_${ContentMatch}`;
104+
105+
function ff4(verify: boolean, contentMatches: boolean) {
106+
const action : Action = verify ? `verify` : `write`;
107+
const contentMatch: ContentMatch = contentMatches ? `match` : `nonMatch`;
108+
const outcome: Outcome = `${action}_${contentMatch}` as const;
109+
return outcome;
110+
}
111+
112+
function ff5(verify: boolean, contentMatches: boolean) {
113+
const action = verify ? `verify` : `write`;
114+
const contentMatch = contentMatches ? `match` : `nonMatch`;
115+
const outcome = `${action}_${contentMatch}` as const;
116+
return outcome;
117+
}
118+
119+
function accessorNames<S extends string>(propName: S) {
120+
return [`get-${propName}`, `set-${propName}`] as const;
121+
}
122+
123+
const ns1 = accessorNames('foo');

tests/baselines/reference/constAssertions.js

+94-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,51 @@ declare function id<T>(x: T): T;
6262
let e1 = v1 as const; // Error
6363
let e2 = (true ? 1 : 0) as const; // Error
6464
let e3 = id(1) as const; // Error
65-
65+
66+
let t1 = 'foo' as const;
67+
let t2 = 'bar' as const;
68+
let t3 = `${t1}-${t2}` as const;
69+
let t4 = `${`(${t1})`}-${`(${t2})`}` as const;
70+
71+
function ff1(x: 'foo' | 'bar', y: 1 | 2) {
72+
return `${x}-${y}` as const;
73+
}
74+
75+
function ff2<T extends string, U extends string>(x: T, y: U) {
76+
return `${x}-${y}` as const;
77+
}
78+
79+
const ts1 = ff2('foo', 'bar');
80+
const ts2 = ff2('foo', !!true ? '0' : '1');
81+
const ts3 = ff2(!!true ? 'top' : 'bottom', !!true ? 'left' : 'right');
82+
83+
function ff3(x: 'foo' | 'bar', y: object) {
84+
return `${x}${y}` as const;
85+
}
86+
87+
type Action = "verify" | "write";
88+
type ContentMatch = "match" | "nonMatch";
89+
type Outcome = `${Action}_${ContentMatch}`;
90+
91+
function ff4(verify: boolean, contentMatches: boolean) {
92+
const action : Action = verify ? `verify` : `write`;
93+
const contentMatch: ContentMatch = contentMatches ? `match` : `nonMatch`;
94+
const outcome: Outcome = `${action}_${contentMatch}` as const;
95+
return outcome;
96+
}
97+
98+
function ff5(verify: boolean, contentMatches: boolean) {
99+
const action = verify ? `verify` : `write`;
100+
const contentMatch = contentMatches ? `match` : `nonMatch`;
101+
const outcome = `${action}_${contentMatch}` as const;
102+
return outcome;
103+
}
104+
105+
function accessorNames<S extends string>(propName: S) {
106+
return [`get-${propName}`, `set-${propName}`] as const;
107+
}
108+
109+
const ns1 = accessorNames('foo');
66110

67111
//// [constAssertions.js]
68112
"use strict";
@@ -117,6 +161,38 @@ let q5 = { x: 10, y: 20 };
117161
let e1 = v1; // Error
118162
let e2 = (true ? 1 : 0); // Error
119163
let e3 = id(1); // Error
164+
let t1 = 'foo';
165+
let t2 = 'bar';
166+
let t3 = `${t1}-${t2}`;
167+
let t4 = `${`(${t1})`}-${`(${t2})`}`;
168+
function ff1(x, y) {
169+
return `${x}-${y}`;
170+
}
171+
function ff2(x, y) {
172+
return `${x}-${y}`;
173+
}
174+
const ts1 = ff2('foo', 'bar');
175+
const ts2 = ff2('foo', !!true ? '0' : '1');
176+
const ts3 = ff2(!!true ? 'top' : 'bottom', !!true ? 'left' : 'right');
177+
function ff3(x, y) {
178+
return `${x}${y}`;
179+
}
180+
function ff4(verify, contentMatches) {
181+
const action = verify ? `verify` : `write`;
182+
const contentMatch = contentMatches ? `match` : `nonMatch`;
183+
const outcome = `${action}_${contentMatch}`;
184+
return outcome;
185+
}
186+
function ff5(verify, contentMatches) {
187+
const action = verify ? `verify` : `write`;
188+
const contentMatch = contentMatches ? `match` : `nonMatch`;
189+
const outcome = `${action}_${contentMatch}`;
190+
return outcome;
191+
}
192+
function accessorNames(propName) {
193+
return [`get-${propName}`, `set-${propName}`];
194+
}
195+
const ns1 = accessorNames('foo');
120196

121197

122198
//// [constAssertions.d.ts]
@@ -218,3 +294,20 @@ declare function id<T>(x: T): T;
218294
declare let e1: "abc";
219295
declare let e2: 0 | 1;
220296
declare let e3: 1;
297+
declare let t1: "foo";
298+
declare let t2: "bar";
299+
declare let t3: "foo-bar";
300+
declare let t4: "(foo)-(bar)";
301+
declare function ff1(x: 'foo' | 'bar', y: 1 | 2): "foo-1" | "foo-2" | "bar-1" | "bar-2";
302+
declare function ff2<T extends string, U extends string>(x: T, y: U): `${T}-${U}`;
303+
declare const ts1: "foo-bar";
304+
declare const ts2: "foo-1" | "foo-0";
305+
declare const ts3: "top-left" | "top-right" | "bottom-left" | "bottom-right";
306+
declare function ff3(x: 'foo' | 'bar', y: object): string;
307+
declare type Action = "verify" | "write";
308+
declare type ContentMatch = "match" | "nonMatch";
309+
declare type Outcome = `${Action}_${ContentMatch}`;
310+
declare function ff4(verify: boolean, contentMatches: boolean): "verify_match" | "verify_nonMatch" | "write_match" | "write_nonMatch";
311+
declare function ff5(verify: boolean, contentMatches: boolean): "verify_match" | "verify_nonMatch" | "write_match" | "write_nonMatch";
312+
declare function accessorNames<S extends string>(propName: S): readonly [`get-${S}`, `set-${S}`];
313+
declare const ns1: readonly ["get-foo", "set-foo"];

tests/baselines/reference/constAssertions.symbols

+135
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,138 @@ let e3 = id(1) as const; // Error
199199
>e3 : Symbol(e3, Decl(constAssertions.ts, 62, 3))
200200
>id : Symbol(id, Decl(constAssertions.ts, 56, 34))
201201

202+
let t1 = 'foo' as const;
203+
>t1 : Symbol(t1, Decl(constAssertions.ts, 64, 3))
204+
205+
let t2 = 'bar' as const;
206+
>t2 : Symbol(t2, Decl(constAssertions.ts, 65, 3))
207+
208+
let t3 = `${t1}-${t2}` as const;
209+
>t3 : Symbol(t3, Decl(constAssertions.ts, 66, 3))
210+
>t1 : Symbol(t1, Decl(constAssertions.ts, 64, 3))
211+
>t2 : Symbol(t2, Decl(constAssertions.ts, 65, 3))
212+
213+
let t4 = `${`(${t1})`}-${`(${t2})`}` as const;
214+
>t4 : Symbol(t4, Decl(constAssertions.ts, 67, 3))
215+
>t1 : Symbol(t1, Decl(constAssertions.ts, 64, 3))
216+
>t2 : Symbol(t2, Decl(constAssertions.ts, 65, 3))
217+
218+
function ff1(x: 'foo' | 'bar', y: 1 | 2) {
219+
>ff1 : Symbol(ff1, Decl(constAssertions.ts, 67, 46))
220+
>x : Symbol(x, Decl(constAssertions.ts, 69, 13))
221+
>y : Symbol(y, Decl(constAssertions.ts, 69, 30))
222+
223+
return `${x}-${y}` as const;
224+
>x : Symbol(x, Decl(constAssertions.ts, 69, 13))
225+
>y : Symbol(y, Decl(constAssertions.ts, 69, 30))
226+
}
227+
228+
function ff2<T extends string, U extends string>(x: T, y: U) {
229+
>ff2 : Symbol(ff2, Decl(constAssertions.ts, 71, 1))
230+
>T : Symbol(T, Decl(constAssertions.ts, 73, 13))
231+
>U : Symbol(U, Decl(constAssertions.ts, 73, 30))
232+
>x : Symbol(x, Decl(constAssertions.ts, 73, 49))
233+
>T : Symbol(T, Decl(constAssertions.ts, 73, 13))
234+
>y : Symbol(y, Decl(constAssertions.ts, 73, 54))
235+
>U : Symbol(U, Decl(constAssertions.ts, 73, 30))
236+
237+
return `${x}-${y}` as const;
238+
>x : Symbol(x, Decl(constAssertions.ts, 73, 49))
239+
>y : Symbol(y, Decl(constAssertions.ts, 73, 54))
240+
}
241+
242+
const ts1 = ff2('foo', 'bar');
243+
>ts1 : Symbol(ts1, Decl(constAssertions.ts, 77, 5))
244+
>ff2 : Symbol(ff2, Decl(constAssertions.ts, 71, 1))
245+
246+
const ts2 = ff2('foo', !!true ? '0' : '1');
247+
>ts2 : Symbol(ts2, Decl(constAssertions.ts, 78, 5))
248+
>ff2 : Symbol(ff2, Decl(constAssertions.ts, 71, 1))
249+
250+
const ts3 = ff2(!!true ? 'top' : 'bottom', !!true ? 'left' : 'right');
251+
>ts3 : Symbol(ts3, Decl(constAssertions.ts, 79, 5))
252+
>ff2 : Symbol(ff2, Decl(constAssertions.ts, 71, 1))
253+
254+
function ff3(x: 'foo' | 'bar', y: object) {
255+
>ff3 : Symbol(ff3, Decl(constAssertions.ts, 79, 70))
256+
>x : Symbol(x, Decl(constAssertions.ts, 81, 13))
257+
>y : Symbol(y, Decl(constAssertions.ts, 81, 30))
258+
259+
return `${x}${y}` as const;
260+
>x : Symbol(x, Decl(constAssertions.ts, 81, 13))
261+
>y : Symbol(y, Decl(constAssertions.ts, 81, 30))
262+
}
263+
264+
type Action = "verify" | "write";
265+
>Action : Symbol(Action, Decl(constAssertions.ts, 83, 1))
266+
267+
type ContentMatch = "match" | "nonMatch";
268+
>ContentMatch : Symbol(ContentMatch, Decl(constAssertions.ts, 85, 33))
269+
270+
type Outcome = `${Action}_${ContentMatch}`;
271+
>Outcome : Symbol(Outcome, Decl(constAssertions.ts, 86, 41))
272+
>Action : Symbol(Action, Decl(constAssertions.ts, 83, 1))
273+
>ContentMatch : Symbol(ContentMatch, Decl(constAssertions.ts, 85, 33))
274+
275+
function ff4(verify: boolean, contentMatches: boolean) {
276+
>ff4 : Symbol(ff4, Decl(constAssertions.ts, 87, 43))
277+
>verify : Symbol(verify, Decl(constAssertions.ts, 89, 13))
278+
>contentMatches : Symbol(contentMatches, Decl(constAssertions.ts, 89, 29))
279+
280+
const action : Action = verify ? `verify` : `write`;
281+
>action : Symbol(action, Decl(constAssertions.ts, 90, 9))
282+
>Action : Symbol(Action, Decl(constAssertions.ts, 83, 1))
283+
>verify : Symbol(verify, Decl(constAssertions.ts, 89, 13))
284+
285+
const contentMatch: ContentMatch = contentMatches ? `match` : `nonMatch`;
286+
>contentMatch : Symbol(contentMatch, Decl(constAssertions.ts, 91, 9))
287+
>ContentMatch : Symbol(ContentMatch, Decl(constAssertions.ts, 85, 33))
288+
>contentMatches : Symbol(contentMatches, Decl(constAssertions.ts, 89, 29))
289+
290+
const outcome: Outcome = `${action}_${contentMatch}` as const;
291+
>outcome : Symbol(outcome, Decl(constAssertions.ts, 92, 9))
292+
>Outcome : Symbol(Outcome, Decl(constAssertions.ts, 86, 41))
293+
>action : Symbol(action, Decl(constAssertions.ts, 90, 9))
294+
>contentMatch : Symbol(contentMatch, Decl(constAssertions.ts, 91, 9))
295+
296+
return outcome;
297+
>outcome : Symbol(outcome, Decl(constAssertions.ts, 92, 9))
298+
}
299+
300+
function ff5(verify: boolean, contentMatches: boolean) {
301+
>ff5 : Symbol(ff5, Decl(constAssertions.ts, 94, 1))
302+
>verify : Symbol(verify, Decl(constAssertions.ts, 96, 13))
303+
>contentMatches : Symbol(contentMatches, Decl(constAssertions.ts, 96, 29))
304+
305+
const action = verify ? `verify` : `write`;
306+
>action : Symbol(action, Decl(constAssertions.ts, 97, 9))
307+
>verify : Symbol(verify, Decl(constAssertions.ts, 96, 13))
308+
309+
const contentMatch = contentMatches ? `match` : `nonMatch`;
310+
>contentMatch : Symbol(contentMatch, Decl(constAssertions.ts, 98, 9))
311+
>contentMatches : Symbol(contentMatches, Decl(constAssertions.ts, 96, 29))
312+
313+
const outcome = `${action}_${contentMatch}` as const;
314+
>outcome : Symbol(outcome, Decl(constAssertions.ts, 99, 9))
315+
>action : Symbol(action, Decl(constAssertions.ts, 97, 9))
316+
>contentMatch : Symbol(contentMatch, Decl(constAssertions.ts, 98, 9))
317+
318+
return outcome;
319+
>outcome : Symbol(outcome, Decl(constAssertions.ts, 99, 9))
320+
}
321+
322+
function accessorNames<S extends string>(propName: S) {
323+
>accessorNames : Symbol(accessorNames, Decl(constAssertions.ts, 101, 1))
324+
>S : Symbol(S, Decl(constAssertions.ts, 103, 23))
325+
>propName : Symbol(propName, Decl(constAssertions.ts, 103, 41))
326+
>S : Symbol(S, Decl(constAssertions.ts, 103, 23))
327+
328+
return [`get-${propName}`, `set-${propName}`] as const;
329+
>propName : Symbol(propName, Decl(constAssertions.ts, 103, 41))
330+
>propName : Symbol(propName, Decl(constAssertions.ts, 103, 41))
331+
}
332+
333+
const ns1 = accessorNames('foo');
334+
>ns1 : Symbol(ns1, Decl(constAssertions.ts, 107, 5))
335+
>accessorNames : Symbol(accessorNames, Decl(constAssertions.ts, 101, 1))
336+

0 commit comments

Comments
 (0)