Skip to content

Commit 70fdb52

Browse files
committed
More specific TemplateStringsArray type for tagged templates
1 parent 3fc5f96 commit 70fdb52

35 files changed

+567
-104
lines changed

src/compiler/checker.ts

+92-18
Original file line numberDiff line numberDiff line change
@@ -971,6 +971,7 @@ namespace ts {
971971
let deferredGlobalAsyncIterableIteratorType: GenericType | undefined;
972972
let deferredGlobalAsyncGeneratorType: GenericType | undefined;
973973
let deferredGlobalTemplateStringsArrayType: ObjectType | undefined;
974+
let deferredGlobalTemplateStringsArrayOfSymbol: Symbol | undefined;
974975
let deferredGlobalImportMetaType: ObjectType;
975976
let deferredGlobalImportMetaExpressionType: ObjectType;
976977
let deferredGlobalImportCallOptionsType: ObjectType | undefined;
@@ -14102,6 +14103,11 @@ namespace ts {
1410214103
return (deferredGlobalBigIntType ||= getGlobalType("BigInt" as __String, /*arity*/ 0, /*reportErrors*/ false)) || emptyObjectType;
1410314104
}
1410414105

14106+
function getGlobalTemplateStringsArrayOfSymbol(): Symbol | undefined {
14107+
deferredGlobalTemplateStringsArrayOfSymbol ||= getGlobalTypeAliasSymbol("TemplateStringsArrayOf" as __String, /*arity*/ 2, /*reportErrors*/ true) || unknownSymbol;
14108+
return deferredGlobalTemplateStringsArrayOfSymbol === unknownSymbol ? undefined : deferredGlobalTemplateStringsArrayOfSymbol;
14109+
}
14110+
1410514111
/**
1410614112
* Instantiates a global type that is generic with some element type, and returns that instantiation.
1410714113
*/
@@ -21171,6 +21177,43 @@ namespace ts {
2117121177
return isArrayType(type) || !(type.flags & TypeFlags.Nullable) && isTypeAssignableTo(type, anyReadonlyArrayType);
2117221178
}
2117321179

21180+
/**
21181+
* Returns `type` if it is an array or tuple type. If `type` is an intersection type,
21182+
* returns the rightmost constituent that is an array or tuple type, but only if there are no
21183+
* other constituents to that contain properties that overlap with array- or tuple- specific
21184+
* members (i.e., index signatures, numeric string property names, or `length`).
21185+
*/
21186+
function tryGetNonShadowedArrayOrTupleType(type: Type) {
21187+
if (isArrayOrTupleType(type)) {
21188+
return type;
21189+
}
21190+
21191+
if (!(type.flags & TypeFlags.Intersection)) {
21192+
return undefined;
21193+
}
21194+
21195+
let arrayOrTupleConstituent: TypeReference | undefined;
21196+
for (const constituent of (type as IntersectionType).types) {
21197+
if (isArrayOrTupleType(constituent)) {
21198+
arrayOrTupleConstituent = constituent;
21199+
}
21200+
else {
21201+
const properties = getPropertiesOfType(constituent);
21202+
for (const property of properties) {
21203+
if (isNumericLiteralName(property.escapedName) || property.escapedName === "length" as __String) {
21204+
return undefined;
21205+
}
21206+
}
21207+
21208+
if (some(getIndexInfosOfType(constituent))) {
21209+
return undefined;
21210+
}
21211+
}
21212+
}
21213+
21214+
return arrayOrTupleConstituent;
21215+
}
21216+
2117421217
function getSingleBaseForNonAugmentingSubtype(type: Type) {
2117521218
if (!(getObjectFlags(type) & ObjectFlags.Reference) || !(getObjectFlags((type as TypeReference).target) & ObjectFlags.ClassOrInterface)) {
2117621219
return undefined;
@@ -22830,68 +22873,69 @@ namespace ts {
2283022873
}
2283122874
// Infer from the members of source and target only if the two types are possibly related
2283222875
if (!typesDefinitelyUnrelated(source, target)) {
22833-
if (isArrayOrTupleType(source)) {
22876+
const sourceArrayOrTuple = tryGetNonShadowedArrayOrTupleType(source);
22877+
if (sourceArrayOrTuple) {
2283422878
if (isTupleType(target)) {
22835-
const sourceArity = getTypeReferenceArity(source);
22879+
const sourceArity = getTypeReferenceArity(sourceArrayOrTuple);
2283622880
const targetArity = getTypeReferenceArity(target);
2283722881
const elementTypes = getTypeArguments(target);
2283822882
const elementFlags = target.target.elementFlags;
2283922883
// When source and target are tuple types with the same structure (fixed, variadic, and rest are matched
2284022884
// to the same kind in each position), simply infer between the element types.
22841-
if (isTupleType(source) && isTupleTypeStructureMatching(source, target)) {
22885+
if (isTupleType(sourceArrayOrTuple) && isTupleTypeStructureMatching(sourceArrayOrTuple, target)) {
2284222886
for (let i = 0; i < targetArity; i++) {
22843-
inferFromTypes(getTypeArguments(source)[i], elementTypes[i]);
22887+
inferFromTypes(getTypeArguments(sourceArrayOrTuple)[i], elementTypes[i]);
2284422888
}
2284522889
return;
2284622890
}
22847-
const startLength = isTupleType(source) ? Math.min(source.target.fixedLength, target.target.fixedLength) : 0;
22848-
const endLength = Math.min(isTupleType(source) ? getEndElementCount(source.target, ElementFlags.Fixed) : 0,
22891+
const startLength = isTupleType(sourceArrayOrTuple) ? Math.min(sourceArrayOrTuple.target.fixedLength, target.target.fixedLength) : 0;
22892+
const endLength = Math.min(isTupleType(sourceArrayOrTuple) ? getEndElementCount(sourceArrayOrTuple.target, ElementFlags.Fixed) : 0,
2284922893
target.target.hasRestElement ? getEndElementCount(target.target, ElementFlags.Fixed) : 0);
2285022894
// Infer between starting fixed elements.
2285122895
for (let i = 0; i < startLength; i++) {
22852-
inferFromTypes(getTypeArguments(source)[i], elementTypes[i]);
22896+
inferFromTypes(getTypeArguments(sourceArrayOrTuple)[i], elementTypes[i]);
2285322897
}
22854-
if (!isTupleType(source) || sourceArity - startLength - endLength === 1 && source.target.elementFlags[startLength] & ElementFlags.Rest) {
22898+
if (!isTupleType(sourceArrayOrTuple) || sourceArity - startLength - endLength === 1 && sourceArrayOrTuple.target.elementFlags[startLength] & ElementFlags.Rest) {
2285522899
// Single rest element remains in source, infer from that to every element in target
22856-
const restType = getTypeArguments(source)[startLength];
22900+
const restType = getTypeArguments(sourceArrayOrTuple)[startLength];
2285722901
for (let i = startLength; i < targetArity - endLength; i++) {
2285822902
inferFromTypes(elementFlags[i] & ElementFlags.Variadic ? createArrayType(restType) : restType, elementTypes[i]);
2285922903
}
2286022904
}
2286122905
else {
2286222906
const middleLength = targetArity - startLength - endLength;
22863-
if (middleLength === 2 && elementFlags[startLength] & elementFlags[startLength + 1] & ElementFlags.Variadic && isTupleType(source)) {
22907+
if (middleLength === 2 && elementFlags[startLength] & elementFlags[startLength + 1] & ElementFlags.Variadic && isTupleType(sourceArrayOrTuple)) {
2286422908
// Middle of target is [...T, ...U] and source is tuple type
2286522909
const targetInfo = getInferenceInfoForType(elementTypes[startLength]);
2286622910
if (targetInfo && targetInfo.impliedArity !== undefined) {
2286722911
// Infer slices from source based on implied arity of T.
22868-
inferFromTypes(sliceTupleType(source, startLength, endLength + sourceArity - targetInfo.impliedArity), elementTypes[startLength]);
22869-
inferFromTypes(sliceTupleType(source, startLength + targetInfo.impliedArity, endLength), elementTypes[startLength + 1]);
22912+
inferFromTypes(sliceTupleType(sourceArrayOrTuple, startLength, endLength + sourceArity - targetInfo.impliedArity), elementTypes[startLength]);
22913+
inferFromTypes(sliceTupleType(sourceArrayOrTuple, startLength + targetInfo.impliedArity, endLength), elementTypes[startLength + 1]);
2287022914
}
2287122915
}
2287222916
else if (middleLength === 1 && elementFlags[startLength] & ElementFlags.Variadic) {
2287322917
// Middle of target is exactly one variadic element. Infer the slice between the fixed parts in the source.
2287422918
// If target ends in optional element(s), make a lower priority a speculative inference.
2287522919
const endsInOptional = target.target.elementFlags[targetArity - 1] & ElementFlags.Optional;
22876-
const sourceSlice = isTupleType(source) ? sliceTupleType(source, startLength, endLength) : createArrayType(getTypeArguments(source)[0]);
22920+
const sourceSlice = isTupleType(sourceArrayOrTuple) ? sliceTupleType(sourceArrayOrTuple, startLength, endLength) : createArrayType(getTypeArguments(sourceArrayOrTuple)[0]);
2287722921
inferWithPriority(sourceSlice, elementTypes[startLength], endsInOptional ? InferencePriority.SpeculativeTuple : 0);
2287822922
}
2287922923
else if (middleLength === 1 && elementFlags[startLength] & ElementFlags.Rest) {
2288022924
// Middle of target is exactly one rest element. If middle of source is not empty, infer union of middle element types.
22881-
const restType = isTupleType(source) ? getElementTypeOfSliceOfTupleType(source, startLength, endLength) : getTypeArguments(source)[0];
22925+
const restType = isTupleType(sourceArrayOrTuple) ? getElementTypeOfSliceOfTupleType(sourceArrayOrTuple, startLength, endLength) : getTypeArguments(sourceArrayOrTuple)[0];
2288222926
if (restType) {
2288322927
inferFromTypes(restType, elementTypes[startLength]);
2288422928
}
2288522929
}
2288622930
}
2288722931
// Infer between ending fixed elements
2288822932
for (let i = 0; i < endLength; i++) {
22889-
inferFromTypes(getTypeArguments(source)[sourceArity - i - 1], elementTypes[targetArity - i - 1]);
22933+
inferFromTypes(getTypeArguments(sourceArrayOrTuple)[sourceArity - i - 1], elementTypes[targetArity - i - 1]);
2289022934
}
2289122935
return;
2289222936
}
2289322937
if (isArrayType(target)) {
22894-
inferFromIndexTypes(source, target);
22938+
inferFromIndexTypes(sourceArrayOrTuple, target);
2289522939
return;
2289622940
}
2289722941
}
@@ -27548,7 +27592,37 @@ namespace ts {
2754827592
return checkIteratedTypeOrElementType(IterationUse.Spread, arrayOrIterableType, undefinedType, node.expression);
2754927593
}
2755027594

27595+
function getTemplateStringsArrayOf(cookedTypes: Type[], rawTypes: Type[]) {
27596+
const templateStringsArrayOfAlias = getGlobalTemplateStringsArrayOfSymbol();
27597+
if (!templateStringsArrayOfAlias) return getGlobalTemplateStringsArrayType();
27598+
const cookedType = createTupleType(cookedTypes, /*elementFlags*/ undefined, /*readonly*/ true);
27599+
const rawType = createTupleType(rawTypes, /*elementFlags*/ undefined, /*readonly*/ true);
27600+
return getTypeAliasInstantiation(templateStringsArrayOfAlias, [cookedType, rawType]);
27601+
}
27602+
27603+
function getRawLiteralType(node: TemplateLiteralLikeNode) {
27604+
const text = getRawTextOfTemplateLiteralLike(node, getSourceFileOfNode(node));
27605+
return getStringLiteralType(text);
27606+
}
27607+
2755127608
function checkSyntheticExpression(node: SyntheticExpression): Type {
27609+
if (isTemplateLiteral(node.parent) && node.type === getGlobalTemplateStringsArrayType()) {
27610+
const cookedStrings: Type[] = [];
27611+
const rawStrings: Type[] = [];
27612+
if (isNoSubstitutionTemplateLiteral(node.parent)) {
27613+
cookedStrings.push(getStringLiteralType(node.parent.text));
27614+
rawStrings.push(getRawLiteralType(node.parent));
27615+
}
27616+
else {
27617+
cookedStrings.push(getStringLiteralType(node.parent.head.text));
27618+
rawStrings.push(getRawLiteralType(node.parent.head));
27619+
for (const templateSpan of node.parent.templateSpans) {
27620+
cookedStrings.push(getStringLiteralType(templateSpan.literal.text));
27621+
rawStrings.push(getRawLiteralType(templateSpan.literal));
27622+
}
27623+
}
27624+
return getTemplateStringsArrayOf(cookedStrings, rawStrings);
27625+
}
2755227626
return node.isSpread ? getIndexedAccessType(node.type, numberType) : node.type;
2755327627
}
2755427628

@@ -30587,10 +30661,10 @@ namespace ts {
3058730661
let typeArguments: NodeArray<TypeNode> | undefined;
3058830662

3058930663
if (!isDecorator) {
30590-
typeArguments = (node as CallExpression).typeArguments;
30664+
typeArguments = node.typeArguments;
3059130665

3059230666
// We already perform checking on the type arguments on the class declaration itself.
30593-
if (isTaggedTemplate || isJsxOpeningOrSelfClosingElement || (node as CallExpression).expression.kind !== SyntaxKind.SuperKeyword) {
30667+
if (isTaggedTemplate || isJsxOpeningOrSelfClosingElement || node.expression.kind !== SyntaxKind.SuperKeyword) {
3059430668
forEach(typeArguments, checkSourceElement);
3059530669
}
3059630670
}

src/compiler/transformers/taggedTemplate.ts

+1-21
Original file line numberDiff line numberDiff line change
@@ -76,27 +76,7 @@ namespace ts {
7676
* @param node The ES6 template literal.
7777
*/
7878
function getRawLiteral(node: TemplateLiteralLikeNode, currentSourceFile: SourceFile) {
79-
// Find original source text, since we need to emit the raw strings of the tagged template.
80-
// The raw strings contain the (escaped) strings of what the user wrote.
81-
// Examples: `\n` is converted to "\\n", a template string with a newline to "\n".
82-
let text = node.rawText;
83-
if (text === undefined) {
84-
Debug.assertIsDefined(currentSourceFile,
85-
"Template literal node is missing 'rawText' and does not have a source file. Possibly bad transform.");
86-
text = getSourceTextOfNodeFromSourceFile(currentSourceFile, node);
87-
88-
// text contains the original source, it will also contain quotes ("`"), dolar signs and braces ("${" and "}"),
89-
// thus we need to remove those characters.
90-
// First template piece starts with "`", others with "}"
91-
// Last template piece ends with "`", others with "${"
92-
const isLast = node.kind === SyntaxKind.NoSubstitutionTemplateLiteral || node.kind === SyntaxKind.TemplateTail;
93-
text = text.substring(1, text.length - (isLast ? 1 : 2));
94-
}
95-
96-
// Newline normalization:
97-
// ES6 Spec 11.8.6.1 - Static Semantics of TV's and TRV's
98-
// <CR><LF> and <CR> LineTerminatorSequences are normalized to <LF> for both TV and TRV.
99-
text = text.replace(/\r\n?/g, "\n");
79+
const text = getRawTextOfTemplateLiteralLike(node, currentSourceFile);
10080
return setTextRange(factory.createStringLiteral(text), node);
10181
}
10282
}

src/compiler/utilities.ts

+24
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,30 @@ namespace ts {
713713
return Debug.fail(`Literal kind '${node.kind}' not accounted for.`);
714714
}
715715

716+
export function getRawTextOfTemplateLiteralLike(node: TemplateLiteralLikeNode, sourceFile: SourceFile) {
717+
// Find original source text, since we need to emit the raw strings of the tagged template.
718+
// The raw strings contain the (escaped) strings of what the user wrote.
719+
// Examples: `\n` is converted to "\\n", a template string with a newline to "\n".
720+
let text = node.rawText;
721+
if (text === undefined) {
722+
Debug.assertIsDefined(sourceFile,
723+
"Template literal node is missing 'rawText' and does not have a source file. Possibly bad transform.");
724+
text = getSourceTextOfNodeFromSourceFile(sourceFile, node);
725+
726+
// text contains the original source, it will also contain quotes ("`"), dolar signs and braces ("${" and "}"),
727+
// thus we need to remove those characters.
728+
// First template piece starts with "`", others with "}"
729+
// Last template piece ends with "`", others with "${"
730+
const isLast = node.kind === SyntaxKind.NoSubstitutionTemplateLiteral || node.kind === SyntaxKind.TemplateTail;
731+
text = text.substring(1, text.length - (isLast ? 1 : 2));
732+
}
733+
734+
// Newline normalization:
735+
// ES6 Spec 11.8.6.1 - Static Semantics of TV's and TRV's
736+
// <CR><LF> and <CR> LineTerminatorSequences are normalized to <LF> for both TV and TRV.
737+
return text.replace(/\r\n?/g, "\n");
738+
}
739+
716740
function canUseOriginalText(node: LiteralLikeNode, flags: GetLiteralTextFlags): boolean {
717741
if (nodeIsSynthesized(node) || !node.parent || (flags & GetLiteralTextFlags.TerminateUnterminatedLiterals && node.isUnterminated)) {
718742
return false;

src/harness/fourslashInterfaceImpl.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1119,6 +1119,7 @@ namespace FourSlashInterface {
11191119
varEntry("Number"),
11201120
interfaceEntry("NumberConstructor"),
11211121
interfaceEntry("TemplateStringsArray"),
1122+
typeEntry("TemplateStringsArrayOf"),
11221123
interfaceEntry("ImportMeta"),
11231124
interfaceEntry("ImportCallOptions"),
11241125
interfaceEntry("ImportAssertions"),

src/lib/es5.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,8 @@ interface TemplateStringsArray extends ReadonlyArray<string> {
594594
readonly raw: readonly string[];
595595
}
596596

597+
type TemplateStringsArrayOf<Cooked extends readonly string[], Raw extends readonly string[] = Cooked> = Cooked & { readonly raw: Raw };
598+
597599
/**
598600
* The type of `import.meta`.
599601
*

tests/baselines/reference/destructuringParameterDeclaration4.errors.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ tests/cases/conformance/es6/destructuring/destructuringParameterDeclaration4.ts(
4141
a1(...array2); // Error parameter type is (number|string)[]
4242
~~~~~~
4343
!!! error TS2552: Cannot find name 'array2'. Did you mean 'Array'?
44-
!!! related TS2728 /.ts/lib.es5.d.ts:1470:13: 'Array' is declared here.
44+
!!! related TS2728 /.ts/lib.es5.d.ts:1472:13: 'Array' is declared here.
4545
a5([1, 2, "string", false, true]); // Error, parameter type is [any, any, [[any]]]
4646
~~~~~~~~
4747
!!! error TS2322: Type 'string' is not assignable to type '[[any]]'.

0 commit comments

Comments
 (0)