Skip to content

Don't eagerly simplify reducible generic union index types #46812

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

Merged
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
28 changes: 25 additions & 3 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,8 @@ namespace ts {

const restrictiveMapper: TypeMapper = makeFunctionTypeMapper(t => t.flags & TypeFlags.TypeParameter ? getRestrictiveTypeParameter(t as TypeParameter) : t);
const permissiveMapper: TypeMapper = makeFunctionTypeMapper(t => t.flags & TypeFlags.TypeParameter ? wildcardType : t);
const uniqueLiteralType = createIntrinsicType(TypeFlags.Never, "never"); // `uniqueLiteralType` is a special `never` flagged by union reduction to behave as a literal
const uniqueLiteralMapper: TypeMapper = makeFunctionTypeMapper(t => t.flags & TypeFlags.TypeParameter ? uniqueLiteralType : t); // replace all type parameters with the unique literal type (disregarding constraints)

const emptyObjectType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, emptyArray);
const emptyJsxObjectType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, emptyArray);
Expand Down Expand Up @@ -12367,10 +12369,10 @@ namespace ts {
else if (type !== firstType) {
checkFlags |= CheckFlags.HasNonUniformType;
}
if (isLiteralType(type) || isPatternLiteralType(type)) {
if (isLiteralType(type) || isPatternLiteralType(type) || type === uniqueLiteralType) {
checkFlags |= CheckFlags.HasLiteralType;
}
if (type.flags & TypeFlags.Never) {
if (type.flags & TypeFlags.Never && type !== uniqueLiteralType) {
checkFlags |= CheckFlags.HasNeverType;
}
propTypes.push(type);
Expand Down Expand Up @@ -15124,9 +15126,24 @@ namespace ts {
/*aliasSymbol*/ undefined, /*aliasTypeArguments*/ undefined, origin);
}

/**
* A union type which is reducible upon instantiation (meaning some members are removed under certain instantiations)
* must be kept generic, as that instantiation information needs to flow through the type system. By replacing all
* type parameters in the union with a special never type that is treated as a literal in `getReducedType`, we can cause the `getReducedType` logic
* to reduce the resulting type if possible (since only intersections with conflicting literal-typed properties are reducible).
*/
function isPossiblyReducibleByInstantiation(type: UnionType): boolean {
return some(type.types, t => {
const uniqueFilled = getUniqueLiteralFilledInstantiation(t);
return getReducedType(uniqueFilled) !== uniqueFilled;
});
}

function getIndexType(type: Type, stringsOnly = keyofStringsOnly, noIndexSignatures?: boolean): Type {
type = getReducedType(type);
return type.flags & TypeFlags.Union ? getIntersectionType(map((type as UnionType).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) :
return type.flags & TypeFlags.Union ? isPossiblyReducibleByInstantiation(type as UnionType)
? getIndexTypeForGenericType(type as InstantiableType | UnionOrIntersectionType, stringsOnly)
: getIntersectionType(map((type as UnionType).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) :
type.flags & TypeFlags.Intersection ? getUnionType(map((type as IntersectionType).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) :
type.flags & TypeFlags.InstantiableNonPrimitive || isGenericTupleType(type) || isGenericMappedType(type) && !hasDistributiveNameType(type) ? getIndexTypeForGenericType(type as InstantiableType | UnionOrIntersectionType, stringsOnly) :
getObjectFlags(type) & ObjectFlags.Mapped ? getIndexTypeForMappedType(type as MappedType, stringsOnly, noIndexSignatures) :
Expand Down Expand Up @@ -17070,6 +17087,11 @@ namespace ts {
return type; // Nested invocation of `inferTypeForHomomorphicMappedType` or the `source` instantiated into something unmappable
}

function getUniqueLiteralFilledInstantiation(type: Type) {
return type.flags & (TypeFlags.Primitive | TypeFlags.AnyOrUnknown | TypeFlags.Never) ? type :
type.uniqueLiteralFilledInstantiation || (type.uniqueLiteralFilledInstantiation = instantiateType(type, uniqueLiteralMapper));
}

function getPermissiveInstantiation(type: Type) {
return type.flags & (TypeFlags.Primitive | TypeFlags.AnyOrUnknown | TypeFlags.Never) ? type :
type.permissiveInstantiation || (type.permissiveInstantiation = instantiateType(type, permissiveMapper));
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5244,6 +5244,8 @@ namespace ts {
/* @internal */
restrictiveInstantiation?: Type; // Instantiation with type parameters mapped to unconstrained form
/* @internal */
uniqueLiteralFilledInstantiation?: Type; // Instantiation with type parameters mapped to never type
/* @internal */
immediateBaseConstraint?: Type; // Immediate base constraint cache
/* @internal */
widened?: Type; // Cached widened form of the type
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
tests/cases/compiler/mappedTypeNotMistakenlyHomomorphic.ts(33,1): error TS2741: Property 'a' is missing in type 'Gen2<ABC.B>' but required in type 'Gen2<ABC.A>'.
tests/cases/compiler/mappedTypeNotMistakenlyHomomorphic.ts(34,1): error TS2741: Property 'b' is missing in type 'Gen2<ABC.A>' but required in type 'Gen2<ABC.B>'.


==== tests/cases/compiler/mappedTypeNotMistakenlyHomomorphic.ts (2 errors) ====
enum ABC { A, B }

type Gen<T extends ABC> = { v: T; } & (
{
v: ABC.A,
a: string,
} | {
v: ABC.B,
b: string,
}
)

// Quick info: ???
//
// type Gen2<T extends ABC> = {
// v: string;
// }
//
type Gen2<T extends ABC> = {
[Property in keyof Gen<T>]: string;
};

// 'a' and 'b' properties required !?!?
const gen2TypeA: Gen2<ABC.A> = { v: "I am A", a: "" };
const gen2TypeB: Gen2<ABC.B> = { v: "I am B", b: "" };

// 'v' ???
type K = keyof Gen2<ABC.A>;

// :(
declare let a: Gen2<ABC.A>;
declare let b: Gen2<ABC.B>;
a = b;
~
!!! error TS2741: Property 'a' is missing in type 'Gen2<ABC.B>' but required in type 'Gen2<ABC.A>'.
!!! related TS2728 tests/cases/compiler/mappedTypeNotMistakenlyHomomorphic.ts:6:5: 'a' is declared here.
b = a;
~
!!! error TS2741: Property 'b' is missing in type 'Gen2<ABC.A>' but required in type 'Gen2<ABC.B>'.
!!! related TS2728 tests/cases/compiler/mappedTypeNotMistakenlyHomomorphic.ts:9:5: 'b' is declared here.

48 changes: 48 additions & 0 deletions tests/baselines/reference/mappedTypeNotMistakenlyHomomorphic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//// [mappedTypeNotMistakenlyHomomorphic.ts]
enum ABC { A, B }

type Gen<T extends ABC> = { v: T; } & (
{
v: ABC.A,
a: string,
} | {
v: ABC.B,
b: string,
}
)

// Quick info: ???
//
// type Gen2<T extends ABC> = {
// v: string;
// }
//
type Gen2<T extends ABC> = {
[Property in keyof Gen<T>]: string;
};

// 'a' and 'b' properties required !?!?
const gen2TypeA: Gen2<ABC.A> = { v: "I am A", a: "" };
const gen2TypeB: Gen2<ABC.B> = { v: "I am B", b: "" };

// 'v' ???
type K = keyof Gen2<ABC.A>;

// :(
declare let a: Gen2<ABC.A>;
declare let b: Gen2<ABC.B>;
a = b;
b = a;


//// [mappedTypeNotMistakenlyHomomorphic.js]
var ABC;
(function (ABC) {
ABC[ABC["A"] = 0] = "A";
ABC[ABC["B"] = 1] = "B";
})(ABC || (ABC = {}));
// 'a' and 'b' properties required !?!?
var gen2TypeA = { v: "I am A", a: "" };
var gen2TypeB = { v: "I am B", b: "" };
a = b;
b = a;
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
=== tests/cases/compiler/mappedTypeNotMistakenlyHomomorphic.ts ===
enum ABC { A, B }
>ABC : Symbol(ABC, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 0))
>A : Symbol(ABC.A, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 10))
>B : Symbol(ABC.B, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 13))

type Gen<T extends ABC> = { v: T; } & (
>Gen : Symbol(Gen, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 17))
>T : Symbol(T, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 2, 9))
>ABC : Symbol(ABC, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 0))
>v : Symbol(v, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 2, 27))
>T : Symbol(T, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 2, 9))
{
v: ABC.A,
>v : Symbol(v, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 3, 3))
>ABC : Symbol(ABC, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 0))
>A : Symbol(ABC.A, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 10))

