Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More specific TemplateStringsArray type for tagged templates #49552

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 109 additions & 21 deletions src/compiler/checker.ts

Large diffs are not rendered by default.

24 changes: 2 additions & 22 deletions src/compiler/transformers/taggedTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
CallExpression,
Debug,
Expression,
getSourceTextOfNodeFromSourceFile,
getRawTextOfTemplateLiteralLike,
hasInvalidEscape,
Identifier,
isExpression,
Expand All @@ -12,7 +12,6 @@ import {
NoSubstitutionTemplateLiteral,
setTextRange,
SourceFile,
SyntaxKind,
TaggedTemplateExpression,
TemplateHead,
TemplateLiteralLikeNode,
Expand Down Expand Up @@ -108,25 +107,6 @@ function createTemplateCooked(factory: NodeFactory, template: TemplateHead | Tem
* @param node The ES6 template literal.
*/
function getRawLiteral(factory: NodeFactory, node: TemplateLiteralLikeNode, currentSourceFile: SourceFile) {
// Find original source text, since we need to emit the raw strings of the tagged template.
// The raw strings contain the (escaped) strings of what the user wrote.
// Examples: `\n` is converted to "\\n", a template string with a newline to "\n".
let text = node.rawText;
if (text === undefined) {
Debug.assertIsDefined(currentSourceFile, "Template literal node is missing 'rawText' and does not have a source file. Possibly bad transform.");
text = getSourceTextOfNodeFromSourceFile(currentSourceFile, node);

// text contains the original source, it will also contain quotes ("`"), dolar signs and braces ("${" and "}"),
// thus we need to remove those characters.
// First template piece starts with "`", others with "}"
// Last template piece ends with "`", others with "${"
const isLast = node.kind === SyntaxKind.NoSubstitutionTemplateLiteral || node.kind === SyntaxKind.TemplateTail;
text = text.substring(1, text.length - (isLast ? 1 : 2));
}

// Newline normalization:
// ES6 Spec 11.8.6.1 - Static Semantics of TV's and TRV's
// <CR><LF> and <CR> LineTerminatorSequences are normalized to <LF> for both TV and TRV.
text = text.replace(/\r\n?/g, "\n");
const text = getRawTextOfTemplateLiteralLike(node, currentSourceFile);
return setTextRange(factory.createStringLiteral(text), node);
}
24 changes: 24 additions & 0 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1719,6 +1719,30 @@ export function getLiteralText(node: LiteralLikeNode, sourceFile: SourceFile | u
return Debug.fail(`Literal kind '${node.kind}' not accounted for.`);
}

/** @internal */
export function getRawTextOfTemplateLiteralLike(node: TemplateLiteralLikeNode, sourceFile: SourceFile) {
// Find original source text, since we need to emit the raw strings of the tagged template.
// The raw strings contain the (escaped) strings of what the user wrote.
// Examples: `\n` is converted to "\\n", a template string with a newline to "\n".
let text = node.rawText;
if (text === undefined) {
Debug.assertIsDefined(sourceFile, "Template literal node is missing 'rawText' and does not have a source file. Possibly bad transform.");
text = getSourceTextOfNodeFromSourceFile(sourceFile, node);

// text contains the original source, it will also contain quotes ("`"), dolar signs and braces ("${" and "}"),
// thus we need to remove those characters.
// First template piece starts with "`", others with "}"
// Last template piece ends with "`", others with "${"
const isLast = node.kind === SyntaxKind.NoSubstitutionTemplateLiteral || node.kind === SyntaxKind.TemplateTail;
text = text.substring(1, text.length - (isLast ? 1 : 2));
}

// Newline normalization:
// ES6 Spec 11.8.6.1 - Static Semantics of TV's and TRV's
// <CR><LF> and <CR> LineTerminatorSequences are normalized to <LF> for both TV and TRV.
return text.replace(/\r\n?/g, "\n");
}

function canUseOriginalText(node: LiteralLikeNode, flags: GetLiteralTextFlags): boolean {
if (nodeIsSynthesized(node) || !node.parent || (flags & GetLiteralTextFlags.TerminateUnterminatedLiterals && node.isUnterminated)) {
return false;
Expand Down
1 change: 1 addition & 0 deletions src/harness/fourslashInterfaceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1132,6 +1132,7 @@ export namespace Completion {
varEntry("Number"),
interfaceEntry("NumberConstructor"),
interfaceEntry("TemplateStringsArray"),
typeEntry("TemplateStringsArrayOf"),
interfaceEntry("ImportMeta"),
interfaceEntry("ImportCallOptions"),
deprecatedInterfaceEntry("ImportAssertions"),
Expand Down
2 changes: 2 additions & 0 deletions src/lib/es5.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,8 @@ interface TemplateStringsArray extends ReadonlyArray<string> {
readonly raw: readonly string[];
}

type TemplateStringsArrayOf<Cooked extends readonly string[], Raw extends readonly string[] = Cooked> = Cooked & { readonly raw: Raw };

/**
* The type of `import.meta`.
*
Expand Down
8 changes: 4 additions & 4 deletions tests/baselines/reference/importHelpers.types
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ function id<T>(x: T) {
}

export const result = id`hello world`;
>result : TemplateStringsArray
>id`hello world` : TemplateStringsArray
>result : TemplateStringsArrayOf<readonly ["hello world"], readonly ["hello world"]>
>id`hello world` : TemplateStringsArrayOf<readonly ["hello world"], readonly ["hello world"]>
>id : <T>(x: T) => T
>`hello world` : "hello world"

Expand Down Expand Up @@ -71,8 +71,8 @@ function id<T>(x: T) {
}

const result = id`hello world`;
>result : TemplateStringsArray
>id`hello world` : TemplateStringsArray
>result : TemplateStringsArrayOf<readonly ["hello world"], readonly ["hello world"]>
>id`hello world` : TemplateStringsArrayOf<readonly ["hello world"], readonly ["hello world"]>
>id : <T>(x: T) => T
>`hello world` : "hello world"

Expand Down
5 changes: 5 additions & 0 deletions tests/baselines/reference/inferTypes1.errors.txt
Original file line number Diff line number Diff line change
Expand Up @@ -246,4 +246,9 @@ inferTypes1.ts(153,40): error TS2322: Type 'T' is not assignable to type 'string
const result = invoker('test', true)({ test: (a: boolean) => 123 })

type Foo2<A extends any[]> = ReturnType<(...args: A) => string>;

// Infer from an intersected tuple
type Head<T extends string[]> = T extends [infer THead, ...infer _] ? THead : never;
type T100 = Head<["a", "c"] & { foo: "bar" }>; // "a"
type T101 = Head<["a", "c"] & readonly ["a", "c"]>; // "a" | "c"

5 changes: 5 additions & 0 deletions tests/baselines/reference/inferTypes1.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,11 @@ function invoker <K extends string | number | symbol, A extends any[]> (key: K,
const result = invoker('test', true)({ test: (a: boolean) => 123 })

type Foo2<A extends any[]> = ReturnType<(...args: A) => string>;

// Infer from an intersected tuple
type Head<T extends string[]> = T extends [infer THead, ...infer _] ? THead : never;
type T100 = Head<["a", "c"] & { foo: "bar" }>; // "a"
type T101 = Head<["a", "c"] & readonly ["a", "c"]>; // "a" | "c"


//// [inferTypes1.js]
Expand Down
18 changes: 18 additions & 0 deletions tests/baselines/reference/inferTypes1.symbols
Original file line number Diff line number Diff line change
Expand Up @@ -777,3 +777,21 @@ type Foo2<A extends any[]> = ReturnType<(...args: A) => string>;
>args : Symbol(args, Decl(inferTypes1.ts, 185, 41))
>A : Symbol(A, Decl(inferTypes1.ts, 185, 10))

// Infer from an intersected tuple
type Head<T extends string[]> = T extends [infer THead, ...infer _] ? THead : never;
>Head : Symbol(Head, Decl(inferTypes1.ts, 185, 64))
>T : Symbol(T, Decl(inferTypes1.ts, 188, 10))
>T : Symbol(T, Decl(inferTypes1.ts, 188, 10))
>THead : Symbol(THead, Decl(inferTypes1.ts, 188, 48))
>_ : Symbol(_, Decl(inferTypes1.ts, 188, 64))
>THead : Symbol(THead, Decl(inferTypes1.ts, 188, 48))

type T100 = Head<["a", "c"] & { foo: "bar" }>; // "a"
>T100 : Symbol(T100, Decl(inferTypes1.ts, 188, 84))
>Head : Symbol(Head, Decl(inferTypes1.ts, 185, 64))
>foo : Symbol(foo, Decl(inferTypes1.ts, 189, 31))

type T101 = Head<["a", "c"] & readonly ["a", "c"]>; // "a" | "c"
>T101 : Symbol(T101, Decl(inferTypes1.ts, 189, 46))
>Head : Symbol(Head, Decl(inferTypes1.ts, 185, 64))

11 changes: 11 additions & 0 deletions tests/baselines/reference/inferTypes1.types
Original file line number Diff line number Diff line change
Expand Up @@ -493,3 +493,14 @@ type Foo2<A extends any[]> = ReturnType<(...args: A) => string>;
>Foo2 : string
>args : A

// Infer from an intersected tuple
type Head<T extends string[]> = T extends [infer THead, ...infer _] ? THead : never;
>Head : Head<T>

type T100 = Head<["a", "c"] & { foo: "bar" }>; // "a"
>T100 : "a"
>foo : "bar"

type T101 = Head<["a", "c"] & readonly ["a", "c"]>; // "a" | "c"
>T101 : "a" | "c"

Loading
Loading