Skip to content

Allow self referential type aliases #33018

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
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
83 changes: 78 additions & 5 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ namespace ts {
let instantiationCount = 0;
let instantiationDepth = 0;
let constraintDepth = 0;
let mapperIdCount = 0;
let currentNode: Node | undefined;

const emptySymbols = createSymbolTable();
Expand Down Expand Up @@ -5028,6 +5029,17 @@ namespace ts {
return true;
}

/**
* Checks the circularity stack like `pushTypeResolution` without making any edits
*/
function peekTypeResolution(target: TypeSystemEntity, propertyName: TypeSystemPropertyName) {
const resolutionCycleStartIndex = findResolutionCycleStartIndex(target, propertyName);
if (resolutionCycleStartIndex >= 0) {
return false;
}
return true;
}

function findResolutionCycleStartIndex(target: TypeSystemEntity, propertyName: TypeSystemPropertyName): number {
for (let i = resolutionTargets.length - 1; i >= 0; i--) {
if (hasType(resolutionTargets[i], resolutionPropertyNames[i])) {
Expand Down Expand Up @@ -6531,6 +6543,12 @@ namespace ts {
// If typeNode is missing, we will error in checkJSDocTypedefTag.
let type = typeNode ? getTypeFromTypeNode(typeNode) : errorType;

if (type.flags & TypeFlags.Substitution) {
// Trigger resolution of any deferred substiutes that are directly assigned to the alias so they
// are marked as circular and the alias becomes `any`
void (type as SubstitutionType).substitute; // tslint:disable-line:no-unused-expression
}

if (popTypeResolution()) {
const typeParameters = getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol);
if (typeParameters) {
Expand Down Expand Up @@ -9145,6 +9163,18 @@ namespace ts {
* declared type. Instantiations are cached using the type identities of the type arguments as the key.
*/
function getTypeFromTypeAliasReference(node: NodeWithTypeArguments, symbol: Symbol, typeArguments: Type[] | undefined): Type {
if (!peekTypeResolution(symbol, TypeSystemPropertyName.DeclaredType)) {
const links = getNodeLinks(node);
let cb = links.substituteCallback;
if (!cb) {
cb = links.substituteCallback = () => getTypeFromTypeAliasReferenceWorker(node, symbol, typeArguments);
}
return getDeferredSubstitutionType(cb, symbol, typeArguments);
}
return getTypeFromTypeAliasReferenceWorker(node, symbol, typeArguments);
}

function getTypeFromTypeAliasReferenceWorker(node: NodeWithTypeArguments, symbol: Symbol, typeArguments: Type[] | undefined) {
const type = getDeclaredTypeOfSymbol(symbol);
const typeParameters = getSymbolLinks(symbol).typeParameters;
if (typeParameters) {
Expand Down Expand Up @@ -9284,6 +9314,33 @@ namespace ts {
return result;
}

/**
* Manufactures a substitute whose typeVariable/substitute are the same type,
* and whose values are not generated until accessed.
*/
function getDeferredSubstitutionType(cb: DeferredSubsCallback, aliasSymbol: Symbol, aliasTypeArguments: readonly Type[] | undefined) {
const id = cb.deferredSubsId;
if (id) {
return substitutionTypes.get(id)!;
}
const result = <SubstitutionType>createType(TypeFlags.Substitution);
result.aliasSymbol = aliasSymbol; // Yep - a substitute with a type alias. Without this, a bunch of anonymous self referential types would probably blow up in printback
result.aliasTypeArguments = aliasTypeArguments;
let cached: Type;
Object.defineProperty(result, "typeVariable", {
get() {
return cached || (cached = cb());
}
});
Object.defineProperty(result, "substitute", {
get() {
return cached || (cached = cb());
}
});
substitutionTypes.set(cb.deferredSubsId = "" + getTypeId(result), result);
return result;
}

function isUnaryTupleTypeNode(node: TypeNode) {
return node.kind === SyntaxKind.TupleType && (<TupleTypeNode>node).elementTypes.length === 1;
}
Expand Down Expand Up @@ -11643,6 +11700,10 @@ namespace ts {
return result;
}

function getTypeMapperId(mapper: TypeMapper) {
return mapper.id || (mapper.id = ++mapperIdCount);
}

function instantiateTypeWorker(type: Type, mapper: TypeMapper): Type {
const flags = type.flags;
if (flags & TypeFlags.TypeParameter) {
Expand Down Expand Up @@ -11687,16 +11748,28 @@ namespace ts {
return getConditionalTypeInstantiation(<ConditionalType>type, combineTypeMappers((<ConditionalType>type).mapper, mapper));
}
if (flags & TypeFlags.Substitution) {
const maybeVariable = instantiateType((<SubstitutionType>type).typeVariable, mapper);
const sub = (type as SubstitutionType);
if (substitutionTypes.has("" + getTypeId(sub))) {
// substitution is deferred - wrap it
const mapperId = "" + getTypeMapperId(mapper);
let cb = sub.deferredInstantiationCallbacks && sub.deferredInstantiationCallbacks.get(mapperId);
if (!cb) {
cb = () => instantiateType(sub.substitute, mapper);
(sub.deferredInstantiationCallbacks || (sub.deferredInstantiationCallbacks = createMap())).set(mapperId, cb);
}

return getDeferredSubstitutionType(cb, sub.aliasSymbol!, instantiateTypes(sub.aliasTypeArguments, mapper));
}
const maybeVariable = instantiateType(sub.typeVariable, mapper);
if (maybeVariable.flags & TypeFlags.TypeVariable) {
return getSubstitutionType(maybeVariable as TypeVariable, instantiateType((<SubstitutionType>type).substitute, mapper));
return getSubstitutionType(maybeVariable as TypeVariable, instantiateType(sub.substitute, mapper));
}
else {
const sub = instantiateType((<SubstitutionType>type).substitute, mapper);
if (sub.flags & TypeFlags.AnyOrUnknown || isTypeAssignableTo(getRestrictiveInstantiation(maybeVariable), getRestrictiveInstantiation(sub))) {
const newSub = instantiateType(sub.substitute, mapper);
if (newSub.flags & TypeFlags.AnyOrUnknown || isTypeAssignableTo(getRestrictiveInstantiation(maybeVariable), getRestrictiveInstantiation(newSub))) {
return maybeVariable;
}
return sub;
return newSub;
}
}
return type;
Expand Down
14 changes: 13 additions & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3953,6 +3953,13 @@ namespace ts {
contextFreeType?: Type; // Cached context-free type used by the first pass of inference; used when a function's return is partially contextually sensitive
deferredNodes?: Map<Node>; // Set of nodes whose checking has been deferred
capturedBlockScopeBindings?: Symbol[]; // Block-scoped bindings captured beneath this part of an IterationStatement
substituteCallback?: DeferredSubsCallback; // For a type alias reference, the callback manufactured to create the reference to the alias in a deferred way
}

/* @internal */
export interface DeferredSubsCallback {
(): Type;
deferredSubsId?: string;
}

export const enum TypeFlags {
Expand Down Expand Up @@ -4428,6 +4435,8 @@ namespace ts {
export interface SubstitutionType extends InstantiableType {
typeVariable: TypeVariable; // Target type variable
substitute: Type; // Type to substitute for type parameter
/* @internal */
deferredInstantiationCallbacks?: Map<DeferredSubsCallback>; // list of callbacks used to instantiate in a deferred way - stored to cache
}

/* @internal */
Expand Down Expand Up @@ -4490,7 +4499,10 @@ namespace ts {
}

/* @internal */
export type TypeMapper = (t: TypeParameter) => Type;
export interface TypeMapper {
(t: TypeParameter): Type;
id?: number; // lazily assigned
}

export const enum InferencePriority {
NakedTypeVariable = 1 << 0, // Naked type variable in union or intersection type
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
tests/cases/conformance/types/specifyingTypes/typeQueries/circularTypeofWithVarOrFunc.ts(1,6): error TS2456: Type alias 'typeAlias1' circularly references itself.
tests/cases/conformance/types/specifyingTypes/typeQueries/circularTypeofWithVarOrFunc.ts(2,5): error TS2502: 'varOfAliasedType1' is referenced directly or indirectly in its own type annotation.
tests/cases/conformance/types/specifyingTypes/typeQueries/circularTypeofWithVarOrFunc.ts(4,5): error TS2502: 'varOfAliasedType2' is referenced directly or indirectly in its own type annotation.
tests/cases/conformance/types/specifyingTypes/typeQueries/circularTypeofWithVarOrFunc.ts(5,6): error TS2456: Type alias 'typeAlias2' circularly references itself.
tests/cases/conformance/types/specifyingTypes/typeQueries/circularTypeofWithVarOrFunc.ts(7,18): error TS2577: Return type annotation circularly references itself.
tests/cases/conformance/types/specifyingTypes/typeQueries/circularTypeofWithVarOrFunc.ts(9,6): error TS2456: Type alias 'typeAlias3' circularly references itself.
tests/cases/conformance/types/specifyingTypes/typeQueries/circularTypeofWithVarOrFunc.ts(18,6): error TS2456: Type alias 'R' circularly references itself.
tests/cases/conformance/types/specifyingTypes/typeQueries/circularTypeofWithVarOrFunc.ts(19,29): error TS2577: Return type annotation circularly references itself.
tests/cases/conformance/types/specifyingTypes/typeQueries/circularTypeofWithVarOrFunc.ts(25,6): error TS2456: Type alias 'R2' circularly references itself.
tests/cases/conformance/types/specifyingTypes/typeQueries/circularTypeofWithVarOrFunc.ts(26,15): error TS2577: Return type annotation circularly references itself.


==== tests/cases/conformance/types/specifyingTypes/typeQueries/circularTypeofWithVarOrFunc.ts (10 errors) ====
==== tests/cases/conformance/types/specifyingTypes/typeQueries/circularTypeofWithVarOrFunc.ts (8 errors) ====
type typeAlias1 = typeof varOfAliasedType1;
~~~~~~~~~~
!!! error TS2456: Type alias 'typeAlias1' circularly references itself.
Expand All @@ -19,15 +17,11 @@ tests/cases/conformance/types/specifyingTypes/typeQueries/circularTypeofWithVarO
!!! error TS2502: 'varOfAliasedType1' is referenced directly or indirectly in its own type annotation.

var varOfAliasedType2: typeAlias2;
~~~~~~~~~~~~~~~~~
!!! error TS2502: 'varOfAliasedType2' is referenced directly or indirectly in its own type annotation.
type typeAlias2 = typeof varOfAliasedType2;
~~~~~~~~~~
!!! error TS2456: Type alias 'typeAlias2' circularly references itself.

function func(): typeAlias3 { return null; }
~~~~~~~~~~
!!! error TS2577: Return type annotation circularly references itself.
var varOfAliasedType3 = func();
type typeAlias3 = typeof varOfAliasedType3;
~~~~~~~~~~
Expand Down
14 changes: 7 additions & 7 deletions tests/baselines/reference/circularTypeofWithVarOrFunc.types
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,24 @@ var varOfAliasedType1: typeAlias1;
>varOfAliasedType1 : any

var varOfAliasedType2: typeAlias2;
>varOfAliasedType2 : any
>varOfAliasedType2 : typeAlias2

type typeAlias2 = typeof varOfAliasedType2;
>typeAlias2 : any
>varOfAliasedType2 : any
>varOfAliasedType2 : typeAlias2

function func(): typeAlias3 { return null; }
>func : () => any
>func : () => typeAlias3
>null : null

var varOfAliasedType3 = func();
>varOfAliasedType3 : any
>func() : any
>func : () => any
>varOfAliasedType3 : typeAlias3
>func() : typeAlias3
>func : () => typeAlias3

type typeAlias3 = typeof varOfAliasedType3;
>typeAlias3 : any
>varOfAliasedType3 : any
>varOfAliasedType3 : typeAlias3

// Repro from #26104

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,9 @@ tests/cases/conformance/types/typeAliases/directDependenceBetweenTypeAliases.ts(
tests/cases/conformance/types/typeAliases/directDependenceBetweenTypeAliases.ts(5,6): error TS2456: Type alias 'T0_1' circularly references itself.
tests/cases/conformance/types/typeAliases/directDependenceBetweenTypeAliases.ts(6,6): error TS2456: Type alias 'T0_2' circularly references itself.
tests/cases/conformance/types/typeAliases/directDependenceBetweenTypeAliases.ts(7,6): error TS2456: Type alias 'T0_3' circularly references itself.
tests/cases/conformance/types/typeAliases/directDependenceBetweenTypeAliases.ts(11,6): error TS2456: Type alias 'T1' circularly references itself.
tests/cases/conformance/types/typeAliases/directDependenceBetweenTypeAliases.ts(14,6): error TS2456: Type alias 'T2' circularly references itself.
tests/cases/conformance/types/typeAliases/directDependenceBetweenTypeAliases.ts(16,6): error TS2456: Type alias 'T2_1' circularly references itself.
tests/cases/conformance/types/typeAliases/directDependenceBetweenTypeAliases.ts(19,6): error TS2456: Type alias 'T3' circularly references itself.
tests/cases/conformance/types/typeAliases/directDependenceBetweenTypeAliases.ts(22,6): error TS2456: Type alias 'T4' circularly references itself.
tests/cases/conformance/types/typeAliases/directDependenceBetweenTypeAliases.ts(25,5): error TS2502: 'x' is referenced directly or indirectly in its own type annotation.
tests/cases/conformance/types/typeAliases/directDependenceBetweenTypeAliases.ts(26,6): error TS2456: Type alias 'T5' circularly references itself.
tests/cases/conformance/types/typeAliases/directDependenceBetweenTypeAliases.ts(29,6): error TS2456: Type alias 'T6' circularly references itself.
tests/cases/conformance/types/typeAliases/directDependenceBetweenTypeAliases.ts(30,6): error TS2456: Type alias 'T7' circularly references itself.
tests/cases/conformance/types/typeAliases/directDependenceBetweenTypeAliases.ts(31,5): error TS2502: 'yy' is referenced directly or indirectly in its own type annotation.
tests/cases/conformance/types/typeAliases/directDependenceBetweenTypeAliases.ts(32,6): error TS2456: Type alias 'T8' circularly references itself.


==== tests/cases/conformance/types/typeAliases/directDependenceBetweenTypeAliases.ts (15 errors) ====
==== tests/cases/conformance/types/typeAliases/directDependenceBetweenTypeAliases.ts (4 errors) ====
// It is an error for the type specified in a type alias to depend on that type alias

// A type alias directly depends on the type it aliases.
Expand All @@ -35,49 +24,27 @@ tests/cases/conformance/types/typeAliases/directDependenceBetweenTypeAliases.ts(
// A type reference directly depends on the referenced type and each of the type arguments, if any.
interface I<T> {}
type T1 = I<T1>
~~
!!! error TS2456: Type alias 'T1' circularly references itself.

// A union type directly depends on each of the constituent types.
type T2 = T2 | string
~~
!!! error TS2456: Type alias 'T2' circularly references itself.
class C<T> {}
type T2_1 = T2_1[] | number
~~~~
!!! error TS2456: Type alias 'T2_1' circularly references itself.

// An array type directly depends on its element type.
type T3 = T3[]
~~
!!! error TS2456: Type alias 'T3' circularly references itself.

// A tuple type directly depends on each of its element types.
type T4 = [number, T4]
~~
!!! error TS2456: Type alias 'T4' circularly references itself.

// A type query directly depends on the type of the referenced entity.
var x: T5[] = []
~
!!! error TS2502: 'x' is referenced directly or indirectly in its own type annotation.
type T5 = typeof x
~~
!!! error TS2456: Type alias 'T5' circularly references itself.

class C1<T> {}
type T6 = T7 | number
~~
!!! error TS2456: Type alias 'T6' circularly references itself.
type T7 = typeof yy
~~
!!! error TS2456: Type alias 'T7' circularly references itself.
var yy: [string, T8[]];
~~
!!! error TS2502: 'yy' is referenced directly or indirectly in its own type annotation.
type T8 = C<T6>
~~
!!! error TS2456: Type alias 'T8' circularly references itself.

// legal cases
type T9 = () => T9
Expand Down
26 changes: 13 additions & 13 deletions tests/baselines/reference/directDependenceBetweenTypeAliases.types
Original file line number Diff line number Diff line change
Expand Up @@ -17,50 +17,50 @@ type T0_3 = T0_1
// A type reference directly depends on the referenced type and each of the type arguments, if any.
interface I<T> {}
type T1 = I<T1>
>T1 : any
>T1 : I<T1>

// A union type directly depends on each of the constituent types.
type T2 = T2 | string
>T2 : any
>T2 : T2

class C<T> {}
>C : C<T>

type T2_1 = T2_1[] | number
>T2_1 : any
>T2_1 : T2_1

// An array type directly depends on its element type.
type T3 = T3[]
>T3 : any
>T3 : T3[]

// A tuple type directly depends on each of its element types.
type T4 = [number, T4]
>T4 : any
>T4 : [number, T4]

// A type query directly depends on the type of the referenced entity.
var x: T5[] = []
>x : any
>x : T5[]
>[] : undefined[]

type T5 = typeof x
>T5 : any
>x : any
>T5 : T5[]
>x : T5[]

class C1<T> {}
>C1 : C1<T>

type T6 = T7 | number
>T6 : any
>T6 : T6

type T7 = typeof yy
>T7 : any
>yy : any
>T7 : [string, C<T6>[]]
>yy : [string, C<T6>[]]

var yy: [string, T8[]];
>yy : any
>yy : [string, C<T6>[]]

type T8 = C<T6>
>T8 : any
>T8 : C<T6>

// legal cases
type T9 = () => T9
Expand Down
Loading