diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index bf10d24e261..9a28179acbb 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -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); @@ -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); @@ -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) : @@ -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)); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 5ce2842fa89..53bb25c4597 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -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 diff --git a/tests/baselines/reference/mappedTypeNotMistakenlyHomomorphic.errors.txt b/tests/baselines/reference/mappedTypeNotMistakenlyHomomorphic.errors.txt new file mode 100644 index 00000000000..4dce86a71a4 --- /dev/null +++ b/tests/baselines/reference/mappedTypeNotMistakenlyHomomorphic.errors.txt @@ -0,0 +1,46 @@ +tests/cases/compiler/mappedTypeNotMistakenlyHomomorphic.ts(33,1): error TS2741: Property 'a' is missing in type 'Gen2' but required in type 'Gen2'. +tests/cases/compiler/mappedTypeNotMistakenlyHomomorphic.ts(34,1): error TS2741: Property 'b' is missing in type 'Gen2' but required in type 'Gen2'. + + +==== tests/cases/compiler/mappedTypeNotMistakenlyHomomorphic.ts (2 errors) ==== + enum ABC { A, B } + + type Gen = { v: T; } & ( + { + v: ABC.A, + a: string, + } | { + v: ABC.B, + b: string, + } + ) + + // Quick info: ??? + // + // type Gen2 = { + // v: string; + // } + // + type Gen2 = { + [Property in keyof Gen]: string; + }; + + // 'a' and 'b' properties required !?!? + const gen2TypeA: Gen2 = { v: "I am A", a: "" }; + const gen2TypeB: Gen2 = { v: "I am B", b: "" }; + + // 'v' ??? + type K = keyof Gen2; + + // :( + declare let a: Gen2; + declare let b: Gen2; + a = b; + ~ +!!! error TS2741: Property 'a' is missing in type 'Gen2' but required in type 'Gen2'. +!!! related TS2728 tests/cases/compiler/mappedTypeNotMistakenlyHomomorphic.ts:6:5: 'a' is declared here. + b = a; + ~ +!!! error TS2741: Property 'b' is missing in type 'Gen2' but required in type 'Gen2'. +!!! related TS2728 tests/cases/compiler/mappedTypeNotMistakenlyHomomorphic.ts:9:5: 'b' is declared here. + \ No newline at end of file diff --git a/tests/baselines/reference/mappedTypeNotMistakenlyHomomorphic.js b/tests/baselines/reference/mappedTypeNotMistakenlyHomomorphic.js new file mode 100644 index 00000000000..730eefc1839 --- /dev/null +++ b/tests/baselines/reference/mappedTypeNotMistakenlyHomomorphic.js @@ -0,0 +1,48 @@ +//// [mappedTypeNotMistakenlyHomomorphic.ts] +enum ABC { A, B } + +type Gen = { v: T; } & ( + { + v: ABC.A, + a: string, + } | { + v: ABC.B, + b: string, + } +) + +// Quick info: ??? +// +// type Gen2 = { +// v: string; +// } +// +type Gen2 = { + [Property in keyof Gen]: string; +}; + +// 'a' and 'b' properties required !?!? +const gen2TypeA: Gen2 = { v: "I am A", a: "" }; +const gen2TypeB: Gen2 = { v: "I am B", b: "" }; + +// 'v' ??? +type K = keyof Gen2; + +// :( +declare let a: Gen2; +declare let b: Gen2; +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; diff --git a/tests/baselines/reference/mappedTypeNotMistakenlyHomomorphic.symbols b/tests/baselines/reference/mappedTypeNotMistakenlyHomomorphic.symbols new file mode 100644 index 00000000000..7cfa949dbb4 --- /dev/null +++ b/tests/baselines/reference/mappedTypeNotMistakenlyHomomorphic.symbols @@ -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 = { 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 = { +// v: string; +// } +// +type Gen2 = { +>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]: 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 = { 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 = { 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; +>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; +>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; +>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)) + diff --git a/tests/baselines/reference/mappedTypeNotMistakenlyHomomorphic.types b/tests/baselines/reference/mappedTypeNotMistakenlyHomomorphic.types new file mode 100644 index 00000000000..588a11df228 --- /dev/null +++ b/tests/baselines/reference/mappedTypeNotMistakenlyHomomorphic.types @@ -0,0 +1,82 @@ +=== tests/cases/compiler/mappedTypeNotMistakenlyHomomorphic.ts === +enum ABC { A, B } +>ABC : ABC +>A : ABC.A +>B : ABC.B + +type Gen = { v: T; } & ( +>Gen : Gen +>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 = { +// v: string; +// } +// +type Gen2 = { +>Gen2 : Gen2 + + [Property in keyof Gen]: string; +}; + +// 'a' and 'b' properties required !?!? +const gen2TypeA: Gen2 = { v: "I am A", a: "" }; +>gen2TypeA : Gen2 +>ABC : any +>{ v: "I am A", a: "" } : { v: string; a: string; } +>v : string +>"I am A" : "I am A" +>a : string +>"" : "" + +const gen2TypeB: Gen2 = { v: "I am B", b: "" }; +>gen2TypeB : Gen2 +>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; +>K : "v" | "a" +>ABC : any + +// :( +declare let a: Gen2; +>a : Gen2 +>ABC : any + +declare let b: Gen2; +>b : Gen2 +>ABC : any + +a = b; +>a = b : Gen2 +>a : Gen2 +>b : Gen2 + +b = a; +>b = a : Gen2 +>b : Gen2 +>a : Gen2 + diff --git a/tests/cases/compiler/mappedTypeNotMistakenlyHomomorphic.ts b/tests/cases/compiler/mappedTypeNotMistakenlyHomomorphic.ts new file mode 100644 index 00000000000..23ae21df7ac --- /dev/null +++ b/tests/cases/compiler/mappedTypeNotMistakenlyHomomorphic.ts @@ -0,0 +1,34 @@ +enum ABC { A, B } + +type Gen = { v: T; } & ( + { + v: ABC.A, + a: string, + } | { + v: ABC.B, + b: string, + } +) + +// Quick info: ??? +// +// type Gen2 = { +// v: string; +// } +// +type Gen2 = { + [Property in keyof Gen]: string; +}; + +// 'a' and 'b' properties required !?!? +const gen2TypeA: Gen2 = { v: "I am A", a: "" }; +const gen2TypeB: Gen2 = { v: "I am B", b: "" }; + +// 'v' ??? +type K = keyof Gen2; + +// :( +declare let a: Gen2; +declare let b: Gen2; +a = b; +b = a;