Skip to content

Commit f701daf

Browse files
authored
Infer over each mapped type constraint member if it is a union (#28006)
1 parent 68ce68d commit f701daf

File tree

6 files changed

+182
-36
lines changed

6 files changed

+182
-36
lines changed

src/compiler/checker.ts

Lines changed: 52 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6786,7 +6786,7 @@ namespace ts {
67866786
const modifiers = getMappedTypeModifiers(type.mappedType);
67876787
const readonlyMask = modifiers & MappedTypeModifiers.IncludeReadonly ? false : true;
67886788
const optionalMask = modifiers & MappedTypeModifiers.IncludeOptional ? 0 : SymbolFlags.Optional;
6789-
const stringIndexInfo = indexInfo && createIndexInfo(inferReverseMappedType(indexInfo.type, type.mappedType), readonlyMask && indexInfo.isReadonly);
6789+
const stringIndexInfo = indexInfo && createIndexInfo(inferReverseMappedType(indexInfo.type, type.mappedType, type.constraintType), readonlyMask && indexInfo.isReadonly);
67906790
const members = createSymbolTable();
67916791
for (const prop of getPropertiesOfType(type.source)) {
67926792
const checkFlags = CheckFlags.ReverseMapped | (readonlyMask && isReadonlySymbol(prop) ? CheckFlags.Readonly : 0);
@@ -6795,6 +6795,7 @@ namespace ts {
67956795
inferredProp.nameType = prop.nameType;
67966796
inferredProp.propertyType = getTypeOfSymbol(prop);
67976797
inferredProp.mappedType = type.mappedType;
6798+
inferredProp.constraintType = type.constraintType;
67986799
members.set(prop.escapedName, inferredProp);
67996800
}
68006801
setStructuredTypeMembers(type, members, emptyArray, emptyArray, stringIndexInfo, undefined);
@@ -13493,18 +13494,18 @@ namespace ts {
1349313494
* property is computed by inferring from the source property type to X for the type
1349413495
* variable T[P] (i.e. we treat the type T[P] as the type variable we're inferring for).
1349513496
*/
13496-
function inferTypeForHomomorphicMappedType(source: Type, target: MappedType): Type | undefined {
13497-
const key = source.id + "," + target.id;
13497+
function inferTypeForHomomorphicMappedType(source: Type, target: MappedType, constraint: IndexType): Type | undefined {
13498+
const key = source.id + "," + target.id + "," + constraint.id;
1349813499
if (reverseMappedCache.has(key)) {
1349913500
return reverseMappedCache.get(key);
1350013501
}
1350113502
reverseMappedCache.set(key, undefined);
13502-
const type = createReverseMappedType(source, target);
13503+
const type = createReverseMappedType(source, target, constraint);
1350313504
reverseMappedCache.set(key, type);
1350413505
return type;
1350513506
}
1350613507

13507-
function createReverseMappedType(source: Type, target: MappedType) {
13508+
function createReverseMappedType(source: Type, target: MappedType, constraint: IndexType) {
1350813509
const properties = getPropertiesOfType(source);
1350913510
if (properties.length === 0 && !getIndexInfoOfType(source, IndexKind.String)) {
1351013511
return undefined;
@@ -13519,13 +13520,13 @@ namespace ts {
1351913520
// For arrays and tuples we infer new arrays and tuples where the reverse mapping has been
1352013521
// applied to the element type(s).
1352113522
if (isArrayType(source)) {
13522-
return createArrayType(inferReverseMappedType((<TypeReference>source).typeArguments![0], target));
13523+
return createArrayType(inferReverseMappedType((<TypeReference>source).typeArguments![0], target, constraint));
1352313524
}
1352413525
if (isReadonlyArrayType(source)) {
13525-
return createReadonlyArrayType(inferReverseMappedType((<TypeReference>source).typeArguments![0], target));
13526+
return createReadonlyArrayType(inferReverseMappedType((<TypeReference>source).typeArguments![0], target, constraint));
1352613527
}
1352713528
if (isTupleType(source)) {
13528-
const elementTypes = map(source.typeArguments || emptyArray, t => inferReverseMappedType(t, target));
13529+
const elementTypes = map(source.typeArguments || emptyArray, t => inferReverseMappedType(t, target, constraint));
1352913530
const minLength = getMappedTypeModifiers(target) & MappedTypeModifiers.IncludeOptional ?
1353013531
getTypeReferenceArity(source) - (source.target.hasRestElement ? 1 : 0) : source.target.minLength;
1353113532
return createTupleType(elementTypes, minLength, source.target.hasRestElement, source.target.associatedNames);
@@ -13535,15 +13536,16 @@ namespace ts {
1353513536
const reversed = createObjectType(ObjectFlags.ReverseMapped | ObjectFlags.Anonymous, /*symbol*/ undefined) as ReverseMappedType;
1353613537
reversed.source = source;
1353713538
reversed.mappedType = target;
13539+
reversed.constraintType = constraint;
1353813540
return reversed;
1353913541
}
1354013542

1354113543
function getTypeOfReverseMappedSymbol(symbol: ReverseMappedSymbol) {
13542-
return inferReverseMappedType(symbol.propertyType, symbol.mappedType);
13544+
return inferReverseMappedType(symbol.propertyType, symbol.mappedType, symbol.constraintType);
1354313545
}
1354413546

13545-
function inferReverseMappedType(sourceType: Type, target: MappedType): Type {
13546-
const typeParameter = <TypeParameter>getIndexedAccessType((<IndexType>getConstraintTypeFromMappedType(target)).type, getTypeParameterFromMappedType(target));
13547+
function inferReverseMappedType(sourceType: Type, target: MappedType, constraint: IndexType): Type {
13548+
const typeParameter = <TypeParameter>getIndexedAccessType(constraint.type, getTypeParameterFromMappedType(target));
1354713549
const templateType = getTemplateTypeFromMappedType(target);
1354813550
const inference = createInferenceInfo(typeParameter);
1354913551
inferTypes([inference], sourceType, templateType);
@@ -13841,6 +13843,44 @@ namespace ts {
1384113843
return undefined;
1384213844
}
1384313845

13846+
function inferFromMappedTypeConstraint(source: Type, target: Type, constraintType: Type): boolean {
13847+
if (constraintType.flags & TypeFlags.Union) {
13848+
let result = false;
13849+
for (const type of (constraintType as UnionType).types) {
13850+
result = inferFromMappedTypeConstraint(source, target, type) || result;
13851+
}
13852+
return result;
13853+
}
13854+
if (constraintType.flags & TypeFlags.Index) {
13855+
// We're inferring from some source type S to a homomorphic mapped type { [P in keyof T]: X },
13856+
// where T is a type variable. Use inferTypeForHomomorphicMappedType to infer a suitable source
13857+
// type and then make a secondary inference from that type to T. We make a secondary inference
13858+
// such that direct inferences to T get priority over inferences to Partial<T>, for example.
13859+
const inference = getInferenceInfoForType((<IndexType>constraintType).type);
13860+
if (inference && !inference.isFixed) {
13861+
const inferredType = inferTypeForHomomorphicMappedType(source, <MappedType>target, constraintType as IndexType);
13862+
if (inferredType) {
13863+
const savePriority = priority;
13864+
priority |= InferencePriority.HomomorphicMappedType;
13865+
inferFromTypes(inferredType, inference.typeParameter);
13866+
priority = savePriority;
13867+
}
13868+
}
13869+
return true;
13870+
}
13871+
if (constraintType.flags & TypeFlags.TypeParameter) {
13872+
// We're inferring from some source type S to a mapped type { [P in T]: X }, where T is a type
13873+
// parameter. Infer from 'keyof S' to T and infer from a union of each property type in S to X.
13874+
const savePriority = priority;
13875+
priority |= InferencePriority.MappedTypeConstraint;
13876+
inferFromTypes(getIndexType(source), constraintType);
13877+
priority = savePriority;
13878+
inferFromTypes(getUnionType(map(getPropertiesOfType(source), getTypeOfSymbol)), getTemplateTypeFromMappedType(<MappedType>target));
13879+
return true;
13880+
}
13881+
return false;
13882+
}
13883+
1384413884
function inferFromObjectTypes(source: Type, target: Type) {
1384513885
if (isGenericMappedType(source) && isGenericMappedType(target)) {
1384613886
// The source and target types are generic types { [P in S]: X } and { [P in T]: Y }, so we infer
@@ -13850,31 +13890,7 @@ namespace ts {
1385013890
}
1385113891
if (getObjectFlags(target) & ObjectFlags.Mapped) {
1385213892
const constraintType = getConstraintTypeFromMappedType(<MappedType>target);
13853-
if (constraintType.flags & TypeFlags.Index) {
13854-
// We're inferring from some source type S to a homomorphic mapped type { [P in keyof T]: X },
13855-
// where T is a type variable. Use inferTypeForHomomorphicMappedType to infer a suitable source
13856-
// type and then make a secondary inference from that type to T. We make a secondary inference
13857-
// such that direct inferences to T get priority over inferences to Partial<T>, for example.
13858-
const inference = getInferenceInfoForType((<IndexType>constraintType).type);
13859-
if (inference && !inference.isFixed) {
13860-
const inferredType = inferTypeForHomomorphicMappedType(source, <MappedType>target);
13861-
if (inferredType) {
13862-
const savePriority = priority;
13863-
priority |= InferencePriority.HomomorphicMappedType;
13864-
inferFromTypes(inferredType, inference.typeParameter);
13865-
priority = savePriority;
13866-
}
13867-
}
13868-
return;
13869-
}
13870-
if (constraintType.flags & TypeFlags.TypeParameter) {
13871-
// We're inferring from some source type S to a mapped type { [P in T]: X }, where T is a type
13872-
// parameter. Infer from 'keyof S' to T and infer from a union of each property type in S to X.
13873-
const savePriority = priority;
13874-
priority |= InferencePriority.MappedTypeConstraint;
13875-
inferFromTypes(getIndexType(source), constraintType);
13876-
priority = savePriority;
13877-
inferFromTypes(getUnionType(map(getPropertiesOfType(source), getTypeOfSymbol)), getTemplateTypeFromMappedType(<MappedType>target));
13893+
if (inferFromMappedTypeConstraint(source, target, constraintType)) {
1387813894
return;
1387913895
}
1388013896
}

src/compiler/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3682,6 +3682,7 @@ namespace ts {
36823682
export interface ReverseMappedSymbol extends TransientSymbol {
36833683
propertyType: Type;
36843684
mappedType: MappedType;
3685+
constraintType: IndexType;
36853686
}
36863687

36873688
export const enum InternalSymbolName {
@@ -4090,6 +4091,7 @@ namespace ts {
40904091
export interface ReverseMappedType extends ObjectType {
40914092
source: Type;
40924093
mappedType: MappedType;
4094+
constraintType: IndexType;
40934095
}
40944096

40954097
/* @internal */
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//// [checkJsxIntersectionElementPropsType.tsx]
2+
declare namespace JSX {
3+
interface ElementAttributesProperty { props: {}; }
4+
}
5+
6+
declare class Component<P> {
7+
constructor(props: Readonly<P>);
8+
readonly props: Readonly<P>;
9+
}
10+
11+
class C<T> extends Component<{ x?: boolean; } & T> {}
12+
const y = new C({foobar: "example"});
13+
const x = <C foobar="example" />
14+
15+
//// [checkJsxIntersectionElementPropsType.jsx]
16+
"use strict";
17+
var __extends = (this && this.__extends) || (function () {
18+
var extendStatics = function (d, b) {
19+
extendStatics = Object.setPrototypeOf ||
20+
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
21+
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
22+
return extendStatics(d, b);
23+
};
24+
return function (d, b) {
25+
extendStatics(d, b);
26+
function __() { this.constructor = d; }
27+
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
28+
};
29+
})();
30+
var C = /** @class */ (function (_super) {
31+
__extends(C, _super);
32+
function C() {
33+
return _super !== null && _super.apply(this, arguments) || this;
34+
}
35+
return C;
36+
}(Component));
37+
var y = new C({ foobar: "example" });
38+
var x = <C foobar="example"/>;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
=== tests/cases/conformance/jsx/checkJsxIntersectionElementPropsType.tsx ===
2+
declare namespace JSX {
3+
>JSX : Symbol(JSX, Decl(checkJsxIntersectionElementPropsType.tsx, 0, 0))
4+
5+
interface ElementAttributesProperty { props: {}; }
6+
>ElementAttributesProperty : Symbol(ElementAttributesProperty, Decl(checkJsxIntersectionElementPropsType.tsx, 0, 23))
7+
>props : Symbol(ElementAttributesProperty.props, Decl(checkJsxIntersectionElementPropsType.tsx, 1, 41))
8+
}
9+
10+
declare class Component<P> {
11+
>Component : Symbol(Component, Decl(checkJsxIntersectionElementPropsType.tsx, 2, 1))
12+
>P : Symbol(P, Decl(checkJsxIntersectionElementPropsType.tsx, 4, 24))
13+
14+
constructor(props: Readonly<P>);
15+
>props : Symbol(props, Decl(checkJsxIntersectionElementPropsType.tsx, 5, 14))
16+
>Readonly : Symbol(Readonly, Decl(lib.es5.d.ts, --, --))
17+
>P : Symbol(P, Decl(checkJsxIntersectionElementPropsType.tsx, 4, 24))
18+
19+
readonly props: Readonly<P>;
20+
>props : Symbol(Component.props, Decl(checkJsxIntersectionElementPropsType.tsx, 5, 34))
21+
>Readonly : Symbol(Readonly, Decl(lib.es5.d.ts, --, --))
22+
>P : Symbol(P, Decl(checkJsxIntersectionElementPropsType.tsx, 4, 24))
23+
}
24+
25+
class C<T> extends Component<{ x?: boolean; } & T> {}
26+
>C : Symbol(C, Decl(checkJsxIntersectionElementPropsType.tsx, 7, 1))
27+
>T : Symbol(T, Decl(checkJsxIntersectionElementPropsType.tsx, 9, 8))
28+
>Component : Symbol(Component, Decl(checkJsxIntersectionElementPropsType.tsx, 2, 1))
29+
>x : Symbol(x, Decl(checkJsxIntersectionElementPropsType.tsx, 9, 30))
30+
>T : Symbol(T, Decl(checkJsxIntersectionElementPropsType.tsx, 9, 8))
31+
32+
const y = new C({foobar: "example"});
33+
>y : Symbol(y, Decl(checkJsxIntersectionElementPropsType.tsx, 10, 5))
34+
>C : Symbol(C, Decl(checkJsxIntersectionElementPropsType.tsx, 7, 1))
35+
>foobar : Symbol(foobar, Decl(checkJsxIntersectionElementPropsType.tsx, 10, 17))
36+
37+
const x = <C foobar="example" />
38+
>x : Symbol(x, Decl(checkJsxIntersectionElementPropsType.tsx, 11, 5))
39+
>C : Symbol(C, Decl(checkJsxIntersectionElementPropsType.tsx, 7, 1))
40+
>foobar : Symbol(foobar, Decl(checkJsxIntersectionElementPropsType.tsx, 11, 12))
41+
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
=== tests/cases/conformance/jsx/checkJsxIntersectionElementPropsType.tsx ===
2+
declare namespace JSX {
3+
interface ElementAttributesProperty { props: {}; }
4+
>props : {}
5+
}
6+
7+
declare class Component<P> {
8+
>Component : Component<P>
9+
10+
constructor(props: Readonly<P>);
11+
>props : Readonly<P>
12+
13+
readonly props: Readonly<P>;
14+
>props : Readonly<P>
15+
}
16+
17+
class C<T> extends Component<{ x?: boolean; } & T> {}
18+
>C : C<T>
19+
>Component : Component<{ x?: boolean | undefined; } & T>
20+
>x : boolean | undefined
21+
22+
const y = new C({foobar: "example"});
23+
>y : C<{ foobar: {}; }>
24+
>new C({foobar: "example"}) : C<{ foobar: {}; }>
25+
>C : typeof C
26+
>{foobar: "example"} : { foobar: string; }
27+
>foobar : string
28+
>"example" : "example"
29+
30+
const x = <C foobar="example" />
31+
>x : error
32+
><C foobar="example" /> : error
33+
>C : typeof C
34+
>foobar : string
35+
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// @jsx: preserve
2+
// @strict: true
3+
declare namespace JSX {
4+
interface ElementAttributesProperty { props: {}; }
5+
}
6+
7+
declare class Component<P> {
8+
constructor(props: Readonly<P>);
9+
readonly props: Readonly<P>;
10+
}
11+
12+
class C<T> extends Component<{ x?: boolean; } & T> {}
13+
const y = new C({foobar: "example"});
14+
const x = <C foobar="example" />

0 commit comments

Comments
 (0)