Skip to content

feat(33304): Better type-safety with tagged template literals #45310

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

Closed
wants to merge 13 commits into from
Closed
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
34 changes: 30 additions & 4 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
@@ -920,7 +920,7 @@ namespace ts {
let deferredGlobalAsyncIteratorType: GenericType;
let deferredGlobalAsyncIterableIteratorType: GenericType;
let deferredGlobalAsyncGeneratorType: GenericType;
let deferredGlobalTemplateStringsArrayType: ObjectType;
let deferredGlobalTemplateStringsArraySymbol: Symbol;
let deferredGlobalImportMetaType: ObjectType;
let deferredGlobalImportMetaExpressionType: ObjectType;
let deferredGlobalExtractSymbol: Symbol;
@@ -13336,8 +13336,8 @@ namespace ts {
return deferredGlobalTypedPropertyDescriptorType || (deferredGlobalTypedPropertyDescriptorType = getGlobalType("TypedPropertyDescriptor" as __String, /*arity*/ 1, /*reportErrors*/ true)) || emptyGenericType;
}

function getGlobalTemplateStringsArrayType() {
return deferredGlobalTemplateStringsArrayType || (deferredGlobalTemplateStringsArrayType = getGlobalType("TemplateStringsArray" as __String, /*arity*/ 0, /*reportErrors*/ true)) || emptyObjectType;
function getGlobalTemplateStringsArraySymbol() {
return deferredGlobalTemplateStringsArraySymbol || (deferredGlobalTemplateStringsArraySymbol = getGlobalSymbol("TemplateStringsArray" as __String, SymbolFlags.TypeAlias, Diagnostics.Cannot_find_global_type_0)!); // TODO: GH#18217
}

function getGlobalImportMetaType() {
@@ -28833,12 +28833,38 @@ namespace ts {
function getEffectiveCallArguments(node: CallLikeExpression): readonly Expression[] {
if (node.kind === SyntaxKind.TaggedTemplateExpression) {
const template = node.template;
const args: Expression[] = [createSyntheticExpression(template, getGlobalTemplateStringsArrayType())];
const args: Expression[] = [];

let templateStringsArrayTypeParameter: Type;
let templateStringsArrayRawTypeParameter: Type;

if (template.kind === SyntaxKind.TemplateExpression) {
const templateStringParts: Type[] = [getStringLiteralType(template.head.text)];
const templateStringPartsRaw: Type[] = [getStringLiteralType(template.head.rawText ?? template.head.text)];
const flags: ElementFlags[] = [ElementFlags.Required];
forEach(template.templateSpans, span => {
args.push(span.expression);
templateStringParts.push(getStringLiteralType(span.literal.text));
templateStringPartsRaw.push(getStringLiteralType(span.literal.rawText ?? span.literal.text));
flags.push(ElementFlags.Required);
});
templateStringsArrayTypeParameter = createTupleType(
templateStringParts, flags);
templateStringsArrayRawTypeParameter = createTupleType(
templateStringPartsRaw, flags);
}
else /* if (template.kind === SyntaxKind.NoSubstitutionTemplateLiteral) */ {
templateStringsArrayTypeParameter = createTupleType(
[getStringLiteralType(template.text)], [ElementFlags.Required]);
templateStringsArrayRawTypeParameter = createTupleType(
[getStringLiteralType(template.rawText ?? template.text)], [ElementFlags.Required]);
}

const templateStringsArray = getGlobalTemplateStringsArraySymbol();
const expr = createSyntheticExpression(template,
getTypeAliasInstantiation(templateStringsArray, [templateStringsArrayTypeParameter, templateStringsArrayRawTypeParameter]));
args.unshift(expr);

return args;
}
if (node.kind === SyntaxKind.Decorator) {
2 changes: 1 addition & 1 deletion src/harness/fourslashInterfaceImpl.ts
Original file line number Diff line number Diff line change
@@ -1043,7 +1043,7 @@ namespace FourSlashInterface {
interfaceEntry("BooleanConstructor"),
varEntry("Number"),
interfaceEntry("NumberConstructor"),
interfaceEntry("TemplateStringsArray"),
typeEntry("TemplateStringsArray"),
interfaceEntry("ImportMeta"),
varEntry("Math"),
varEntry("Date"),
7 changes: 4 additions & 3 deletions src/lib/es5.d.ts
Original file line number Diff line number Diff line change
@@ -581,9 +581,10 @@ interface NumberConstructor {
/** An object that represents a number of any kind. All JavaScript numbers are 64-bit floating-point numbers. */
declare var Number: NumberConstructor;

interface TemplateStringsArray extends ReadonlyArray<string> {
readonly raw: readonly string[];
}
type TemplateStringsArray<
TParts extends readonly string[] = readonly string[],
TRawParts extends readonly string[] = readonly string[],
> = TParts & {readonly raw: TRawParts};

/**
* The type of `import.meta`.
Original file line number Diff line number Diff line change
@@ -39,7 +39,7 @@ export interface StyledComponentBase<TTag extends string, TTheme = DefaultTheme,
export interface StyledInterface {
div: (a: TemplateStringsArray) => StyledComponent<"div">;
>div : (a: TemplateStringsArray) => StyledComponent<"div">
>a : TemplateStringsArray
>a : TemplateStringsArray<readonly string[], readonly string[]>
}

declare const styled: StyledInterface;
@@ -55,25 +55,25 @@ import styled from "styled-components";
const A = styled.div``;
>A : import("tests/cases/compiler/node_modules/styled-components/index").StyledComponent<"div", import("tests/cases/compiler/node_modules/styled-components/index").DefaultTheme, {}, never>
>styled.div`` : import("tests/cases/compiler/node_modules/styled-components/index").StyledComponent<"div", import("tests/cases/compiler/node_modules/styled-components/index").DefaultTheme, {}, never>
>styled.div : (a: TemplateStringsArray) => import("tests/cases/compiler/node_modules/styled-components/index").StyledComponent<"div", import("tests/cases/compiler/node_modules/styled-components/index").DefaultTheme, {}, never>
>styled.div : (a: TemplateStringsArray<readonly string[], readonly string[]>) => import("tests/cases/compiler/node_modules/styled-components/index").StyledComponent<"div", import("tests/cases/compiler/node_modules/styled-components/index").DefaultTheme, {}, never>
>styled : import("tests/cases/compiler/node_modules/styled-components/index").StyledInterface
>div : (a: TemplateStringsArray) => import("tests/cases/compiler/node_modules/styled-components/index").StyledComponent<"div", import("tests/cases/compiler/node_modules/styled-components/index").DefaultTheme, {}, never>
>div : (a: TemplateStringsArray<readonly string[], readonly string[]>) => import("tests/cases/compiler/node_modules/styled-components/index").StyledComponent<"div", import("tests/cases/compiler/node_modules/styled-components/index").DefaultTheme, {}, never>
>`` : ""

const B = styled.div``;
>B : import("tests/cases/compiler/node_modules/styled-components/index").StyledComponent<"div", import("tests/cases/compiler/node_modules/styled-components/index").DefaultTheme, {}, never>
>styled.div`` : import("tests/cases/compiler/node_modules/styled-components/index").StyledComponent<"div", import("tests/cases/compiler/node_modules/styled-components/index").DefaultTheme, {}, never>
>styled.div : (a: TemplateStringsArray) => import("tests/cases/compiler/node_modules/styled-components/index").StyledComponent<"div", import("tests/cases/compiler/node_modules/styled-components/index").DefaultTheme, {}, never>
>styled.div : (a: TemplateStringsArray<readonly string[], readonly string[]>) => import("tests/cases/compiler/node_modules/styled-components/index").StyledComponent<"div", import("tests/cases/compiler/node_modules/styled-components/index").DefaultTheme, {}, never>
>styled : import("tests/cases/compiler/node_modules/styled-components/index").StyledInterface
>div : (a: TemplateStringsArray) => import("tests/cases/compiler/node_modules/styled-components/index").StyledComponent<"div", import("tests/cases/compiler/node_modules/styled-components/index").DefaultTheme, {}, never>
>div : (a: TemplateStringsArray<readonly string[], readonly string[]>) => import("tests/cases/compiler/node_modules/styled-components/index").StyledComponent<"div", import("tests/cases/compiler/node_modules/styled-components/index").DefaultTheme, {}, never>
>`` : ""

export const C = styled.div``;
>C : import("tests/cases/compiler/node_modules/styled-components/index").StyledComponent<"div", import("tests/cases/compiler/node_modules/styled-components/index").DefaultTheme, {}, never>
>styled.div`` : import("tests/cases/compiler/node_modules/styled-components/index").StyledComponent<"div", import("tests/cases/compiler/node_modules/styled-components/index").DefaultTheme, {}, never>
>styled.div : (a: TemplateStringsArray) => import("tests/cases/compiler/node_modules/styled-components/index").StyledComponent<"div", import("tests/cases/compiler/node_modules/styled-components/index").DefaultTheme, {}, never>
>styled.div : (a: TemplateStringsArray<readonly string[], readonly string[]>) => import("tests/cases/compiler/node_modules/styled-components/index").StyledComponent<"div", import("tests/cases/compiler/node_modules/styled-components/index").DefaultTheme, {}, never>
>styled : import("tests/cases/compiler/node_modules/styled-components/index").StyledInterface
>div : (a: TemplateStringsArray) => import("tests/cases/compiler/node_modules/styled-components/index").StyledComponent<"div", import("tests/cases/compiler/node_modules/styled-components/index").DefaultTheme, {}, never>
>div : (a: TemplateStringsArray<readonly string[], readonly string[]>) => import("tests/cases/compiler/node_modules/styled-components/index").StyledComponent<"div", import("tests/cases/compiler/node_modules/styled-components/index").DefaultTheme, {}, never>
>`` : ""

export default Object.assign(A, {
Original file line number Diff line number Diff line change
@@ -41,7 +41,7 @@ tests/cases/conformance/es6/destructuring/destructuringParameterDeclaration4.ts(
a1(...array2); // Error parameter type is (number|string)[]
~~~~~~
!!! error TS2552: Cannot find name 'array2'. Did you mean 'Array'?
!!! related TS2728 /.ts/lib.es5.d.ts:1415:13: 'Array' is declared here.
!!! related TS2728 /.ts/lib.es5.d.ts:1416:13: 'Array' is declared here.
a5([1, 2, "string", false, true]); // Error, parameter type is [any, any, [[any]]]
~~~~~~~~
!!! error TS2322: Type 'string' is not assignable to type '[[any]]'.
4 changes: 2 additions & 2 deletions tests/baselines/reference/destructuringTuple.errors.txt
Original file line number Diff line number Diff line change
@@ -33,8 +33,8 @@ tests/cases/compiler/destructuringTuple.ts(11,60): error TS2769: No overload mat
!!! error TS2769: Overload 2 of 3, '(callbackfn: (previousValue: [], currentValue: number, currentIndex: number, array: number[]) => [], initialValue: []): []', gave the following error.
!!! error TS2769: Type 'never[]' is not assignable to type '[]'.
!!! error TS2769: Target allows only 0 element(s) but source may have more.
!!! related TS6502 /.ts/lib.es5.d.ts:1380:24: The expected type comes from the return type of this signature.
!!! related TS6502 /.ts/lib.es5.d.ts:1386:27: The expected type comes from the return type of this signature.
!!! related TS6502 /.ts/lib.es5.d.ts:1381:24: The expected type comes from the return type of this signature.
!!! related TS6502 /.ts/lib.es5.d.ts:1387:27: The expected type comes from the return type of this signature.
~~
!!! error TS2769: No overload matches this call.
!!! error TS2769: Overload 1 of 2, '(...items: ConcatArray<never>[]): never[]', gave the following error.
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@ tests/cases/conformance/types/members/duplicateNumericIndexers.ts(25,5): error T
tests/cases/conformance/types/members/duplicateNumericIndexers.ts(29,5): error TS2374: Duplicate index signature for type 'number'.
tests/cases/conformance/types/members/duplicateNumericIndexers.ts(30,5): error TS2374: Duplicate index signature for type 'number'.
lib.es5.d.ts(514,5): error TS2374: Duplicate index signature for type 'number'.
lib.es5.d.ts(1401,5): error TS2374: Duplicate index signature for type 'number'.
lib.es5.d.ts(1402,5): error TS2374: Duplicate index signature for type 'number'.


==== tests/cases/conformance/types/members/duplicateNumericIndexers.ts (12 errors) ====
8 changes: 4 additions & 4 deletions tests/baselines/reference/externModule.errors.txt
Original file line number Diff line number Diff line change
@@ -66,20 +66,20 @@ tests/cases/compiler/externModule.ts(37,3): error TS2552: Cannot find name 'XDat
var d=new XDate();
~~~~~
!!! error TS2552: Cannot find name 'XDate'. Did you mean 'Date'?
!!! related TS2728 /.ts/lib.es5.d.ts:907:13: 'Date' is declared here.
!!! related TS2728 /.ts/lib.es5.d.ts:908:13: 'Date' is declared here.
d.getDay();
d=new XDate(1978,2);
~~~~~
!!! error TS2552: Cannot find name 'XDate'. Did you mean 'Date'?
!!! related TS2728 /.ts/lib.es5.d.ts:907:13: 'Date' is declared here.
!!! related TS2728 /.ts/lib.es5.d.ts:908:13: 'Date' is declared here.
d.getXDate();
var n=XDate.parse("3/2/2004");
~~~~~
!!! error TS2552: Cannot find name 'XDate'. Did you mean 'Date'?
!!! related TS2728 /.ts/lib.es5.d.ts:907:13: 'Date' is declared here.
!!! related TS2728 /.ts/lib.es5.d.ts:908:13: 'Date' is declared here.
n=XDate.UTC(1964,2,1);
~~~~~
!!! error TS2552: Cannot find name 'XDate'. Did you mean 'Date'?
!!! related TS2728 /.ts/lib.es5.d.ts:907:13: 'Date' is declared here.
!!! related TS2728 /.ts/lib.es5.d.ts:908:13: 'Date' is declared here.


Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
=== tests/cases/compiler/genericTemplateOverloadResolution.ts ===
interface IFooFn {
(strings: TemplateStringsArray): Promise<{}>;
>strings : TemplateStringsArray
>strings : TemplateStringsArray<readonly string[], readonly string[]>

<T>(strings: TemplateStringsArray): Promise<T>;
>strings : TemplateStringsArray
>strings : TemplateStringsArray<readonly string[], readonly string[]>
}

declare const fooFn: IFooFn;
8 changes: 4 additions & 4 deletions tests/baselines/reference/importHelpers.types
Original file line number Diff line number Diff line change
@@ -31,8 +31,8 @@ function id<T>(x: T) {
}

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

@@ -69,8 +69,8 @@ function id<T>(x: T) {
}

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

Original file line number Diff line number Diff line change
@@ -7,5 +7,5 @@ tests/cases/compiler/mappedTypeWithAsClauseAndLateBoundProperty.ts(3,1): error T
tgt2 = src2; // Should error
~~~~
!!! error TS2741: Property 'length' is missing in type '{ [x: number]: number; toString: () => string; toLocaleString: () => string; pop: () => number; push: (...items: number[]) => number; concat: { (...items: ConcatArray<number>[]): number[]; (...items: (number | ConcatArray<number>)[]): number[]; }; join: (separator?: string) => string; reverse: () => number[]; shift: () => number; slice: (start?: number, end?: number) => number[]; sort: (compareFn?: (a: number, b: number) => number) => number[]; splice: { (start: number, deleteCount?: number): number[]; (start: number, deleteCount: number, ...items: number[]): number[]; }; unshift: (...items: number[]) => number; indexOf: (searchElement: number, fromIndex?: number) => number; lastIndexOf: (searchElement: number, fromIndex?: number) => number; every: { <S extends number>(predicate: (value: number, index: number, array: number[]) => value is S, thisArg?: any): this is S[]; (predicate: (value: number, index: number, array: number[]) => unknown, thisArg?: any): boolean; }; some: (predicate: (value: number, index: number, array: number[]) => unknown, thisArg?: any) => boolean; forEach: (callbackfn: (value: number, index: number, array: number[]) => void, thisArg?: any) => void; map: <U>(callbackfn: (value: number, index: number, array: number[]) => U, thisArg?: any) => U[]; filter: { <S extends number>(predicate: (value: number, index: number, array: number[]) => value is S, thisArg?: any): S[]; (predicate: (value: number, index: number, array: number[]) => unknown, thisArg?: any): number[]; }; reduce: { (callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number): number; (callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number, initialValue: number): number; <U>(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: number[]) => U, initialValue: U): U; }; reduceRight: { (callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number): number; (callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number, initialValue: number): number; <U>(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: number[]) => U, initialValue: U): U; }; find: { <S extends number>(predicate: (this: void, value: number, index: number, obj: number[]) => value is S, thisArg?: any): S; (predicate: (value: number, index: number, obj: number[]) => unknown, thisArg?: any): number; }; findIndex: (predicate: (value: number, index: number, obj: number[]) => unknown, thisArg?: any) => number; fill: (value: number, start?: number, end?: number) => number[]; copyWithin: (target: number, start: number, end?: number) => number[]; entries: () => IterableIterator<[number, number]>; keys: () => IterableIterator<number>; values: () => IterableIterator<number>; includes: (searchElement: number, fromIndex?: number) => boolean; flatMap: <U, This = undefined>(callback: (this: This, value: number, index: number, array: number[]) => U | readonly U[], thisArg?: This) => U[]; flat: <A, D extends number = 1>(this: A, depth?: D) => FlatArray<A, D>[]; [iterator]: () => IterableIterator<number>; [unscopables]: () => { copyWithin: boolean; entries: boolean; fill: boolean; find: boolean; findIndex: boolean; keys: boolean; values: boolean; }; }' but required in type 'number[]'.
!!! related TS2728 /.ts/lib.es5.d.ts:1224:5: 'length' is declared here.
!!! related TS2728 /.ts/lib.es5.d.ts:1225:5: 'length' is declared here.

Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ tests/cases/conformance/types/any/narrowExceptionVariableInCatchClause.ts(16,17)
err.massage; // ERROR: Property 'massage' does not exist on type 'Error'
~~~~~~~
!!! error TS2551: Property 'massage' does not exist on type 'Error'. Did you mean 'message'?
!!! related TS2728 /.ts/lib.es5.d.ts:974:5: 'message' is declared here.
!!! related TS2728 /.ts/lib.es5.d.ts:975:5: 'message' is declared here.
}

else {
Original file line number Diff line number Diff line change
@@ -22,14 +22,14 @@ tests/cases/conformance/types/any/narrowFromAnyWithInstanceof.ts(22,7): error TS
x.mesage;
~~~~~~
!!! error TS2551: Property 'mesage' does not exist on type 'Error'. Did you mean 'message'?
!!! related TS2728 /.ts/lib.es5.d.ts:974:5: 'message' is declared here.
!!! related TS2728 /.ts/lib.es5.d.ts:975:5: 'message' is declared here.
}

if (x instanceof Date) {
x.getDate();
x.getHuors();
~~~~~~~~
!!! error TS2551: Property 'getHuors' does not exist on type 'Date'. Did you mean 'getHours'?
!!! related TS2728 /.ts/lib.es5.d.ts:763:5: 'getHours' is declared here.
!!! related TS2728 /.ts/lib.es5.d.ts:764:5: 'getHours' is declared here.
}

Loading