a: string,
>a : Symbol(a, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 4, 13))

} | {
v: ABC.B,
>v : Symbol(v, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 6, 7))
>ABC : Symbol(ABC, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 0))
>B : Symbol(ABC.B, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 13))

b: string,
>b : Symbol(b, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 7, 13))
}
)

// Quick info: ???
//
// type Gen2<T extends ABC> = {
// v: string;
// }
//
type Gen2<T extends ABC> = {
>Gen2 : Symbol(Gen2, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 10, 1))
>T : Symbol(T, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 18, 10))
>ABC : Symbol(ABC, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 0))

[Property in keyof Gen<T>]: string;
>Property : Symbol(Property, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 19, 3))
>Gen : Symbol(Gen, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 17))
>T : Symbol(T, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 18, 10))

};

// 'a' and 'b' properties required !?!?
const gen2TypeA: Gen2<ABC.A> = { v: "I am A", a: "" };
>gen2TypeA : Symbol(gen2TypeA, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 23, 5))
>Gen2 : Symbol(Gen2, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 10, 1))
>ABC : Symbol(ABC, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 0))
>A : Symbol(ABC.A, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 10))
>v : Symbol(v, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 23, 32))
>a : Symbol(a, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 23, 46))

const gen2TypeB: Gen2<ABC.B> = { v: "I am B", b: "" };
>gen2TypeB : Symbol(gen2TypeB, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 24, 5))
>Gen2 : Symbol(Gen2, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 10, 1))
>ABC : Symbol(ABC, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 0))
>B : Symbol(ABC.B, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 13))
>v : Symbol(v, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 24, 32))
>b : Symbol(b, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 24, 46))

