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 1 commit
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
110 changes: 92 additions & 18 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -971,6 +971,7 @@ namespace ts {
let deferredGlobalAsyncIterableIteratorType: GenericType | undefined;
let deferredGlobalAsyncGeneratorType: GenericType | undefined;
let deferredGlobalTemplateStringsArrayType: ObjectType | undefined;
let deferredGlobalTemplateStringsArrayOfSymbol: Symbol | undefined;
let deferredGlobalImportMetaType: ObjectType;
let deferredGlobalImportMetaExpressionType: ObjectType;
let deferredGlobalImportCallOptionsType: ObjectType | undefined;
Expand Down Expand Up @@ -14102,6 +14103,11 @@ namespace ts {
return (deferredGlobalBigIntType ||= getGlobalType("BigInt" as __String, /*arity*/ 0, /*reportErrors*/ false)) || emptyObjectType;
}

function getGlobalTemplateStringsArrayOfSymbol(): Symbol | undefined {
deferredGlobalTemplateStringsArrayOfSymbol ||= getGlobalTypeAliasSymbol("TemplateStringsArrayOf" as __String, /*arity*/ 2, /*reportErrors*/ true) || unknownSymbol;
return deferredGlobalTemplateStringsArrayOfSymbol === unknownSymbol ? undefined : deferredGlobalTemplateStringsArrayOfSymbol;
}

/**
* Instantiates a global type that is generic with some element type, and returns that instantiation.
*/
Expand Down Expand Up @@ -21171,6 +21177,43 @@ namespace ts {
return isArrayType(type) || !(type.flags & TypeFlags.Nullable) && isTypeAssignableTo(type, anyReadonlyArrayType);
}

/**
* Returns `type` if it is an array or tuple type. If `type` is an intersection type,
* returns the rightmost constituent that is an array or tuple type, but only if there are no
* other constituents to that contain properties that overlap with array- or tuple- specific
* members (i.e., index signatures, numeric string property names, or `length`).
*/
function tryGetNonShadowedArrayOrTupleType(type: Type) {
if (isArrayOrTupleType(type)) {
return type;
}

if (!(type.flags & TypeFlags.Intersection)) {
return undefined;
}

let arrayOrTupleConstituent: TypeReference | undefined;
for (const constituent of (type as IntersectionType).types) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also this always just picks the last array or tuple type, making it order-dependent which tuple is picked, which seems bad. We should either have tuple-intersecting behavior or return undefined when there are multiple arrays/tuples, IMO.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are already order dependent for this case: Playground link

Copy link
Member Author

@rbuckton rbuckton Jun 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think intersecting the tuples is viable here, as it introduces far too much complexity. I'm leaning towards what appears to be the current behavior in this case (i.e., picking the right-most), though another viable alternative would be to pick neither if there is more than one (since they would overlap).

Copy link
Member

@weswigham weswigham Jun 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Picking neither if there's more than one is often what we do in other situations, and order-dependence in inference is bad anywhere it crops up, since it means inference results can shift with file ordering, which is a pain in the butt bug to track down when it's reported later. Our existing left-preferring behavior among multiple matching (mutually subtyped) inferences is already a big "this really should be fixed"; I'd much prefer to bring the cases that introduce order dependence down if possible.

if (isArrayOrTupleType(constituent)) {
rbuckton marked this conversation as resolved.
Show resolved Hide resolved
arrayOrTupleConstituent = constituent;
}
else {
const properties = getPropertiesOfType(constituent);
for (const property of properties) {
if (isNumericLiteralName(property.escapedName) || property.escapedName === "length" as __String) {
return undefined;
}
}

if (some(getIndexInfosOfType(constituent))) {
return undefined;
}
}
}

return arrayOrTupleConstituent;
}

