Skip to content

Commit

Permalink
Const contexts for template literals (#40707)
Browse files Browse the repository at this point in the history
* Support const assertions with template literal expressions

* Add tests

* Accept new baselines
  • Loading branch information
ahejlsberg authored Sep 22, 2020
1 parent c5a28fc commit 5d6cce5
Show file tree
Hide file tree
Showing 6 changed files with 517 additions and 14 deletions.
24 changes: 12 additions & 12 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28212,6 +28212,7 @@ namespace ts {
case SyntaxKind.FalseKeyword:
case SyntaxKind.ArrayLiteralExpression:
case SyntaxKind.ObjectLiteralExpression:
case SyntaxKind.TemplateExpression:
return true;
case SyntaxKind.ParenthesizedExpression:
return isValidConstAssertionArgument((<ParenthesizedExpression>node).expression);
Expand Down Expand Up @@ -30284,18 +30285,17 @@ namespace ts {
}

function checkTemplateExpression(node: TemplateExpression): Type {
// We just want to check each expressions, but we are unconcerned with
// the type of each expression, as any value may be coerced into a string.
// It is worth asking whether this is what we really want though.
// A place where we actually *are* concerned with the expressions' types are
// in tagged templates.
forEach(node.templateSpans, templateSpan => {
if (maybeTypeOfKind(checkExpression(templateSpan.expression), TypeFlags.ESSymbolLike)) {
error(templateSpan.expression, Diagnostics.Implicit_conversion_of_a_symbol_to_a_string_will_fail_at_runtime_Consider_wrapping_this_expression_in_String);
const texts = [node.head.text];
const types = [];
for (const span of node.templateSpans) {
const type = checkExpression(span.expression);
if (maybeTypeOfKind(type, TypeFlags.ESSymbolLike)) {
error(span.expression, Diagnostics.Implicit_conversion_of_a_symbol_to_a_string_will_fail_at_runtime_Consider_wrapping_this_expression_in_String);
}
});

return stringType;
texts.push(span.literal.text);
types.push(isTypeAssignableTo(type, templateConstraintType) ? type : stringType);
}
return isConstContext(node) ? getTemplateLiteralType(texts, types) : stringType;
}

function getContextNode(node: Expression): Node {
Expand Down Expand Up @@ -30427,7 +30427,7 @@ namespace ts {
const parent = node.parent;
return isAssertionExpression(parent) && isConstTypeReference(parent.type) ||
(isParenthesizedExpression(parent) || isArrayLiteralExpression(parent) || isSpreadElement(parent)) && isConstContext(parent) ||
(isPropertyAssignment(parent) || isShorthandPropertyAssignment(parent)) && isConstContext(parent.parent);
(isPropertyAssignment(parent) || isShorthandPropertyAssignment(parent) || isTemplateSpan(parent)) && isConstContext(parent.parent);
}

function checkExpressionForMutableLocation(node: Expression, checkMode: CheckMode | undefined, contextualType?: Type, forceTuple?: boolean): Type {
Expand Down
46 changes: 45 additions & 1 deletion tests/baselines/reference/constAssertions.errors.txt
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,48 @@ tests/cases/conformance/expressions/typeAssertions/constAssertions.ts(63,10): er
let e3 = id(1) as const; // Error
~~~~~
!!! error TS1355: A 'const' assertions can only be applied to references to enum members, or string, number, boolean, array, or object literals.


let t1 = 'foo' as const;
let t2 = 'bar' as const;
let t3 = `${t1}-${t2}` as const;
let t4 = `${`(${t1})`}-${`(${t2})`}` as const;

function ff1(x: 'foo' | 'bar', y: 1 | 2) {
return `${x}-${y}` as const;
}

function ff2<T extends string, U extends string>(x: T, y: U) {
return `${x}-${y}` as const;
}

const ts1 = ff2('foo', 'bar');
const ts2 = ff2('foo', !!true ? '0' : '1');
const ts3 = ff2(!!true ? 'top' : 'bottom', !!true ? 'left' : 'right');

function ff3(x: 'foo' | 'bar', y: object) {
return `${x}${y}` as const;
}

type Action = "verify" | "write";
type ContentMatch = "match" | "nonMatch";
type Outcome = `${Action}_${ContentMatch}`;

function ff4(verify: boolean, contentMatches: boolean) {
const action : Action = verify ? `verify` : `write`;
const contentMatch: ContentMatch = contentMatches ? `match` : `nonMatch`;
const outcome: Outcome = `${action}_${contentMatch}` as const;
return outcome;
}

function ff5(verify: boolean, contentMatches: boolean) {
const action = verify ? `verify` : `write`;
const contentMatch = contentMatches ? `match` : `nonMatch`;
const outcome = `${action}_${contentMatch}` as const;
return outcome;
}

function accessorNames<S extends string>(propName: S) {
return [`get-${propName}`, `set-${propName}`] as const;
}

const ns1 = accessorNames('foo');
95 changes: 94 additions & 1 deletion tests/baselines/reference/constAssertions.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,51 @@ declare function id<T>(x: T): T;
let e1 = v1 as const; // Error
let e2 = (true ? 1 : 0) as const; // Error
let e3 = id(1) as const; // Error


let t1 = 'foo' as const;
let t2 = 'bar' as const;
let t3 = `${t1}-${t2}` as const;
let t4 = `${`(${t1})`}-${`(${t2})`}` as const;

function ff1(x: 'foo' | 'bar', y: 1 | 2) {
return `${x}-${y}` as const;
}

function ff2<T extends string, U extends string>(x: T, y: U) {
return `${x}-${y}` as const;
}

const ts1 = ff2('foo', 'bar');
const ts2 = ff2('foo', !!true ? '0' : '1');
const ts3 = ff2(!!true ? 'top' : 'bottom', !!true ? 'left' : 'right');

function ff3(x: 'foo' | 'bar', y: object) {
return `${x}${y}` as const;
}

type Action = "verify" | "write";
type ContentMatch = "match" | "nonMatch";
type Outcome = `${Action}_${ContentMatch}`;

function ff4(verify: boolean, contentMatches: boolean) {
const action : Action = verify ? `verify` : `write`;
const contentMatch: ContentMatch = contentMatches ? `match` : `nonMatch`;
const outcome: Outcome = `${action}_${contentMatch}` as const;
return outcome;
}

function ff5(verify: boolean, contentMatches: boolean) {
const action = verify ? `verify` : `write`;
const contentMatch = contentMatches ? `match` : `nonMatch`;
const outcome = `${action}_${contentMatch}` as const;
return outcome;
}

function accessorNames<S extends string>(propName: S) {
return [`get-${propName}`, `set-${propName}`] as const;
}

const ns1 = accessorNames('foo');

//// [constAssertions.js]
"use strict";
Expand Down Expand Up @@ -117,6 +161,38 @@ let q5 = { x: 10, y: 20 };
let e1 = v1; // Error
let e2 = (true ? 1 : 0); // Error
let e3 = id(1); // Error
let t1 = 'foo';
let t2 = 'bar';
let t3 = `${t1}-${t2}`;
let t4 = `${`(${t1})`}-${`(${t2})`}`;
function ff1(x, y) {
return `${x}-${y}`;
}
function ff2(x, y) {
return `${x}-${y}`;
}
const ts1 = ff2('foo', 'bar');
const ts2 = ff2('foo', !!true ? '0' : '1');
const ts3 = ff2(!!true ? 'top' : 'bottom', !!true ? 'left' : 'right');
function ff3(x, y) {
return `${x}${y}`;
}
function ff4(verify, contentMatches) {
const action = verify ? `verify` : `write`;
const contentMatch = contentMatches ? `match` : `nonMatch`;
const outcome = `${action}_${contentMatch}`;
return outcome;
}
function ff5(verify, contentMatches) {
const action = verify ? `verify` : `write`;
const contentMatch = contentMatches ? `match` : `nonMatch`;
const outcome = `${action}_${contentMatch}`;
return outcome;
}
function accessorNames(propName) {
return [`get-${propName}`, `set-${propName}`];
}
const ns1 = accessorNames('foo');


//// [constAssertions.d.ts]
Expand Down Expand Up @@ -218,3 +294,20 @@ declare function id<T>(x: T): T;
declare let e1: "abc";
declare let e2: 0 | 1;
declare let e3: 1;
declare let t1: "foo";
declare let t2: "bar";
declare let t3: "foo-bar";
declare let t4: "(foo)-(bar)";
declare function ff1(x: 'foo' | 'bar', y: 1 | 2): "foo-1" | "foo-2" | "bar-1" | "bar-2";
declare function ff2<T extends string, U extends string>(x: T, y: U): `${T}-${U}`;
declare const ts1: "foo-bar";
declare const ts2: "foo-1" | "foo-0";
declare const ts3: "top-left" | "top-right" | "bottom-left" | "bottom-right";
declare function ff3(x: 'foo' | 'bar', y: object): string;
declare type Action = "verify" | "write";
declare type ContentMatch = "match" | "nonMatch";
declare type Outcome = `${Action}_${ContentMatch}`;
declare function ff4(verify: boolean, contentMatches: boolean): "verify_match" | "verify_nonMatch" | "write_match" | "write_nonMatch";
declare function ff5(verify: boolean, contentMatches: boolean): "verify_match" | "verify_nonMatch" | "write_match" | "write_nonMatch";
declare function accessorNames<S extends string>(propName: S): readonly [`get-${S}`, `set-${S}`];
declare const ns1: readonly ["get-foo", "set-foo"];
135 changes: 135 additions & 0 deletions tests/baselines/reference/constAssertions.symbols
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,138 @@ let e3 = id(1) as const; // Error
>e3 : Symbol(e3, Decl(constAssertions.ts, 62, 3))
>id : Symbol(id, Decl(constAssertions.ts, 56, 34))

let t1 = 'foo' as const;
>t1 : Symbol(t1, Decl(constAssertions.ts, 64, 3))

let t2 = 'bar' as const;
>t2 : Symbol(t2, Decl(constAssertions.ts, 65, 3))

let t3 = `${t1}-${t2}` as const;
>t3 : Symbol(t3, Decl(constAssertions.ts, 66, 3))
>t1 : Symbol(t1, Decl(constAssertions.ts, 64, 3))
>t2 : Symbol(t2, Decl(constAssertions.ts, 65, 3))

let t4 = `${`(${t1})`}-${`(${t2})`}` as const;
>t4 : Symbol(t4, Decl(constAssertions.ts, 67, 3))
>t1 : Symbol(t1, Decl(constAssertions.ts, 64, 3))
>t2 : Symbol(t2, Decl(constAssertions.ts, 65, 3))

function ff1(x: 'foo' | 'bar', y: 1 | 2) {
>ff1 : Symbol(ff1, Decl(constAssertions.ts, 67, 46))
>x : Symbol(x, Decl(constAssertions.ts, 69, 13))
>y : Symbol(y, Decl(constAssertions.ts, 69, 30))

return `${x}-${y}` as const;
>x : Symbol(x, Decl(constAssertions.ts, 69, 13))
>y : Symbol(y, Decl(constAssertions.ts, 69, 30))
}

function ff2<T extends string, U extends string>(x: T, y: U) {
>ff2 : Symbol(ff2, Decl(constAssertions.ts, 71, 1))
>T : Symbol(T, Decl(constAssertions.ts, 73, 13))
>U : Symbol(U, Decl(constAssertions.ts, 73, 30))
>x : Symbol(x, Decl(constAssertions.ts, 73, 49))
>T : Symbol(T, Decl(constAssertions.ts, 73, 13))
>y : Symbol(y, Decl(constAssertions.ts, 73, 54))
>U : Symbol(U, Decl(constAssertions.ts, 73, 30))

return `${x}-${y}` as const;
>x : Symbol(x, Decl(constAssertions.ts, 73, 49))
>y : Symbol(y, Decl(constAssertions.ts, 73, 54))
}

const ts1 = ff2('foo', 'bar');
>ts1 : Symbol(ts1, Decl(constAssertions.ts, 77, 5))
>ff2 : Symbol(ff2, Decl(constAssertions.ts, 71, 1))

const ts2 = ff2('foo', !!true ? '0' : '1');
>ts2 : Symbol(ts2, Decl(constAssertions.ts, 78, 5))
>ff2 : Symbol(ff2, Decl(constAssertions.ts, 71, 1))

const ts3 = ff2(!!true ? 'top' : 'bottom', !!true ? 'left' : 'right');
>ts3 : Symbol(ts3, Decl(constAssertions.ts, 79, 5))
>ff2 : Symbol(ff2, Decl(constAssertions.ts, 71, 1))

function ff3(x: 'foo' | 'bar', y: object) {
>ff3 : Symbol(ff3, Decl(constAssertions.ts, 79, 70))
>x : Symbol(x, Decl(constAssertions.ts, 81, 13))
>y : Symbol(y, Decl(constAssertions.ts, 81, 30))

return `${x}${y}` as const;
>x : Symbol(x, Decl(constAssertions.ts, 81, 13))
>y : Symbol(y, Decl(constAssertions.ts, 81, 30))
}

type Action = "verify" | "write";
>Action : Symbol(Action, Decl(constAssertions.ts, 83, 1))

type ContentMatch = "match" | "nonMatch";
>ContentMatch : Symbol(ContentMatch, Decl(constAssertions.ts, 85, 33))

type Outcome = `${Action}_${ContentMatch}`;
>Outcome : Symbol(Outcome, Decl(constAssertions.ts, 86, 41))
>Action : Symbol(Action, Decl(constAssertions.ts, 83, 1))
>ContentMatch : Symbol(ContentMatch, Decl(constAssertions.ts, 85, 33))

function ff4(verify: boolean, contentMatches: boolean) {
>ff4 : Symbol(ff4, Decl(constAssertions.ts, 87, 43))
>verify : Symbol(verify, Decl(constAssertions.ts, 89, 13))
>contentMatches : Symbol(contentMatches, Decl(constAssertions.ts, 89, 29))

const action : Action = verify ? `verify` : `write`;
>action : Symbol(action, Decl(constAssertions.ts, 90, 9))
>Action : Symbol(Action, Decl(constAssertions.ts, 83, 1))
>verify : Symbol(verify, Decl(constAssertions.ts, 89, 13))

const contentMatch: ContentMatch = contentMatches ? `match` : `nonMatch`;
>contentMatch : Symbol(contentMatch, Decl(constAssertions.ts, 91, 9))
>ContentMatch : Symbol(ContentMatch, Decl(constAssertions.ts, 85, 33))
>contentMatches : Symbol(contentMatches, Decl(constAssertions.ts, 89, 29))

const outcome: Outcome = `${action}_${contentMatch}` as const;
>outcome : Symbol(outcome, Decl(constAssertions.ts, 92, 9))
>Outcome : Symbol(Outcome, Decl(constAssertions.ts, 86, 41))
>action : Symbol(action, Decl(constAssertions.ts, 90, 9))
>contentMatch : Symbol(contentMatch, Decl(constAssertions.ts, 91, 9))

return outcome;
>outcome : Symbol(outcome, Decl(constAssertions.ts, 92, 9))
}

function ff5(verify: boolean, contentMatches: boolean) {
>ff5 : Symbol(ff5, Decl(constAssertions.ts, 94, 1))
>verify : Symbol(verify, Decl(constAssertions.ts, 96, 13))
>contentMatches : Symbol(contentMatches, Decl(constAssertions.ts, 96, 29))

const action = verify ? `verify` : `write`;
>action : Symbol(action, Decl(constAssertions.ts, 97, 9))
>verify : Symbol(verify, Decl(constAssertions.ts, 96, 13))

const contentMatch = contentMatches ? `match` : `nonMatch`;
>contentMatch : Symbol(contentMatch, Decl(constAssertions.ts, 98, 9))
>contentMatches : Symbol(contentMatches, Decl(constAssertions.ts, 96, 29))

const outcome = `${action}_${contentMatch}` as const;
>outcome : Symbol(outcome, Decl(constAssertions.ts, 99, 9))
>action : Symbol(action, Decl(constAssertions.ts, 97, 9))
>contentMatch : Symbol(contentMatch, Decl(constAssertions.ts, 98, 9))

return outcome;
>outcome : Symbol(outcome, Decl(constAssertions.ts, 99, 9))
}

function accessorNames<S extends string>(propName: S) {
>accessorNames : Symbol(accessorNames, Decl(constAssertions.ts, 101, 1))
>S : Symbol(S, Decl(constAssertions.ts, 103, 23))
>propName : Symbol(propName, Decl(constAssertions.ts, 103, 41))
>S : Symbol(S, Decl(constAssertions.ts, 103, 23))

return [`get-${propName}`, `set-${propName}`] as const;
>propName : Symbol(propName, Decl(constAssertions.ts, 103, 41))
>propName : Symbol(propName, Decl(constAssertions.ts, 103, 41))
}

const ns1 = accessorNames('foo');
>ns1 : Symbol(ns1, Decl(constAssertions.ts, 107, 5))
>accessorNames : Symbol(accessorNames, Decl(constAssertions.ts, 101, 1))

Loading

0 comments on commit 5d6cce5

Please sign in to comment.