// 'v' ???
type K = keyof Gen2<ABC.A>;
>K : Symbol(K, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 24, 55))
>Gen2 : Symbol(Gen2, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 10, 1))
>ABC : Symbol(ABC, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 0))
>A : Symbol(ABC.A, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 10))

// :(
declare let a: Gen2<ABC.A>;
>a : Symbol(a, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 30, 11))
>Gen2 : Symbol(Gen2, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 10, 1))
>ABC : Symbol(ABC, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 0))
>A : Symbol(ABC.A, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 10))

declare let b: Gen2<ABC.B>;
>b : Symbol(b, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 31, 11))
>Gen2 : Symbol(Gen2, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 10, 1))
>ABC : Symbol(ABC, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 0))
>B : Symbol(ABC.B, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 0, 13))

a = b;
>a : Symbol(a, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 30, 11))
>b : Symbol(b, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 31, 11))

b = a;
>b : Symbol(b, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 31, 11))
>a : Symbol(a, Decl(mappedTypeNotMistakenlyHomomorphic.ts, 30, 11))

82 changes: 82 additions & 0 deletions tests/baselines/reference/mappedTypeNotMistakenlyHomomorphic.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
=== tests/cases/compiler/mappedTypeNotMistakenlyHomomorphic.ts ===
enum ABC { A, B }
>ABC : ABC
>A : ABC.A
>B : ABC.B

type Gen<T extends ABC> = { v: T; } & (
>Gen : Gen<T>
>v : T
{
v: ABC.A,
>v : ABC.A
>ABC : any

a: string,
>a : string

} | {
v: ABC.B,
>v : ABC.B
>ABC : any

b: string,
>b : string
}
)

// Quick info: ???
//
// type Gen2<T extends ABC> = {
// v: string;
// }
//
type Gen2<T extends ABC> = {
>Gen2 : Gen2<T>

[Property in keyof Gen<T>]: string;
};

// 'a' and 'b' properties required !?!?
const gen2TypeA: Gen2<ABC.A> = { v: "I am A", a: "" };
>gen2TypeA : Gen2<ABC.A>
>ABC : any
>{ v: "I am A", a: "" } : { v: string; a: string; }
>v : string
>"I am A" : "I am A"
>a : string
>"" : ""

const gen2TypeB: Gen2<ABC.B> = { v: "I am B", b: "" };
>gen2TypeB : Gen2<ABC.B>
>ABC : any
>{ v: "I am B", b: "" } : { v: string; b: string; }
>v : string
>"I am B" : "I am B"
>b : string
>"" : ""

// 'v' ???
type K = keyof Gen2<ABC.A>;
>K : "v" | "a"
>ABC : any

// :(
declare let a: Gen2<ABC.A>;
>a : Gen2<ABC.A>
>ABC : any

declare let b: Gen2<ABC.B>;
>b : Gen2<ABC.B>
>ABC : any

a = b;
>a = b : Gen2<ABC.B>
>a : Gen2<ABC.A>
>b : Gen2<ABC.B>

b = a;
>b = a : Gen2<ABC.A>
>b : Gen2<ABC.B>
>a : Gen2<ABC.A>

34 changes: 34 additions & 0 deletions tests/cases/compiler/mappedTypeNotMistakenlyHomomorphic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
enum ABC { A, B }

type Gen<T extends ABC> = { v: T; } & (
{
v: ABC.A,
a: string,
} | {
v: ABC.B,
b: string,
}
)

// Quick info: ???
//
// type Gen2<T extends ABC> = {
// v: string;
// }
//
type Gen2<T extends ABC> = {
[Property in keyof Gen<T>]: string;
};

// 'a' and 'b' properties required !?!?
const gen2TypeA: Gen2<ABC.A> = { v: "I am A", a: "" };
const gen2TypeB: Gen2<ABC.B> = { v: "I am B", b: "" };

// 'v' ???
type K = keyof Gen2<ABC.A>;

// :(
declare let a: Gen2<ABC.A>;
declare let b: Gen2<ABC.B>;
a = b;
b = a;