function getSingleBaseForNonAugmentingSubtype(type: Type) {
if (!(getObjectFlags(type) & ObjectFlags.Reference) || !(getObjectFlags((type as TypeReference).target) & ObjectFlags.ClassOrInterface)) {
return undefined;
Expand Down Expand Up @@ -22830,68 +22873,69 @@ namespace ts {
}
// Infer from the members of source and target only if the two types are possibly related
if (!typesDefinitelyUnrelated(source, target)) {
if (isArrayOrTupleType(source)) {
const sourceArrayOrTuple = tryGetNonShadowedArrayOrTupleType(source);
if (sourceArrayOrTuple) {
if (isTupleType(target)) {
const sourceArity = getTypeReferenceArity(source);
const sourceArity = getTypeReferenceArity(sourceArrayOrTuple);
const targetArity = getTypeReferenceArity(target);
const elementTypes = getTypeArguments(target);
const elementFlags = target.target.elementFlags;
// When source and target are tuple types with the same structure (fixed, variadic, and rest are matched
// to the same kind in each position), simply infer between the element types.
if (isTupleType(source) && isTupleTypeStructureMatching(source, target)) {
if (isTupleType(sourceArrayOrTuple) && isTupleTypeStructureMatching(sourceArrayOrTuple, target)) {
for (let i = 0; i < targetArity; i++) {
inferFromTypes(getTypeArguments(source)[i], elementTypes[i]);
inferFromTypes(getTypeArguments(sourceArrayOrTuple)[i], elementTypes[i]);
}
return;
}
const startLength = isTupleType(source) ? Math.min(source.target.fixedLength, target.target.fixedLength) : 0;
const endLength = Math.min(isTupleType(source) ? getEndElementCount(source.target, ElementFlags.Fixed) : 0,
const startLength = isTupleType(sourceArrayOrTuple) ? Math.min(sourceArrayOrTuple.target.fixedLength, target.target.fixedLength) : 0;
const endLength = Math.min(isTupleType(sourceArrayOrTuple) ? getEndElementCount(sourceArrayOrTuple.target, ElementFlags.Fixed) : 0,
target.target.hasRestElement ? getEndElementCount(target.target, ElementFlags.Fixed) : 0);
// Infer between starting fixed elements.
for (let i = 0; i < startLength; i++) {
inferFromTypes(getTypeArguments(source)[i], elementTypes[i]);
inferFromTypes(getTypeArguments(sourceArrayOrTuple)[i], elementTypes[i]);
}
if (!isTupleType(source) || sourceArity - startLength - endLength === 1 && source.target.elementFlags[startLength] & ElementFlags.Rest) {
if (!isTupleType(sourceArrayOrTuple) || sourceArity - startLength - endLength === 1 && sourceArrayOrTuple.target.elementFlags[startLength] & ElementFlags.Rest) {
// Single rest element remains in source, infer from that to every element in target
const restType = getTypeArguments(source)[startLength];
const restType = getTypeArguments(sourceArrayOrTuple)[startLength];
for (let i = startLength; i < targetArity - endLength; i++) {
inferFromTypes(elementFlags[i] & ElementFlags.Variadic ? createArrayType(restType) : restType, elementTypes[i]);
}
}
else {
const middleLength = targetArity - startLength - endLength;
if (middleLength === 2 && elementFlags[startLength] & elementFlags[startLength + 1] & ElementFlags.Variadic && isTupleType(source)) {
if (middleLength === 2 && elementFlags[startLength] & elementFlags[startLength + 1] & ElementFlags.Variadic && isTupleType(sourceArrayOrTuple)) {
// Middle of target is [...T, ...U] and source is tuple type
const targetInfo = getInferenceInfoForType(elementTypes[startLength]);
if (targetInfo && targetInfo.impliedArity !== undefined) {
// Infer slices from source based on implied arity of T.
inferFromTypes(sliceTupleType(source, startLength, endLength + sourceArity - targetInfo.impliedArity), elementTypes[startLength]);
inferFromTypes(sliceTupleType(source, startLength + targetInfo.impliedArity, endLength), elementTypes[startLength + 1]);
inferFromTypes(sliceTupleType(sourceArrayOrTuple, startLength, endLength + sourceArity - targetInfo.impliedArity), elementTypes[startLength]);
inferFromTypes(sliceTupleType(sourceArrayOrTuple, startLength + targetInfo.impliedArity, endLength), elementTypes[startLength + 1]);
}
}
else if (middleLength === 1 && elementFlags[startLength] & ElementFlags.Variadic) {
// Middle of target is exactly one variadic element. Infer the slice between the fixed parts in the source.
// If target ends in optional element(s), make a lower priority a speculative inference.
const endsInOptional = target.target.elementFlags[targetArity - 1] & ElementFlags.Optional;
const sourceSlice = isTupleType(source) ? sliceTupleType(source, startLength, endLength) : createArrayType(getTypeArguments(source)[0]);
const sourceSlice = isTupleType(sourceArrayOrTuple) ? sliceTupleType(sourceArrayOrTuple, startLength, endLength) : createArrayType(getTypeArguments(sourceArrayOrTuple)[0]);
inferWithPriority(sourceSlice, elementTypes[startLength], endsInOptional ? InferencePriority.SpeculativeTuple : 0);
}
else if (middleLength === 1 && elementFlags[startLength] & ElementFlags.Rest) {
// Middle of target is exactly one rest element. If middle of source is not empty, infer union of middle element types.
const restType = isTupleType(source) ? getElementTypeOfSliceOfTupleType(source, startLength, endLength) : getTypeArguments(source)[0];
const restType = isTupleType(sourceArrayOrTuple) ? getElementTypeOfSliceOfTupleType(sourceArrayOrTuple, startLength, endLength) : getTypeArguments(sourceArrayOrTuple)[0];
if (restType) {
inferFromTypes(restType, elementTypes[startLength]);
}
}
}
// Infer between ending fixed elements
for (let i = 0; i < endLength; i++) {
inferFromTypes(getTypeArguments(source)[sourceArity - i - 1], elementTypes[targetArity - i - 1]);
inferFromTypes(getTypeArguments(sourceArrayOrTuple)[sourceArity - i - 1], elementTypes[targetArity - i - 1]);
}
return;
}
if (isArrayType(target)) {
inferFromIndexTypes(source, target);
inferFromIndexTypes(sourceArrayOrTuple, target);
return;
}
}
Expand Down Expand Up @@ -27548,7 +27592,37 @@ namespace ts {
return checkIteratedTypeOrElementType(IterationUse.Spread, arrayOrIterableType, undefinedType, node.expression);
}

function getTemplateStringsArrayOf(cookedTypes: Type[], rawTypes: Type[]) {
const templateStringsArrayOfAlias = getGlobalTemplateStringsArrayOfSymbol();
if (!templateStringsArrayOfAlias) return getGlobalTemplateStringsArrayType();
const cookedType = createTupleType(cookedTypes, /*elementFlags*/ undefined, /*readonly*/ true);
const rawType = createTupleType(rawTypes, /*elementFlags*/ undefined, /*readonly*/ true);
return getTypeAliasInstantiation(templateStringsArrayOfAlias, [cookedType, rawType]);
}

function getRawLiteralType(node: TemplateLiteralLikeNode) {
const text = getRawTextOfTemplateLiteralLike(node, getSourceFileOfNode(node));
return getStringLiteralType(text);
}

function checkSyntheticExpression(node: SyntheticExpression): Type {
if (isTemplateLiteral(node.parent) && node.type === getGlobalTemplateStringsArrayType()) {
const cookedStrings: Type[] = [];
const rawStrings: Type[] = [];
if (isNoSubstitutionTemplateLiteral(node.parent)) {
cookedStrings.push(getStringLiteralType(node.parent.text));
rawStrings.push(getRawLiteralType(node.parent));
}
else {
cookedStrings.push(getStringLiteralType(node.parent.head.text));
rawStrings.push(getRawLiteralType(node.parent.head));
for (const templateSpan of node.parent.templateSpans) {
cookedStrings.push(getStringLiteralType(templateSpan.literal.text));
rawStrings.push(getRawLiteralType(templateSpan.literal));
}
}
return getTemplateStringsArrayOf(cookedStrings, rawStrings);
}
return node.isSpread ? getIndexedAccessType(node.type, numberType) : node.type;
}

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

if (!isDecorator) {
typeArguments = (node as CallExpression).typeArguments;
typeArguments = node.typeArguments;

// We already perform checking on the type arguments on the class declaration itself.
if (isTaggedTemplate || isJsxOpeningOrSelfClosingElement || (node as CallExpression).expression.kind !== SyntaxKind.SuperKeyword) {
if (isTaggedTemplate || isJsxOpeningOrSelfClosingElement || node.expression.kind !== SyntaxKind.SuperKeyword) {
forEach(typeArguments, checkSourceElement);
}
}
Expand Down
22 changes: 1 addition & 21 deletions src/compiler/transformers/taggedTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,27 +76,7 @@ namespace ts {
* @param node The ES6 template literal.
*/
function getRawLiteral(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 @@ -713,6 +713,30 @@ namespace ts {
return Debug.fail(`Literal kind '${node.kind}' not accounted for.`);
}

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 @@ -1119,6 +1119,7 @@ namespace FourSlashInterface {
varEntry("Number"),
interfaceEntry("NumberConstructor"),
interfaceEntry("TemplateStringsArray"),
typeEntry("TemplateStringsArrayOf"),
interfaceEntry("ImportMeta"),
interfaceEntry("ImportCallOptions"),
interfaceEntry("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 @@ -594,6 +594,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
Original file line number Diff line number Diff line change
Expand Up @@ -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:1470:13: 'Array' is declared here.
!!! related TS2728 /.ts/lib.es5.d.ts:1472: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]]'.
Expand Down
Loading