Skip to content

Inline type aliases #30979

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 3 commits into from
Closed
Show file tree
Hide file tree
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
23 changes: 23 additions & 0 deletions src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ namespace ts {
HasLocals = 1 << 5,
IsInterface = 1 << 6,
IsObjectLiteralOrClassExpressionMethod = 1 << 7,
// Indicates that the container isn't stored as a local/export in its parent, but is stored as a local within itself
IsSelfContained = 1 << 8,
}

let flowNodeCreated: <T extends FlowNode>(node: T) => T = identity;
Expand Down Expand Up @@ -502,6 +504,15 @@ namespace ts {
}
}

function bindSelfContainedDeclaration(node: Declaration) {
switch (node.kind) {
case SyntaxKind.InlineTypeAliasDeclaration:
return declareSymbolAndAddToSymbolTable(node, SymbolFlags.TypeAlias, SymbolFlags.TypeAliasExcludes);
default:
return Debug.fail("Unhandled self-containing declaration kind");
}
}

// All container nodes are kept on a linked list in declaration order. This list is used by
// the getLocalNameOfContainer function in the type checker to validate that the local name
// used for a container is unique.
Expand Down Expand Up @@ -539,6 +550,10 @@ namespace ts {
container.locals = createSymbolTable();
}
addToContainerChain(container);
if (containerFlags & ContainerFlags.IsSelfContained) {
// We have to bind self-contained structures _after_ setting up the container symbol tables, but _before_ binding children
bindSelfContainedDeclaration(<Declaration>container);
}
}
else if (containerFlags & ContainerFlags.IsBlockScopedContainer) {
blockScopeContainer = node;
Expand Down Expand Up @@ -1483,6 +1498,9 @@ namespace ts {
case SyntaxKind.MappedType:
return ContainerFlags.IsContainer | ContainerFlags.HasLocals;

case SyntaxKind.InlineTypeAliasDeclaration:
return ContainerFlags.IsContainer | ContainerFlags.HasLocals | ContainerFlags.IsSelfContained;

case SyntaxKind.SourceFile:
return ContainerFlags.IsContainer | ContainerFlags.IsControlFlowContainer | ContainerFlags.HasLocals;

Expand Down Expand Up @@ -1601,6 +1619,7 @@ namespace ts {
case SyntaxKind.JSDocTypedefTag:
case SyntaxKind.JSDocCallbackTag:
case SyntaxKind.TypeAliasDeclaration:
case SyntaxKind.InlineTypeAliasDeclaration:
case SyntaxKind.MappedType:
// All the children of these container types are never visible through another
// symbol (i.e. through another symbol's 'exports' or 'members'). Instead,
Expand Down Expand Up @@ -2183,6 +2202,8 @@ namespace ts {
return;
case SyntaxKind.TypePredicate:
break; // Binding the children will handle everything
case SyntaxKind.InlineTypeAliasDeclaration:
break; // Container binding should have handled everything
case SyntaxKind.TypeParameter:
return bindTypeParameter(node as TypeParameterDeclaration);
case SyntaxKind.Parameter:
Expand Down Expand Up @@ -3751,6 +3772,7 @@ namespace ts {
case SyntaxKind.ParenthesizedType:
case SyntaxKind.InterfaceDeclaration:
case SyntaxKind.TypeAliasDeclaration:
case SyntaxKind.InlineTypeAliasDeclaration:
case SyntaxKind.ThisType:
case SyntaxKind.TypeOperator:
case SyntaxKind.IndexedAccessType:
Expand Down Expand Up @@ -3920,6 +3942,7 @@ namespace ts {
case SyntaxKind.IndexSignature:
case SyntaxKind.InterfaceDeclaration:
case SyntaxKind.TypeAliasDeclaration:
case SyntaxKind.InlineTypeAliasDeclaration:
return TransformFlags.TypeExcludes;
case SyntaxKind.ObjectLiteralExpression:
return TransformFlags.ObjectLiteralExcludes;
Expand Down
74 changes: 66 additions & 8 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3606,6 +3606,12 @@ namespace ts {
? symbolToTypeNode(type.symbol, context, SymbolFlags.Type)
: createTypeReferenceNode(createIdentifier("?"), /*typeArguments*/ undefined);
}
if (context.visitedTypes && context.visitedTypes.has("" + getTypeId(type))) {
// _Within_ an alias symbol declaration, references to the alias type should be the alias name
// If this is done, we need to mark the type as needing to preserve the alias when we get back up to it.
context.visitedTypes.set("" + getTypeId(type), true);
return createTypeReferenceNode(getNameForInlineTypeAliasCreation(type, context), /*typeArguments*/ undefined);
}
if (!inTypeAlias && type.aliasSymbol && (context.flags & NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope || isTypeSymbolAccessible(type.aliasSymbol, context.enclosingDeclaration))) {
const typeArgumentNodes = mapToTypeNodes(type.aliasTypeArguments, context);
if (isReservedMemberName(type.aliasSymbol.escapedName) && !(type.aliasSymbol.flags & SymbolFlags.Class)) return createTypeReferenceNode(createIdentifier(""), typeArgumentNodes);
Expand Down Expand Up @@ -3715,7 +3721,7 @@ namespace ts {
// Since instantiations of the same anonymous type have the same symbol, tracking symbols instead
// of types allows us to catch circular references to instantiations of the same anonymous type
if (!context.visitedTypes) {
context.visitedTypes = createMap<true>();
context.visitedTypes = createMap<boolean>();
}
if (!context.symbolDepth) {
context.symbolDepth = createMap<number>();
Expand All @@ -3726,8 +3732,13 @@ namespace ts {
return createElidedInformationPlaceholder(context);
}
context.symbolDepth.set(id, depth + 1);
context.visitedTypes.set(typeId, true);
const result = createTypeNodeFromObjectType(type);
Debug.assert(!context.visitedTypes.has(typeId));
context.visitedTypes.set(typeId, false);
let result = createTypeNodeFromObjectType(type);
if (context.visitedTypes.get(typeId) === true) {
// Circular reference is required - emit an inline type alias
result = createInlineTypeAliasDeclaration(getNameForInlineTypeAliasCreation(type, context), result);
}
context.visitedTypes.delete(typeId);
context.symbolDepth.set(id, depth);
return result;
Expand All @@ -3752,6 +3763,35 @@ namespace ts {
}
}

function nameShadowsNameInScope(escapedName: __String, context: NodeBuilderContext) {
return !!resolveName(context.enclosingDeclaration, escapedName, SymbolFlags.Type, /*nameNotFoundArg*/ undefined, escapedName, /*isUse*/ false);
}

function getSyntheticTypeNameForType(type: Type, context: NodeBuilderContext, seed: string): string {
const typeId = "" + getTypeId(type);
if (context.syntheticTypeNames && context.syntheticTypeNames.get(typeId)) {
return context.syntheticTypeNames.get(typeId)!;
}
let i = 0;
let text = seed;
while ((context.usedSyntheticTypeNames && context.usedSyntheticTypeNames.get(text)) || nameShadowsNameInScope(escapeLeadingUnderscores(text), context)) {
i++;
text = `${seed}_${i}`;
}
(context.usedSyntheticTypeNames || (context.usedSyntheticTypeNames = createMap())).set(text, true);
(context.syntheticTypeNames || (context.syntheticTypeNames = createMap())).set(typeId, text);
return text;
}

function getNameForInlineTypeAliasCreation(type: Type, context: NodeBuilderContext): Identifier {
// If it was originally made using an inline type alias, reuse that name, otherwise
// get a generated name for the (usually anonymous) type node which contains the
// circular reference
return type.aliasSymbol && some(type.aliasSymbol.declarations, isInlineTypeAlias)
? createIdentifier(unescapeLeadingUnderscores(type.aliasSymbol.escapedName))
: createIdentifier(getSyntheticTypeNameForType(type, context, "Anon"));
}

function createTypeNodeFromObjectType(type: ObjectType): TypeNode {
if (isGenericMappedType(type)) {
return createMappedTypeNodeFromType(type);
Expand Down Expand Up @@ -4632,12 +4672,18 @@ namespace ts {

// State
encounteredError: boolean;
visitedTypes: Map<true> | undefined;
/**
* Entry is set to `false` when first encountered, and `true` if a circular reference
* was emitted (and thus needs to has its a name emitted)
*/
visitedTypes: Map<boolean> | undefined;
symbolDepth: Map<number> | undefined;
inferTypeParameters: TypeParameter[] | undefined;
approximateLength: number;
truncating?: boolean;
typeParameterSymbolList?: Map<true>;
usedSyntheticTypeNames?: Map<true>;
syntheticTypeNames?: Map<string>;
}

function isDefaultBindingContext(location: Node) {
Expand Down Expand Up @@ -4723,6 +4769,8 @@ namespace ts {

function determineIfDeclarationIsVisible() {
switch (node.kind) {
case SyntaxKind.InlineTypeAliasDeclaration:
return true; // Always consider inline type alias declaration as "visible" when they were reachable (since they can only be referenced from a child)
case SyntaxKind.JSDocCallbackTag:
case SyntaxKind.JSDocTypedefTag:
// Top-level jsdoc type aliases are considered exported
Expand Down Expand Up @@ -6339,8 +6387,8 @@ namespace ts {
return errorType;
}

const declaration = <JSDocTypedefTag | JSDocCallbackTag | TypeAliasDeclaration>find(symbol.declarations, d =>
isJSDocTypeAlias(d) || d.kind === SyntaxKind.TypeAliasDeclaration);
const declaration = <JSDocTypedefTag | JSDocCallbackTag | TypeAliasDeclaration | InlineTypeAliasDeclaration>find(symbol.declarations, d =>
isJSDocTypeAlias(d) || d.kind === SyntaxKind.TypeAliasDeclaration || d.kind === SyntaxKind.InlineTypeAliasDeclaration);
const typeNode = isJSDocTypeAlias(declaration) ? declaration.typeExpression : declaration.type;
// If typeNode is missing, we will error in checkJSDocTypedefTag.
let type = typeNode ? getTypeFromTypeNode(typeNode) : errorType;
Expand Down Expand Up @@ -10673,7 +10721,7 @@ namespace ts {
}

function getAliasSymbolForTypeNode(node: TypeNode) {
return isTypeAlias(node.parent) ? getSymbolOfNode(node.parent) : undefined;
return isTypeAlias(node.parent) || isInlineTypeAlias(node.parent) ? getSymbolOfNode(node.parent) : undefined;
}

function getTypeArgumentsForAliasSymbol(symbol: Symbol | undefined) {
Expand All @@ -10684,6 +10732,14 @@ namespace ts {
return !!(type.flags & TypeFlags.Object) && !isGenericMappedType(type);
}

function getTypeFromInlineTypeAliasDeclaration(node: InlineTypeAliasDeclaration): Type {
const links = getNodeLinks(node);
if (!links.resolvedType) {
links.resolvedType = getDeclaredTypeOfTypeAlias(node.symbol);
}
return links.resolvedType;
}

/**
* Since the source of spread types are object literals, which are not binary,
* this function should be called in a left folding style, with left = previous result of getSpreadType
Expand Down Expand Up @@ -10985,6 +11041,8 @@ namespace ts {
return getTypeFromInferTypeNode(<InferTypeNode>node);
case SyntaxKind.ImportType:
return getTypeFromImportTypeNode(<ImportTypeNode>node);
case SyntaxKind.InlineTypeAliasDeclaration:
return getTypeFromInlineTypeAliasDeclaration(<InlineTypeAliasDeclaration>node);
// This function assumes that an identifier or qualified name is a type expression
// Callers should first ensure this by calling isTypeNode
case SyntaxKind.Identifier:
Expand Down Expand Up @@ -11251,7 +11309,7 @@ namespace ts {
return !!tp.isThisType;
case SyntaxKind.Identifier:
return !tp.isThisType && isPartOfTypeNode(node) && maybeTypeParameterReference(node) &&
getTypeFromTypeNode(<TypeNode>node) === tp;
getSymbolAtLocation(node) === tp.symbol;
case SyntaxKind.TypeQuery:
return true;
}
Expand Down
12 changes: 12 additions & 0 deletions src/compiler/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1221,6 +1221,8 @@ namespace ts {
return emitLiteralType(<LiteralTypeNode>node);
case SyntaxKind.ImportType:
return emitImportTypeNode(<ImportTypeNode>node);
case SyntaxKind.InlineTypeAliasDeclaration:
return emitInlineTypeAliasDeclaration(<InlineTypeAliasDeclaration>node);
case SyntaxKind.JSDocAllType:
writePunctuation("*");
return;
Expand Down Expand Up @@ -2074,6 +2076,16 @@ namespace ts {
emitTypeArguments(node, node.typeArguments);
}

function emitInlineTypeAliasDeclaration(node: InlineTypeAliasDeclaration) {
writeKeyword("type");
writeSpace();
emit(node.name);
writeSpace();
writePunctuation("=");
writeSpace();
emit(node.type);
}

//
// Binding patterns
//
Expand Down
14 changes: 14 additions & 0 deletions src/compiler/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -932,6 +932,20 @@ namespace ts {
: node;
}

export function createInlineTypeAliasDeclaration(name: Identifier | string, type: TypeNode) {
const node = createSynthesizedNode(SyntaxKind.InlineTypeAliasDeclaration) as InlineTypeAliasDeclaration;
node.name = asName(name);
node.type = type;
return node;
}

export function updateInlineTypeAliasDeclaration(node: InlineTypeAliasDeclaration, name: Identifier | string, type: TypeNode) {
return node.name !== name
|| node.type !== type
? updateNode(createInlineTypeAliasDeclaration(name, type), node)
: node;
}

// Binding Patterns

export function createObjectBindingPattern(elements: ReadonlyArray<BindingElement>) {
Expand Down
15 changes: 15 additions & 0 deletions src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,9 @@ namespace ts {
visitNode(cbNode, (<MappedTypeNode>node).type);
case SyntaxKind.LiteralType:
return visitNode(cbNode, (<LiteralTypeNode>node).literal);
case SyntaxKind.InlineTypeAliasDeclaration:
return visitNode(cbNode, (<InlineTypeAliasDeclaration>node).name) ||
visitNode(cbNode, (<InlineTypeAliasDeclaration>node).type);
case SyntaxKind.ObjectBindingPattern:
case SyntaxKind.ArrayBindingPattern:
return visitNodes(cbNode, cbNodes, (<BindingPattern>node).elements);
Expand Down Expand Up @@ -3030,6 +3033,7 @@ namespace ts {
case SyntaxKind.DotDotDotToken:
case SyntaxKind.InferKeyword:
case SyntaxKind.ImportKeyword:
case SyntaxKind.TypeKeyword:
return true;
case SyntaxKind.FunctionKeyword:
return !inStartOfParameter;
Expand Down Expand Up @@ -3119,6 +3123,8 @@ namespace ts {
return parseTypeOperator(operator);
case SyntaxKind.InferKeyword:
return parseInferType();
case SyntaxKind.TypeKeyword:
return parseInlineTypeAliasDeclaration();
}
return parsePostfixTypeOrHigher();
}
Expand Down Expand Up @@ -5996,6 +6002,15 @@ namespace ts {
return finishNode(node);
}

function parseInlineTypeAliasDeclaration(): InlineTypeAliasDeclaration {
const node = <InlineTypeAliasDeclaration>createNode(SyntaxKind.InlineTypeAliasDeclaration);
parseExpected(SyntaxKind.TypeKeyword);
node.name = parseIdentifier();
parseExpected(SyntaxKind.EqualsToken);
node.type = parseType();
return finishNode(node);
}

// In an ambient declaration, the grammar only allows integer literals as initializers.
// In a non-ambient declaration, the grammar allows uninitialized members only in a
// ConstantEnumMemberSection, which starts at the beginning of an enum declaration
Expand Down
3 changes: 2 additions & 1 deletion src/compiler/transformers/declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,8 @@ namespace ts {
|| isInterfaceDeclaration(node)
|| isFunctionLike(node)
|| isIndexSignatureDeclaration(node)
|| isMappedTypeNode(node);
|| isMappedTypeNode(node)
|| isInlineTypeAlias(node);
}

function checkEntityNameVisibility(entityName: EntityNameOrEntityNameExpression, enclosingDeclaration: Node) {
Expand Down
9 changes: 8 additions & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ namespace ts {
MappedType,
LiteralType,
ImportType,
InlineTypeAliasDeclaration,
// Binding patterns
ObjectBindingPattern,
ArrayBindingPattern,
Expand Down Expand Up @@ -495,7 +496,7 @@ namespace ts {
FirstFutureReservedWord = ImplementsKeyword,
LastFutureReservedWord = YieldKeyword,
FirstTypeNode = TypePredicate,
LastTypeNode = ImportType,
LastTypeNode = InlineTypeAliasDeclaration,
FirstPunctuation = OpenBraceToken,
LastPunctuation = CaretEqualsToken,
FirstToken = Unknown,
Expand Down Expand Up @@ -2204,6 +2205,12 @@ namespace ts {
type: TypeNode;
}

export interface InlineTypeAliasDeclaration extends TypeNode, Declaration, JSDocContainer {
kind: SyntaxKind.InlineTypeAliasDeclaration;
name: Identifier;
type: TypeNode;
}

export interface EnumMember extends NamedDeclaration, JSDocContainer {
kind: SyntaxKind.EnumMember;
parent: EnumDeclaration;
Expand Down
Loading