Skip to content

Commit 883fc2a

Browse files
committed
More specific TemplateStringsArray type for tagged templates
1 parent 3fc5f96 commit 883fc2a

35 files changed

+580
-104
lines changed

src/compiler/checker.ts

Lines changed: 105 additions & 18 deletions
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,53 @@ namespace ts {
2117121177
return isArrayType(type) || !(type.flags & TypeFlags.Nullable) && isTypeAssignableTo(type, anyReadonlyArrayType);
2117221178
}
2117321179

21180+
function tryGetNonShadowedArrayOrTupleType(type: Type) {
21181+
if (isArrayOrTupleType(type)) {
21182+
return type;
21183+
}
21184+
21185+
if (!(type.flags & TypeFlags.Intersection)) {
21186+
return undefined;
21187+
}
21188+
21189+
// pick an intersection constituent if it is an array or tuple type, but only
21190+
// if any non-array, non-tuple constituents that follow it do not shadow any
21191+
// tuple-specific members.
21192+
let arrayOrTuple: TypeReference | undefined;
21193+
let constituents: Type[] | undefined;
21194+
for (const constituent of (type as IntersectionType).types) {
21195+
if (isArrayOrTupleType(constituent)) {
21196+
arrayOrTuple = constituent;
21197+
constituents = undefined;
21198+
}
21199+
else {
21200+
constituents = append(constituents, constituent);
21201+
}
21202+
}
21203+
21204+
if (!arrayOrTuple || !constituents) {
21205+
return arrayOrTuple;
21206+
}
21207+
21208+
for (const constituent of constituents) {
21209+
if (!(constituent.flags & TypeFlags.Object)) continue;
21210+
const properties = getPropertiesOfType(constituent);
21211+
for (const property of properties) {
21212+
// consider numeric literal names or the string 'length' to be overlapping
21213+
if (isNumericLiteralName(property.escapedName) || property.escapedName === "length" as __String) {
21214+
return undefined;
21215+
}
21216+
}
21217+
21218+
// Consider a constituent to be overlapping if it contains an index signature that shadows array indexes
21219+
if (findApplicableIndexInfo(getIndexInfosOfType(constituent), numberType)) {
21220+
return undefined;
21221+
}
21222+
}
21223+
21224+
return arrayOrTuple;
21225+
}
21226+
2117421227
function getSingleBaseForNonAugmentingSubtype(type: Type) {
2117521228
if (!(getObjectFlags(type) & ObjectFlags.Reference) || !(getObjectFlags((type as TypeReference).target) & ObjectFlags.ClassOrInterface)) {
2117621229
return undefined;
@@ -22830,68 +22883,69 @@ namespace ts {
2283022883
}
2283122884
// Infer from the members of source and target only if the two types are possibly related
2283222885
if (!typesDefinitelyUnrelated(source, target)) {
22833-
if (isArrayOrTupleType(source)) {
22886+
const sourceArrayOrTuple = tryGetNonShadowedArrayOrTupleType(source);
22887+
if (sourceArrayOrTuple) {
2283422888
if (isTupleType(target)) {
22835-
const sourceArity = getTypeReferenceArity(source);
22889+
const sourceArity = getTypeReferenceArity(sourceArrayOrTuple);
2283622890
const targetArity = getTypeReferenceArity(target);
2283722891
const elementTypes = getTypeArguments(target);
2283822892
const elementFlags = target.target.elementFlags;
2283922893
// When source and target are tuple types with the same structure (fixed, variadic, and rest are matched
2284022894
// to the same kind in each position), simply infer between the element types.
22841-
if (isTupleType(source) && isTupleTypeStructureMatching(source, target)) {
22895+
if (isTupleType(sourceArrayOrTuple) && isTupleTypeStructureMatching(sourceArrayOrTuple, target)) {
2284222896
for (let i = 0; i < targetArity; i++) {
22843-
inferFromTypes(getTypeArguments(source)[i], elementTypes[i]);
22897+
inferFromTypes(getTypeArguments(sourceArrayOrTuple)[i], elementTypes[i]);
2284422898
}
2284522899
return;
2284622900
}
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,
22901+
const startLength = isTupleType(sourceArrayOrTuple) ? Math.min(sourceArrayOrTuple.target.fixedLength, target.target.fixedLength) : 0;
22902+
const endLength = Math.min(isTupleType(sourceArrayOrTuple) ? getEndElementCount(sourceArrayOrTuple.target, ElementFlags.Fixed) : 0,
2284922903
target.target.hasRestElement ? getEndElementCount(target.target, ElementFlags.Fixed) : 0);
2285022904
// Infer between starting fixed elements.
2285122905
for (let i = 0; i < startLength; i++) {
22852-
inferFromTypes(getTypeArguments(source)[i], elementTypes[i]);
22906+
inferFromTypes(getTypeArguments(sourceArrayOrTuple)[i], elementTypes[i]);
2285322907
}
22854-
if (!isTupleType(source) || sourceArity - startLength - endLength === 1 && source.target.elementFlags[startLength] & ElementFlags.Rest) {
22908+
if (!isTupleType(sourceArrayOrTuple) || sourceArity - startLength - endLength === 1 && sourceArrayOrTuple.target.elementFlags[startLength] & ElementFlags.Rest) {
2285522909
// Single rest element remains in source, infer from that to every element in target
22856-
const restType = getTypeArguments(source)[startLength];
22910+
const restType = getTypeArguments(sourceArrayOrTuple)[startLength];
2285722911
for (let i = startLength; i < targetArity - endLength; i++) {
2285822912
inferFromTypes(elementFlags[i] & ElementFlags.Variadic ? createArrayType(restType) : restType, elementTypes[i]);
2285922913
}
2286022914
}
2286122915
else {
2286222916
const middleLength = targetArity - startLength - endLength;
22863-
if (middleLength === 2 && elementFlags[startLength] & elementFlags[startLength + 1] & ElementFlags.Variadic && isTupleType(source)) {
22917+
if (middleLength === 2 && elementFlags[startLength] & elementFlags[startLength + 1] & ElementFlags.Variadic && isTupleType(sourceArrayOrTuple)) {
2286422918
// Middle of target is [...T, ...U] and source is tuple type
2286522919
const targetInfo = getInferenceInfoForType(elementTypes[startLength]);
2286622920
if (targetInfo && targetInfo.impliedArity !== undefined) {
2286722921
// 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]);
22922+
inferFromTypes(sliceTupleType(sourceArrayOrTuple, startLength, endLength + sourceArity - targetInfo.impliedArity), elementTypes[startLength]);
22923+
inferFromTypes(sliceTupleType(sourceArrayOrTuple, startLength + targetInfo.impliedArity, endLength), elementTypes[startLength + 1]);
2287022924
}
2287122925
}
2287222926
else if (middleLength === 1 && elementFlags[startLength] & ElementFlags.Variadic) {
2287322927
// Middle of target is exactly one variadic element. Infer the slice between the fixed parts in the source.
2287422928
// If target ends in optional element(s), make a lower priority a speculative inference.
2287522929
const endsInOptional = target.target.elementFlags[targetArity - 1] & ElementFlags.Optional;
22876-
const sourceSlice = isTupleType(source) ? sliceTupleType(source, startLength, endLength) : createArrayType(getTypeArguments(source)[0]);
22930+
const sourceSlice = isTupleType(sourceArrayOrTuple) ? sliceTupleType(sourceArrayOrTuple, startLength, endLength) : createArrayType(getTypeArguments(sourceArrayOrTuple)[0]);
2287722931
inferWithPriority(sourceSlice, elementTypes[startLength], endsInOptional ? InferencePriority.SpeculativeTuple : 0);
2287822932
}
2287922933
else if (middleLength === 1 && elementFlags[startLength] & ElementFlags.Rest) {
2288022934
// 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];
22935+
const restType = isTupleType(sourceArrayOrTuple) ? getElementTypeOfSliceOfTupleType(sourceArrayOrTuple, startLength, endLength) : getTypeArguments(sourceArrayOrTuple)[0];
2288222936
if (restType) {
2288322937
inferFromTypes(restType, elementTypes[startLength]);
2288422938
}
2288522939
}
2288622940
}
2288722941
// Infer between ending fixed elements
2288822942
for (let i = 0; i < endLength; i++) {
22889-
inferFromTypes(getTypeArguments(source)[sourceArity - i - 1], elementTypes[targetArity - i - 1]);
22943+
inferFromTypes(getTypeArguments(sourceArrayOrTuple)[sourceArity - i - 1], elementTypes[targetArity - i - 1]);
2289022944
}
2289122945
return;
2289222946
}
2289322947
if (isArrayType(target)) {
22894-
inferFromIndexTypes(source, target);
22948+
inferFromIndexTypes(sourceArrayOrTuple, target);
2289522949
return;
2289622950
}
2289722951
}
@@ -27548,7 +27602,37 @@ namespace ts {
2754827602
return checkIteratedTypeOrElementType(IterationUse.Spread, arrayOrIterableType, undefinedType, node.expression);
2754927603
}
2755027604

27605+
function getTemplateStringsArrayOf(cookedTypes: Type[], rawTypes: Type[]) {
27606+
const templateStringsArrayOfAlias = getGlobalTemplateStringsArrayOfSymbol();
27607+
if (!templateStringsArrayOfAlias) return getGlobalTemplateStringsArrayType();
27608+
const cookedType = createTupleType(cookedTypes, /*elementFlags*/ undefined, /*readonly*/ true);
27609+
const rawType = createTupleType(rawTypes, /*elementFlags*/ undefined, /*readonly*/ true);
27610+
return getTypeAliasInstantiation(templateStringsArrayOfAlias, [cookedType, rawType]);
27611+
}
27612+
27613+
function getRawLiteralType(node: TemplateLiteralLikeNode) {
27614+
const text = getRawTextOfTemplateLiteralLike(node, getSourceFileOfNode(node));
27615+
return getStringLiteralType(text);
27616+
}
27617+
2755127618
function checkSyntheticExpression(node: SyntheticExpression): Type {
27619+
if (isTemplateLiteral(node.parent) && node.type === getGlobalTemplateStringsArrayType()) {
27620+
const cookedStrings: Type[] = [];
27621+
const rawStrings: Type[] = [];
27622+
if (isNoSubstitutionTemplateLiteral(node.parent)) {
27623+
cookedStrings.push(getStringLiteralType(node.parent.text));
27624+
rawStrings.push(getRawLiteralType(node.parent));
27625+
}
27626+
else {
27627+
cookedStrings.push(getStringLiteralType(node.parent.head.text));
27628+
rawStrings.push(getRawLiteralType(node.parent.head));
27629+
for (const templateSpan of node.parent.templateSpans) {
27630+
cookedStrings.push(getStringLiteralType(templateSpan.literal.text));
27631+
rawStrings.push(getRawLiteralType(templateSpan.literal));
27632+
}
27633+
}
27634+
return getTemplateStringsArrayOf(cookedStrings, rawStrings);
27635+
}
2755227636
return node.isSpread ? getIndexedAccessType(node.type, numberType) : node.type;
2755327637
}
2755427638

@@ -30587,10 +30671,10 @@ namespace ts {
3058730671
let typeArguments: NodeArray<TypeNode> | undefined;
3058830672

3058930673
if (!isDecorator) {
30590-
typeArguments = (node as CallExpression).typeArguments;
30674+
typeArguments = node.typeArguments;
3059130675

3059230676
// We already perform checking on the type arguments on the class declaration itself.
30593-
if (isTaggedTemplate || isJsxOpeningOrSelfClosingElement || (node as CallExpression).expression.kind !== SyntaxKind.SuperKeyword) {
30677+
if (isTaggedTemplate || isJsxOpeningOrSelfClosingElement || node.expression.kind !== SyntaxKind.SuperKeyword) {
3059430678
forEach(typeArguments, checkSourceElement);
3059530679
}
3059630680
}
@@ -35682,6 +35766,9 @@ namespace ts {
3568235766
}
3568335767

3568435768
function checkTypeReferenceNode(node: TypeReferenceNode | ExpressionWithTypeArguments) {
35769+
if (isTypeReferenceNode(node) && isIdentifier(node.typeName) && node.typeName.escapedText === "Scenario") {
35770+
debugger;
35771+
}
3568535772
checkGrammarTypeArguments(node, node.typeArguments);
3568635773
if (node.kind === SyntaxKind.TypeReference && node.typeName.jsdocDotPos !== undefined && !isInJSFile(node) && !isInJSDoc(node)) {
3568735774
grammarErrorAtPos(node, node.typeName.jsdocDotPos, 1, Diagnostics.JSDoc_types_can_only_be_used_inside_documentation_comments);

src/compiler/transformers/taggedTemplate.ts

Lines changed: 1 addition & 21 deletions
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

Lines changed: 24 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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
*

0 commit comments

Comments
 (0)