Skip to content

Erase type parameters to a type which behaves as never in a union and unknown in an intersection or any otherwise #39217

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 2 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
10 changes: 7 additions & 3 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,7 @@ namespace ts {
const anyType = createIntrinsicType(TypeFlags.Any, "any");
const autoType = createIntrinsicType(TypeFlags.Any, "any");
const wildcardType = createIntrinsicType(TypeFlags.Any, "any");
const erasedType = createIntrinsicType(TypeFlags.Any | TypeFlags.Never | TypeFlags.Unknown, "any");
Copy link
Member

Choose a reason for hiding this comment

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

Wow, a schizophrenic type. We have an implicit assumption everywhere that only one of these flags will be set--with a few exceptions such as TypeFlags.EnumLiteral. Are you confident there are no places we'll get confused by all three being set in this type? Tests for these now become order dependent.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep, I'm literally abusing the order dependence of handling each of these to avoid special-casing the handling of this type in most type construction places (partially because we're essentially out of normal type flags). Generally speaking we handle Any, then Never, then Unknown (if we handle both), so it's OK, since this marker is mostly any-like (except in unions/intersections, where we handle unknown or never, and then any, which is what causes the desirable vaporization in both structures). At the start, I also wanted to add ObjectFlags.NonInferrableType to it, but that has the side effect of cases like <T extends T[]>() => T where we currently infer any[] becoming unknown. I don't think any[] is great (it injects any into the program without the user ever explicitly writing it, and it's not an noImplicitAny error!), but unknown isn't really much better. I think the ideal would be unknown[] - but that relies on knowing that T[] is covariant on T... I guess since we use getVariances in inference already, that should be safe. So In theory I could get what I want by mapping this erased type to unknown in covariant positions and never in contravariant positions, when it persists into the constructed type (rather than just vaporizing in a union or intersection and letting it persist beyond that as a psuedo-any). I could also make this a special type parameter, and handle all this with a special instantiator that tracks some context of the instantiation - in fact, to do the variance bit, I probably need to.

const errorType = createIntrinsicType(TypeFlags.Any, "error");
const nonInferrableAnyType = createIntrinsicType(TypeFlags.Any, "any", ObjectFlags.ContainsWideningType);
const unknownType = createIntrinsicType(TypeFlags.Unknown, "unknown");
Expand Down Expand Up @@ -12896,8 +12897,8 @@ namespace ts {
typeSet.set(type.id.toString(), type);
}
}
else {
if (flags & TypeFlags.AnyOrUnknown) {
else if (!(flags & TypeFlags.Unknown)) {
if (flags & TypeFlags.Any) {
if (type === wildcardType) includes |= TypeFlags.IncludesWildcard;
}
else if ((strictNullChecks || !(flags & TypeFlags.Nullable)) && !typeSet.has(type.id.toString())) {
Expand Down Expand Up @@ -13604,6 +13605,9 @@ namespace ts {
if (objectType === wildcardType || indexType === wildcardType) {
return wildcardType;
}
if (objectType === erasedType || indexType === erasedType) {
return erasedType;
}
// If the object type has a string index signature and no other members we know that the result will
// always be the type of that index signature and we can simplify accordingly.
if (isStringIndexSignatureOnlyType(objectType) && !(indexType.flags & TypeFlags.Nullable) && isTypeAssignableToKind(indexType, TypeFlags.String | TypeFlags.Number)) {
Expand Down Expand Up @@ -14461,7 +14465,7 @@ namespace ts {
}

function createTypeEraser(sources: readonly TypeParameter[]): TypeMapper {
return createTypeMapper(sources, /*targets*/ undefined);
return createTypeMapper(sources, arrayOf(sources.length, () => erasedType));
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
tests/cases/compiler/immutable.ts(189,20): error TS2430: Interface 'Stack<T>' incorrectly extends interface 'Indexed<T>'.
The types returned by 'concat(...).map(...).filter(...)' are incompatible between these types.
Type 'Set<any>' is not assignable to type 'Indexed<any>'.
Copy link
Member Author

Choose a reason for hiding this comment

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

This is just a new follow-on error to the existing errors below, I believe, as the incorrect extensions below make Set and Indexed incompatible with one another, which is now properly reported here (as we now no longer fully erase all the data in the signature and can keep comparing until we get to that point).

tests/cases/compiler/immutable.ts(341,22): error TS2430: Interface 'Keyed<K, V>' incorrectly extends interface 'Collection<K, V>'.
The types returned by 'toSeq()' are incompatible between these types.
Type 'Keyed<K, V>' is not assignable to type 'this'.
Expand Down Expand Up @@ -33,7 +36,7 @@ tests/cases/compiler/immutable.ts(391,22): error TS2430: Interface 'Set<T>' inco
flatMap<M>(mapper: (value: T, key: void, iter: this) => Ara<M>, context?: any): N2<M>;
toSeq(): N2<T>;
}
==== tests/cases/compiler/immutable.ts (3 errors) ====
==== tests/cases/compiler/immutable.ts (4 errors) ====
// Test that complex recursive collections can pass the `extends` assignability check without
// running out of memory. This bug was exposed in Typescript 2.4 when more generic signatures
// started being checked.
Expand Down Expand Up @@ -223,6 +226,10 @@ tests/cases/compiler/immutable.ts(391,22): error TS2430: Interface 'Set<T>' inco
export function Stack<T>(): Stack<T>;
export function Stack<T>(collection: Iterable<T>): Stack<T>;
export interface Stack<T> extends Collection.Indexed<T> {
~~~~~
!!! error TS2430: Interface 'Stack<T>' incorrectly extends interface 'Indexed<T>'.
!!! error TS2430: The types returned by 'concat(...).map(...).filter(...)' are incompatible between these types.
!!! error TS2430: Type 'Set<any>' is not assignable to type 'Indexed<any>'.
// Reading values
peek(): T | undefined;
// Persistent changes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1137,7 +1137,7 @@ declare module Immutable {
>Seq : typeof Seq

function isSeq(maybeSeq: any): maybeSeq is Seq.Indexed<any> | Seq.Keyed<any, any>;
>isSeq : (maybeSeq: any) => maybeSeq is Indexed<any> | Keyed<any, any>
>isSeq : (maybeSeq: any) => maybeSeq is Keyed<any, any> | Indexed<any>
>maybeSeq : any
>Seq : any
>Seq : any
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//// [functionInferenceDecomposesIntersection.ts]
declare namespace React {
type WeakValidationMap<T> = {
[K in keyof T]?: null extends T[K] ? string : string
};

interface FunctionComponent<P = {}> {
propTypes?: WeakValidationMap<P>;
}
}

type A<T1> = <T2>() => React.FunctionComponent<T1 & T2>;

function B<T>(_: A<T>) {}

interface C {
r: string;
}

function myFunction<T2>(): React.FunctionComponent<C & T2> {
return {};
}

// B<C>(myFunction) // No error
B(myFunction) // should be the same as above (T in B inferred as C)

//// [functionInferenceDecomposesIntersection.js]
function B(_) { }
function myFunction() {
return {};
}
// B<C>(myFunction) // No error
B(myFunction); // should be the same as above (T in B inferred as C)
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
=== tests/cases/compiler/functionInferenceDecomposesIntersection.ts ===
declare namespace React {
>React : Symbol(React, Decl(functionInferenceDecomposesIntersection.ts, 0, 0))

type WeakValidationMap<T> = {
>WeakValidationMap : Symbol(WeakValidationMap, Decl(functionInferenceDecomposesIntersection.ts, 0, 25))
>T : Symbol(T, Decl(functionInferenceDecomposesIntersection.ts, 1, 27))

[K in keyof T]?: null extends T[K] ? string : string
>K : Symbol(K, Decl(functionInferenceDecomposesIntersection.ts, 2, 9))
>T : Symbol(T, Decl(functionInferenceDecomposesIntersection.ts, 1, 27))
>T : Symbol(T, Decl(functionInferenceDecomposesIntersection.ts, 1, 27))
>K : Symbol(K, Decl(functionInferenceDecomposesIntersection.ts, 2, 9))

};

interface FunctionComponent<P = {}> {
>FunctionComponent : Symbol(FunctionComponent, Decl(functionInferenceDecomposesIntersection.ts, 3, 6))
>P : Symbol(P, Decl(functionInferenceDecomposesIntersection.ts, 5, 32))

propTypes?: WeakValidationMap<P>;
>propTypes : Symbol(FunctionComponent.propTypes, Decl(functionInferenceDecomposesIntersection.ts, 5, 41))
>WeakValidationMap : Symbol(WeakValidationMap, Decl(functionInferenceDecomposesIntersection.ts, 0, 25))
>P : Symbol(P, Decl(functionInferenceDecomposesIntersection.ts, 5, 32))
}
}

type A<T1> = <T2>() => React.FunctionComponent<T1 & T2>;
>A : Symbol(A, Decl(functionInferenceDecomposesIntersection.ts, 8, 1))
>T1 : Symbol(T1, Decl(functionInferenceDecomposesIntersection.ts, 10, 7))
>T2 : Symbol(T2, Decl(functionInferenceDecomposesIntersection.ts, 10, 14))
>React : Symbol(React, Decl(functionInferenceDecomposesIntersection.ts, 0, 0))
>FunctionComponent : Symbol(React.FunctionComponent, Decl(functionInferenceDecomposesIntersection.ts, 3, 6))
>T1 : Symbol(T1, Decl(functionInferenceDecomposesIntersection.ts, 10, 7))
>T2 : Symbol(T2, Decl(functionInferenceDecomposesIntersection.ts, 10, 14))

function B<T>(_: A<T>) {}
>B : Symbol(B, Decl(functionInferenceDecomposesIntersection.ts, 10, 56))
>T : Symbol(T, Decl(functionInferenceDecomposesIntersection.ts, 12, 11))
>_ : Symbol(_, Decl(functionInferenceDecomposesIntersection.ts, 12, 14))
>A : Symbol(A, Decl(functionInferenceDecomposesIntersection.ts, 8, 1))
>T : Symbol(T, Decl(functionInferenceDecomposesIntersection.ts, 12, 11))

interface C {
>C : Symbol(C, Decl(functionInferenceDecomposesIntersection.ts, 12, 25))

r: string;
>r : Symbol(C.r, Decl(functionInferenceDecomposesIntersection.ts, 14, 13))
}

function myFunction<T2>(): React.FunctionComponent<C & T2> {
>myFunction : Symbol(myFunction, Decl(functionInferenceDecomposesIntersection.ts, 16, 1))
>T2 : Symbol(T2, Decl(functionInferenceDecomposesIntersection.ts, 18, 20))
>React : Symbol(React, Decl(functionInferenceDecomposesIntersection.ts, 0, 0))
>FunctionComponent : Symbol(React.FunctionComponent, Decl(functionInferenceDecomposesIntersection.ts, 3, 6))
>C : Symbol(C, Decl(functionInferenceDecomposesIntersection.ts, 12, 25))
>T2 : Symbol(T2, Decl(functionInferenceDecomposesIntersection.ts, 18, 20))

return {};
}

// B<C>(myFunction) // No error
B(myFunction) // should be the same as above (T in B inferred as C)
>B : Symbol(B, Decl(functionInferenceDecomposesIntersection.ts, 10, 56))
>myFunction : Symbol(myFunction, Decl(functionInferenceDecomposesIntersection.ts, 16, 1))

Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
=== tests/cases/compiler/functionInferenceDecomposesIntersection.ts ===
declare namespace React {
type WeakValidationMap<T> = {
>WeakValidationMap : WeakValidationMap<T>

[K in keyof T]?: null extends T[K] ? string : string
>null : null

};

interface FunctionComponent<P = {}> {
propTypes?: WeakValidationMap<P>;
>propTypes : WeakValidationMap<P>
}
}

type A<T1> = <T2>() => React.FunctionComponent<T1 & T2>;
>A : A<T1>
>React : any

function B<T>(_: A<T>) {}
>B : <T>(_: A<T>) => void
>_ : A<T>

interface C {
r: string;
>r : string
}

function myFunction<T2>(): React.FunctionComponent<C & T2> {
>myFunction : <T2>() => React.FunctionComponent<C & T2>
>React : any

return {};
>{} : {}
}

// B<C>(myFunction) // No error
B(myFunction) // should be the same as above (T in B inferred as C)
>B(myFunction) : void
>B : <T>(_: A<T>) => void
>myFunction : <T2>() => React.FunctionComponent<C & T2>

8 changes: 4 additions & 4 deletions tests/baselines/reference/subtypingWithCallSignatures2.types
Original file line number Diff line number Diff line change
Expand Up @@ -762,8 +762,8 @@ var r15arg1 = <T>(x: T) => <T[]>null
>null : null

var r15 = foo15(r15arg1); // any
>r15 : any
>foo15(r15arg1) : any
>r15 : { (x: number): number[]; (x: string): string[]; }
>foo15(r15arg1) : { (x: number): number[]; (x: string): string[]; }
>foo15 : { (a: { (x: number): number[]; (x: string): string[]; }): { (x: number): number[]; (x: string): string[]; }; (a: any): any; }
>r15arg1 : <T>(x: T) => T[]

Expand All @@ -789,8 +789,8 @@ var r17arg1 = <T>(x: (a: T) => T) => <T[]>null;
>null : null

var r17 = foo17(r17arg1); // any
>r17 : any
>foo17(r17arg1) : any
>r17 : { (x: (a: number) => number): number[]; (x: (a: string) => string): string[]; }
>foo17(r17arg1) : { (x: (a: number) => number): number[]; (x: (a: string) => string): string[]; }
>foo17 : { (a: { (x: (a: number) => number): number[]; (x: (a: string) => string): string[]; }): { (x: (a: number) => number): number[]; (x: (a: string) => string): string[]; }; (a: any): any; }
>r17arg1 : <T>(x: (a: T) => T) => T[]

Expand Down
4 changes: 2 additions & 2 deletions tests/baselines/reference/subtypingWithCallSignatures3.types
Original file line number Diff line number Diff line change
Expand Up @@ -457,8 +457,8 @@ module Errors {
>null : null

var r8 = foo16(r8arg); // any
>r8 : any
>foo16(r8arg) : any
>r8 : { (x: { (a: number): number; (a?: number): number; }): number[]; (x: { (a: boolean): boolean; (a?: boolean): boolean; }): boolean[]; }
>foo16(r8arg) : { (x: { (a: number): number; (a?: number): number; }): number[]; (x: { (a: boolean): boolean; (a?: boolean): boolean; }): boolean[]; }
>foo16 : { (a2: { (x: { (a: number): number; (a?: number): number; }): number[]; (x: { (a: boolean): boolean; (a?: boolean): boolean; }): boolean[]; }): { (x: { (a: number): number; (a?: number): number; }): number[]; (x: { (a: boolean): boolean; (a?: boolean): boolean; }): boolean[]; }; (a2: any): any; }
>r8arg : <T>(x: (a: T) => T) => T[]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -675,8 +675,8 @@ var r15arg1: new <T>(x: T) => T[];
>x : T

var r15 = foo15(r15arg1); // any
>r15 : any
>foo15(r15arg1) : any
>r15 : { new (x: number): number[]; new (x: string): string[]; }
>foo15(r15arg1) : { new (x: number): number[]; new (x: string): string[]; }
>foo15 : { (a: { new (x: number): number[]; new (x: string): string[]; }): { new (x: number): number[]; new (x: string): string[]; }; (a: any): any; }
>r15arg1 : new <T>(x: T) => T[]

Expand All @@ -696,8 +696,8 @@ var r17arg1: new <T>(x: (a: T) => T) => T[];
>a : T

var r17 = foo17(r17arg1); // any
>r17 : any
>foo17(r17arg1) : any
>r17 : { new (x: (a: number) => number): number[]; new (x: (a: string) => string): string[]; }
>foo17(r17arg1) : { new (x: (a: number) => number): number[]; new (x: (a: string) => string): string[]; }
>foo17 : { (a: { new (x: (a: number) => number): number[]; new (x: (a: string) => string): string[]; }): { new (x: (a: number) => number): number[]; new (x: (a: string) => string): string[]; }; (a: any): any; }
>r17arg1 : new <T>(x: (a: T) => T) => T[]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -406,8 +406,8 @@ module Errors {
>a : T

var r8 = foo16(r8arg); // any
>r8 : any
>foo16(r8arg) : any
>r8 : { new (x: { new (a: number): number; new (a?: number): number; }): number[]; new (x: { new (a: boolean): boolean; new (a?: boolean): boolean; }): boolean[]; }
>foo16(r8arg) : { new (x: { new (a: number): number; new (a?: number): number; }): number[]; new (x: { new (a: boolean): boolean; new (a?: boolean): boolean; }): boolean[]; }
>foo16 : { (a2: { new (x: { new (a: number): number; new (a?: number): number; }): number[]; new (x: { new (a: boolean): boolean; new (a?: boolean): boolean; }): boolean[]; }): { new (x: { new (a: number): number; new (a?: number): number; }): number[]; new (x: { new (a: boolean): boolean; new (a?: boolean): boolean; }): boolean[]; }; (a2: any): any; }
>r8arg : new <T>(x: new (a: T) => T) => T[]

Expand Down
25 changes: 25 additions & 0 deletions tests/cases/compiler/functionInferenceDecomposesIntersection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@

declare namespace React {
type WeakValidationMap<T> = {
[K in keyof T]?: null extends T[K] ? string : string
};

interface FunctionComponent<P = {}> {
propTypes?: WeakValidationMap<P>;
}
}

type A<T1> = <T2>() => React.FunctionComponent<T1 & T2>;

function B<T>(_: A<T>) {}

interface C {
r: string;
}

function myFunction<T2>(): React.FunctionComponent<C & T2> {
return {};
}

// B<C>(myFunction) // No error
B(myFunction) // should be the same as above (T in B inferred as C)