Skip to content

Commit f6e2298

Browse files
committed
Instantiable tuple labels
1 parent 3af710e commit f6e2298

File tree

12 files changed

+302
-27
lines changed

12 files changed

+302
-27
lines changed

src/compiler/checker.ts

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7004,11 +7004,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
70047004
for (let i = 0; i < tupleConstituentNodes.length; i++) {
70057005
const flags = (type.target as TupleType).elementFlags[i];
70067006
const labeledElementDeclaration = labeledElementDeclarations?.[i];
7007+
const label = labeledElementDeclaration && getTupleElementLabel(type as TupleTypeReference , labeledElementDeclaration);
70077008

7008-
if (labeledElementDeclaration) {
7009+
if (label) {
70097010
tupleConstituentNodes[i] = factory.createNamedTupleMember(
70107011
flags & ElementFlags.Variable ? factory.createToken(SyntaxKind.DotDotDotToken) : undefined,
7011-
factory.createIdentifier(unescapeLeadingUnderscores(getTupleElementLabel(labeledElementDeclaration))),
7012+
factory.createIdentifier(unescapeLeadingUnderscores(label)),
70127013
flags & ElementFlags.Optional ? factory.createToken(SyntaxKind.QuestionToken) : undefined,
70137014
flags & ElementFlags.Rest ? factory.createArrayTypeNode(tupleConstituentNodes[i]) :
70147015
tupleConstituentNodes[i],
@@ -12954,7 +12955,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1295412955
function getUniqAssociatedNamesFromTupleType(type: TupleTypeReference, restName: __String) {
1295512956
const associatedNamesMap = new Map<__String, number>();
1295612957
return map(type.target.labeledElementDeclarations, (labeledElement, i) => {
12957-
const name = getTupleElementLabel(labeledElement, i, restName);
12958+
const name = getTupleElementLabel(type, labeledElement, i, restName);
1295812959
const prevCounter = associatedNamesMap.get(name);
1295912960
if (prevCounter === undefined) {
1296012961
associatedNamesMap.set(name, 1);
@@ -35297,13 +35298,24 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3529735298
return type;
3529835299
}
3529935300

35300-
function getTupleElementLabel(d: ParameterDeclaration | NamedTupleMember): __String;
35301-
function getTupleElementLabel(d: ParameterDeclaration | NamedTupleMember | undefined, index: number, restParameterName?: __String): __String;
35302-
function getTupleElementLabel(d: ParameterDeclaration | NamedTupleMember | undefined, index?: number, restParameterName = "arg" as __String) {
35301+
function getTupleElementLabel(tupleType: TupleTypeReference, d: ParameterDeclaration | NamedTupleMember): __String | undefined;
35302+
function getTupleElementLabel(tupleType: TupleTypeReference, d: ParameterDeclaration | NamedTupleMember | undefined, index: number, restParameterName?: __String): __String;
35303+
function getTupleElementLabel(tupleType: TupleTypeReference, d: ParameterDeclaration | NamedTupleMember | undefined, index?: number, restParameterName = "arg" as __String) {
3530335304
if (!d) {
3530435305
return `${restParameterName}_${index}` as __String;
3530535306
}
35306-
Debug.assert(isIdentifier(d.name)); // Parameter declarations could be binding patterns, but we only allow identifier names
35307+
Debug.assert(!isBindingPattern(d.name)); // Parameter declarations could be binding patterns, but we only allow identifier names with them
35308+
if (d.name.kind === SyntaxKind.TemplateLiteralType) {
35309+
const label = instantiateType(getTypeFromTypeNode(d.name), tupleType.mapper);
35310+
return label.flags & TypeFlags.StringLiteral
35311+
? escapeLeadingUnderscores((label as StringLiteralType).value)
35312+
: typeof index === 'number'
35313+
? `${restParameterName}_${index}` as __String
35314+
: undefined;
35315+
}
35316+
if (d.name.kind === SyntaxKind.NoSubstitutionTemplateLiteral) {
35317+
return escapeLeadingUnderscores(d.name.text);
35318+
}
3530735319
return d.name.escapedText;
3530835320
}
3530935321

@@ -35315,9 +35327,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3531535327
const restParameter = signature.parameters[paramCount] || unknownSymbol;
3531635328
const restType = overrideRestType || getTypeOfSymbol(restParameter);
3531735329
if (isTupleType(restType)) {
35318-
const associatedNames = ((restType as TypeReference).target as TupleType).labeledElementDeclarations;
35330+
const associatedNames = restType.target.labeledElementDeclarations;
3531935331
const index = pos - paramCount;
35320-
return getTupleElementLabel(associatedNames?.[index], index, restParameter.escapedName);
35332+
return getTupleElementLabel(restType, associatedNames?.[index], index, restParameter.escapedName);
3532135333
}
3532235334
return restParameter.escapedName;
3532335335
}
@@ -35345,13 +35357,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3534535357

3534635358
const restType = getTypeOfSymbol(restParameter);
3534735359
if (isTupleType(restType)) {
35348-
const associatedNames = ((restType as TypeReference).target as TupleType).labeledElementDeclarations;
35360+
const associatedNames = restType.target.labeledElementDeclarations;
3534935361
const index = pos - paramCount;
3535035362
const associatedName = associatedNames?.[index];
3535135363
const isRestTupleElement = !!associatedName?.dotDotDotToken;
3535235364

35353-
if (associatedName) {
35354-
Debug.assert(isIdentifier(associatedName.name));
35365+
// TODO: figure out the non-identifier case here
35366+
if (associatedName && isIdentifier(associatedName.name)) {
3535535367
return { parameter: associatedName.name, parameterName: associatedName.name.escapedText, isRestParameter: isRestTupleElement };
3535635368
}
3535735369

@@ -35380,7 +35392,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3538035392
const restParameter = signature.parameters[paramCount] || unknownSymbol;
3538135393
const restType = getTypeOfSymbol(restParameter);
3538235394
if (isTupleType(restType)) {
35383-
const associatedNames = ((restType as TypeReference).target as TupleType).labeledElementDeclarations;
35395+
const associatedNames = restType.target.labeledElementDeclarations;
3538435396
const index = pos - paramCount;
3538535397
return associatedNames && associatedNames[index];
3538635398
}
@@ -39580,6 +39592,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3958039592
if (node.type.kind === SyntaxKind.RestType) {
3958139593
grammarErrorOnNode(node.type, Diagnostics.A_labeled_tuple_element_is_declared_as_rest_with_a_before_the_name_rather_than_before_the_type);
3958239594
}
39595+
if (node.name.kind === SyntaxKind.TemplateLiteralType) {
39596+
checkSourceElement(node.name);
39597+
}
3958339598
checkSourceElement(node.type);
3958439599
getTypeFromTypeNode(node);
3958539600
}

src/compiler/factory/nodeFactory.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,7 @@ import {
326326
NamedImportBindings,
327327
NamedImports,
328328
NamedTupleMember,
329+
NamedTupleMemberName,
329330
NamespaceExport,
330331
NamespaceExportDeclaration,
331332
NamespaceImport,
@@ -2499,7 +2500,7 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
24992500
}
25002501

25012502
// @api
2502-
function createNamedTupleMember(dotDotDotToken: DotDotDotToken | undefined, name: Identifier, questionToken: QuestionToken | undefined, type: TypeNode) {
2503+
function createNamedTupleMember(dotDotDotToken: DotDotDotToken | undefined, name: NamedTupleMemberName, questionToken: QuestionToken | undefined, type: TypeNode) {
25032504
const node = createBaseDeclaration<NamedTupleMember>(SyntaxKind.NamedTupleMember);
25042505
node.dotDotDotToken = dotDotDotToken;
25052506
node.name = name;
@@ -2512,7 +2513,7 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
25122513
}
25132514

25142515
// @api
2515-
function updateNamedTupleMember(node: NamedTupleMember, dotDotDotToken: DotDotDotToken | undefined, name: Identifier, questionToken: QuestionToken | undefined, type: TypeNode) {
2516+
function updateNamedTupleMember(node: NamedTupleMember, dotDotDotToken: DotDotDotToken | undefined, name: NamedTupleMemberName, questionToken: QuestionToken | undefined, type: TypeNode) {
25162517
return node.dotDotDotToken !== dotDotDotToken
25172518
|| node.name !== name
25182519
|| node.questionToken !== questionToken

src/compiler/parser.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4404,23 +4404,36 @@ namespace Parser {
44044404
return type;
44054405
}
44064406

4407-
function isNextTokenColonOrQuestionColon() {
4408-
return nextToken() === SyntaxKind.ColonToken || (token() === SyntaxKind.QuestionToken && nextToken() === SyntaxKind.ColonToken);
4407+
function isTokenColonOrQuestionColon() {
4408+
return token() === SyntaxKind.ColonToken || (token() === SyntaxKind.QuestionToken && nextToken() === SyntaxKind.ColonToken);
44094409
}
44104410

44114411
function isTupleElementName() {
44124412
if (token() === SyntaxKind.DotDotDotToken) {
4413-
return tokenIsIdentifierOrKeyword(nextToken()) && isNextTokenColonOrQuestionColon();
4413+
nextToken()
4414+
}
4415+
if (token() === SyntaxKind.TemplateHead) {
4416+
parseTemplateType();
4417+
return isTokenColonOrQuestionColon();
44144418
}
4415-
return tokenIsIdentifierOrKeyword(token()) && isNextTokenColonOrQuestionColon();
4419+
if (tokenIsIdentifierOrKeyword(token()) || token() === SyntaxKind.NoSubstitutionTemplateLiteral) {
4420+
nextToken();
4421+
return isTokenColonOrQuestionColon();
4422+
}
4423+
return false
44164424
}
44174425

44184426
function parseTupleElementNameOrTupleElementType() {
4427+
// TODO: optimize this so we don't have to do lookahead
44194428
if (lookAhead(isTupleElementName)) {
44204429
const pos = getNodePos();
44214430
const hasJSDoc = hasPrecedingJSDocComment();
44224431
const dotDotDotToken = parseOptionalToken(SyntaxKind.DotDotDotToken);
4423-
const name = parseIdentifierName();
4432+
const name = token() === SyntaxKind.NoSubstitutionTemplateLiteral
4433+
? (parseLiteralLikeNode(token()) as NoSubstitutionTemplateLiteral)
4434+
: token() === SyntaxKind.TemplateHead
4435+
? parseTemplateType()
4436+
: parseIdentifierName();
44244437
const questionToken = parseOptionalToken(SyntaxKind.QuestionToken);
44254438
parseExpected(SyntaxKind.ColonToken);
44264439
const type = parseTupleElementType();

src/compiler/types.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2222,10 +2222,12 @@ export interface TupleTypeNode extends TypeNode {
22222222
readonly elements: NodeArray<TypeNode | NamedTupleMember>;
22232223
}
22242224

2225+
export type NamedTupleMemberName = Identifier | NoSubstitutionTemplateLiteral | TemplateLiteralTypeNode;
2226+
22252227
export interface NamedTupleMember extends TypeNode, Declaration, JSDocContainer {
22262228
readonly kind: SyntaxKind.NamedTupleMember;
22272229
readonly dotDotDotToken?: Token<SyntaxKind.DotDotDotToken>;
2228-
readonly name: Identifier;
2230+
readonly name: NamedTupleMemberName;
22292231
readonly questionToken?: Token<SyntaxKind.QuestionToken>;
22302232
readonly type: TypeNode;
22312233
}
@@ -8379,8 +8381,8 @@ export interface NodeFactory {
83798381
updateArrayTypeNode(node: ArrayTypeNode, elementType: TypeNode): ArrayTypeNode;
83808382
createTupleTypeNode(elements: readonly (TypeNode | NamedTupleMember)[]): TupleTypeNode;
83818383
updateTupleTypeNode(node: TupleTypeNode, elements: readonly (TypeNode | NamedTupleMember)[]): TupleTypeNode;
8382-
createNamedTupleMember(dotDotDotToken: DotDotDotToken | undefined, name: Identifier, questionToken: QuestionToken | undefined, type: TypeNode): NamedTupleMember;
8383-
updateNamedTupleMember(node: NamedTupleMember, dotDotDotToken: DotDotDotToken | undefined, name: Identifier, questionToken: QuestionToken | undefined, type: TypeNode): NamedTupleMember;
8384+
createNamedTupleMember(dotDotDotToken: DotDotDotToken | undefined, name: NamedTupleMemberName, questionToken: QuestionToken | undefined, type: TypeNode): NamedTupleMember;
8385+
updateNamedTupleMember(node: NamedTupleMember, dotDotDotToken: DotDotDotToken | undefined, name: NamedTupleMemberName, questionToken: QuestionToken | undefined, type: TypeNode): NamedTupleMember;
83848386
createOptionalTypeNode(type: TypeNode): OptionalTypeNode;
83858387
updateOptionalTypeNode(node: OptionalTypeNode, type: TypeNode): OptionalTypeNode;
83868388
createRestTypeNode(type: TypeNode): RestTypeNode;

src/compiler/utilities.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@ import {
403403
NamedExports,
404404
NamedImports,
405405
NamedImportsOrExports,
406+
NamedTupleMemberName,
406407
NamespaceExport,
407408
NamespaceImport,
408409
NewExpression,
@@ -10434,3 +10435,8 @@ export function getPropertyNameFromType(type: StringLiteralType | NumberLiteralT
1043410435
}
1043510436
return Debug.fail();
1043610437
}
10438+
10439+
/** @internal */
10440+
export function isNamedTupleMemberName(node: Node): node is NamedTupleMemberName {
10441+
return node.kind === SyntaxKind.Identifier || node.kind === SyntaxKind.TemplateLiteralType || node.kind === SyntaxKind.NoSubstitutionTemplateLiteral;
10442+
}

src/compiler/visitorPublic.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import {
6363
isModuleReference,
6464
isNamedExportBindings,
6565
isNamedImportBindings,
66+
isNamedTupleMemberName,
6667
isObjectLiteralElementLike,
6768
isOptionalChain,
6869
isParameter,
@@ -913,7 +914,7 @@ const visitEachChildTable: VisitEachChildTable = {
913914
return context.factory.updateNamedTupleMember(
914915
node,
915916
tokenVisitor ? nodeVisitor(node.dotDotDotToken, tokenVisitor, isDotDotDotToken) : node.dotDotDotToken,
916-
Debug.checkDefined(nodeVisitor(node.name, visitor, isIdentifier)),
917+
Debug.checkDefined(nodeVisitor(node.name, visitor, isNamedTupleMemberName)),
917918
tokenVisitor ? nodeVisitor(node.questionToken, tokenVisitor, isQuestionToken) : node.questionToken,
918919
Debug.checkDefined(nodeVisitor(node.type, visitor, isTypeNode)),
919920
);

tests/baselines/reference/api/typescript.d.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5196,10 +5196,11 @@ declare namespace ts {
51965196
readonly kind: SyntaxKind.TupleType;
51975197
readonly elements: NodeArray<TypeNode | NamedTupleMember>;
51985198
}
5199+
type NamedTupleMemberName = Identifier | NoSubstitutionTemplateLiteral | TemplateLiteralTypeNode;
51995200
interface NamedTupleMember extends TypeNode, Declaration, JSDocContainer {
52005201
readonly kind: SyntaxKind.NamedTupleMember;
52015202
readonly dotDotDotToken?: Token<SyntaxKind.DotDotDotToken>;
5202-
readonly name: Identifier;
5203+
readonly name: NamedTupleMemberName;
52035204
readonly questionToken?: Token<SyntaxKind.QuestionToken>;
52045205
readonly type: TypeNode;
52055206
}
@@ -7932,8 +7933,8 @@ declare namespace ts {
79327933
updateArrayTypeNode(node: ArrayTypeNode, elementType: TypeNode): ArrayTypeNode;
79337934
createTupleTypeNode(elements: readonly (TypeNode | NamedTupleMember)[]): TupleTypeNode;
79347935
updateTupleTypeNode(node: TupleTypeNode, elements: readonly (TypeNode | NamedTupleMember)[]): TupleTypeNode;
7935-
createNamedTupleMember(dotDotDotToken: DotDotDotToken | undefined, name: Identifier, questionToken: QuestionToken | undefined, type: TypeNode): NamedTupleMember;
7936-
updateNamedTupleMember(node: NamedTupleMember, dotDotDotToken: DotDotDotToken | undefined, name: Identifier, questionToken: QuestionToken | undefined, type: TypeNode): NamedTupleMember;
7936+
createNamedTupleMember(dotDotDotToken: DotDotDotToken | undefined, name: NamedTupleMemberName, questionToken: QuestionToken | undefined, type: TypeNode): NamedTupleMember;
7937+
updateNamedTupleMember(node: NamedTupleMember, dotDotDotToken: DotDotDotToken | undefined, name: NamedTupleMemberName, questionToken: QuestionToken | undefined, type: TypeNode): NamedTupleMember;
79377938
createOptionalTypeNode(type: TypeNode): OptionalTypeNode;
79387939
updateOptionalTypeNode(node: OptionalTypeNode, type: TypeNode): OptionalTypeNode;
79397940
createRestTypeNode(type: TypeNode): RestTypeNode;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
instantiableTupleLabels1.ts(4,27): error TS5087: A labeled tuple element is declared as rest with a '...' before the name, rather than before the type.
2+
instantiableTupleLabels1.ts(15,24): error TS2322: Type 'unknown' is not assignable to type 'string | number | bigint | boolean | null | undefined'.
3+
instantiableTupleLabels1.ts(18,46): error TS2322: Type 'T1' is not assignable to type 'string | number | bigint | boolean | null | undefined'.
4+
instantiableTupleLabels1.ts(18,71): error TS2322: Type 'T2' is not assignable to type 'string | number | bigint | boolean | null | undefined'.
5+
6+
7+
==== instantiableTupleLabels1.ts (4 errors) ====
8+
type T1 = [`wow`: boolean];
9+
type T2 = [number, `wow`: boolean];
10+
type T3 = [number, ...`wow`: boolean[]];
11+
type T4 = [number, `wow`: ...boolean[]]; // error
12+
~~~~~~~~~~~~
13+
!!! error TS5087: A labeled tuple element is declared as rest with a '...' before the name, rather than before the type.
14+
15+
type Prefix = 'pre';
16+
17+
type T5 = [`${Prefix}wow`: boolean];
18+
type T6 = [number, `${Prefix}wow`: boolean];
19+
type T7 = [number, ...`${Prefix}wow`: boolean[]];
20+
21+
type T8 = [number, `${never}wontfly`: boolean]; // no label displayed
22+
type T9 = [number, `${any}wontfly`: boolean]; // no label displayed
23+
type T11 = [number, `${"a" | "b"}wontfly`: boolean]; // no label displayed
24+
type T12 = [number, `${unknown}wontfly`: boolean]; // error
25+
~~~~~~~
26+
!!! error TS2322: Type 'unknown' is not assignable to type 'string | number | bigint | boolean | null | undefined'.
27+
28+
type MakeTuple1<T1 extends string, T2 extends string> = [number, `second-${T1}`: string, ...`rest-${T2}`: boolean[]];
29+
type MakeTuple2<T1, T2> = [number, `second-${T1}`: string, ...`rest-${T2}`: boolean[]]; // error
30+
~~
31+
!!! error TS2322: Type 'T1' is not assignable to type 'string | number | bigint | boolean | null | undefined'.
32+
!!! related TS2208 instantiableTupleLabels1.ts:18:17: This type parameter might need an `extends string | number | bigint | boolean | null | undefined` constraint.
33+
~~
34+
!!! error TS2322: Type 'T2' is not assignable to type 'string | number | bigint | boolean | null | undefined'.
35+
!!! related TS2208 instantiableTupleLabels1.ts:18:21: This type parameter might need an `extends string | number | bigint | boolean | null | undefined` constraint.
36+
37+
type T13 = MakeTuple1<"awesome", "tail">;
38+
type T14 = MakeTuple1<any, "tail">;
39+
type T15 = MakeTuple1<"a" | "b", "tail">;
40+

0 commit comments

Comments
 (0)