Skip to content

Infer over each mapped type constraint member if it is a union #28006

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
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
88 changes: 52 additions & 36 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6786,7 +6786,7 @@ namespace ts {
const modifiers = getMappedTypeModifiers(type.mappedType);
const readonlyMask = modifiers & MappedTypeModifiers.IncludeReadonly ? false : true;
const optionalMask = modifiers & MappedTypeModifiers.IncludeOptional ? 0 : SymbolFlags.Optional;
const stringIndexInfo = indexInfo && createIndexInfo(inferReverseMappedType(indexInfo.type, type.mappedType), readonlyMask && indexInfo.isReadonly);
const stringIndexInfo = indexInfo && createIndexInfo(inferReverseMappedType(indexInfo.type, type.mappedType, type.constraintType), readonlyMask && indexInfo.isReadonly);
const members = createSymbolTable();
for (const prop of getPropertiesOfType(type.source)) {
const checkFlags = CheckFlags.ReverseMapped | (readonlyMask && isReadonlySymbol(prop) ? CheckFlags.Readonly : 0);
Expand All @@ -6795,6 +6795,7 @@ namespace ts {
inferredProp.nameType = prop.nameType;
inferredProp.propertyType = getTypeOfSymbol(prop);
inferredProp.mappedType = type.mappedType;
inferredProp.constraintType = type.constraintType;
members.set(prop.escapedName, inferredProp);
}
setStructuredTypeMembers(type, members, emptyArray, emptyArray, stringIndexInfo, undefined);
Expand Down Expand Up @@ -13493,18 +13494,18 @@ namespace ts {
* property is computed by inferring from the source property type to X for the type
* variable T[P] (i.e. we treat the type T[P] as the type variable we're inferring for).
*/
function inferTypeForHomomorphicMappedType(source: Type, target: MappedType): Type | undefined {
const key = source.id + "," + target.id;
function inferTypeForHomomorphicMappedType(source: Type, target: MappedType, constraint: IndexType): Type | undefined {
const key = source.id + "," + target.id + "," + constraint.id;
if (reverseMappedCache.has(key)) {
return reverseMappedCache.get(key);
}
reverseMappedCache.set(key, undefined);
const type = createReverseMappedType(source, target);
const type = createReverseMappedType(source, target, constraint);
reverseMappedCache.set(key, type);
return type;
}

function createReverseMappedType(source: Type, target: MappedType) {
function createReverseMappedType(source: Type, target: MappedType, constraint: IndexType) {
const properties = getPropertiesOfType(source);
if (properties.length === 0 && !getIndexInfoOfType(source, IndexKind.String)) {
return undefined;
Expand All @@ -13519,13 +13520,13 @@ namespace ts {
// For arrays and tuples we infer new arrays and tuples where the reverse mapping has been
// applied to the element type(s).
if (isArrayType(source)) {
return createArrayType(inferReverseMappedType((<TypeReference>source).typeArguments![0], target));
return createArrayType(inferReverseMappedType((<TypeReference>source).typeArguments![0], target, constraint));
}
if (isReadonlyArrayType(source)) {
return createReadonlyArrayType(inferReverseMappedType((<TypeReference>source).typeArguments![0], target));
return createReadonlyArrayType(inferReverseMappedType((<TypeReference>source).typeArguments![0], target, constraint));
}
if (isTupleType(source)) {
const elementTypes = map(source.typeArguments || emptyArray, t => inferReverseMappedType(t, target));
const elementTypes = map(source.typeArguments || emptyArray, t => inferReverseMappedType(t, target, constraint));
const minLength = getMappedTypeModifiers(target) & MappedTypeModifiers.IncludeOptional ?
getTypeReferenceArity(source) - (source.target.hasRestElement ? 1 : 0) : source.target.minLength;
return createTupleType(elementTypes, minLength, source.target.hasRestElement, source.target.associatedNames);
Expand All @@ -13535,15 +13536,16 @@ namespace ts {
const reversed = createObjectType(ObjectFlags.ReverseMapped | ObjectFlags.Anonymous, /*symbol*/ undefined) as ReverseMappedType;
reversed.source = source;
reversed.mappedType = target;
reversed.constraintType = constraint;
return reversed;
}

function getTypeOfReverseMappedSymbol(symbol: ReverseMappedSymbol) {
return inferReverseMappedType(symbol.propertyType, symbol.mappedType);
return inferReverseMappedType(symbol.propertyType, symbol.mappedType, symbol.constraintType);
}

function inferReverseMappedType(sourceType: Type, target: MappedType): Type {
const typeParameter = <TypeParameter>getIndexedAccessType((<IndexType>getConstraintTypeFromMappedType(target)).type, getTypeParameterFromMappedType(target));
function inferReverseMappedType(sourceType: Type, target: MappedType, constraint: IndexType): Type {
const typeParameter = <TypeParameter>getIndexedAccessType(constraint.type, getTypeParameterFromMappedType(target));
const templateType = getTemplateTypeFromMappedType(target);
const inference = createInferenceInfo(typeParameter);
inferTypes([inference], sourceType, templateType);
Expand Down Expand Up @@ -13841,6 +13843,44 @@ namespace ts {
return undefined;
}

function inferFromMappedTypeConstraint(source: Type, target: Type, constraintType: Type): boolean {
if (constraintType.flags & TypeFlags.Union) {
let result = false;
for (const type of (constraintType as UnionType).types) {
result = inferFromMappedTypeConstraint(source, target, type) || result;
}
return result;
}
if (constraintType.flags & TypeFlags.Index) {
// We're inferring from some source type S to a homomorphic mapped type { [P in keyof T]: X },
// where T is a type variable. Use inferTypeForHomomorphicMappedType to infer a suitable source
// type and then make a secondary inference from that type to T. We make a secondary inference
// such that direct inferences to T get priority over inferences to Partial<T>, for example.
const inference = getInferenceInfoForType((<IndexType>constraintType).type);
if (inference && !inference.isFixed) {
const inferredType = inferTypeForHomomorphicMappedType(source, <MappedType>target, constraintType as IndexType);
if (inferredType) {
const savePriority = priority;
priority |= InferencePriority.HomomorphicMappedType;
inferFromTypes(inferredType, inference.typeParameter);
priority = savePriority;
}
}
return true;
}
if (constraintType.flags & TypeFlags.TypeParameter) {
// We're inferring from some source type S to a mapped type { [P in T]: X }, where T is a type
// parameter. Infer from 'keyof S' to T and infer from a union of each property type in S to X.
const savePriority = priority;
priority |= InferencePriority.MappedTypeConstraint;
inferFromTypes(getIndexType(source), constraintType);
priority = savePriority;
inferFromTypes(getUnionType(map(getPropertiesOfType(source), getTypeOfSymbol)), getTemplateTypeFromMappedType(<MappedType>target));
return true;
}
return false;
}

function inferFromObjectTypes(source: Type, target: Type) {
if (isGenericMappedType(source) && isGenericMappedType(target)) {
// The source and target types are generic types { [P in S]: X } and { [P in T]: Y }, so we infer
Expand All @@ -13850,31 +13890,7 @@ namespace ts {
}
if (getObjectFlags(target) & ObjectFlags.Mapped) {
const constraintType = getConstraintTypeFromMappedType(<MappedType>target);
if (constraintType.flags & TypeFlags.Index) {
// We're inferring from some source type S to a homomorphic mapped type { [P in keyof T]: X },
// where T is a type variable. Use inferTypeForHomomorphicMappedType to infer a suitable source
// type and then make a secondary inference from that type to T. We make a secondary inference
// such that direct inferences to T get priority over inferences to Partial<T>, for example.
const inference = getInferenceInfoForType((<IndexType>constraintType).type);
if (inference && !inference.isFixed) {
const inferredType = inferTypeForHomomorphicMappedType(source, <MappedType>target);
if (inferredType) {
const savePriority = priority;
priority |= InferencePriority.HomomorphicMappedType;
inferFromTypes(inferredType, inference.typeParameter);
priority = savePriority;
}
}
return;
}
if (constraintType.flags & TypeFlags.TypeParameter) {
// We're inferring from some source type S to a mapped type { [P in T]: X }, where T is a type
// parameter. Infer from 'keyof S' to T and infer from a union of each property type in S to X.
const savePriority = priority;
priority |= InferencePriority.MappedTypeConstraint;
inferFromTypes(getIndexType(source), constraintType);
priority = savePriority;
inferFromTypes(getUnionType(map(getPropertiesOfType(source), getTypeOfSymbol)), getTemplateTypeFromMappedType(<MappedType>target));
if (inferFromMappedTypeConstraint(source, target, constraintType)) {
return;
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3682,6 +3682,7 @@ namespace ts {
export interface ReverseMappedSymbol extends TransientSymbol {
propertyType: Type;
mappedType: MappedType;
constraintType: IndexType;
}

export const enum InternalSymbolName {
Expand Down Expand Up @@ -4090,6 +4091,7 @@ namespace ts {
export interface ReverseMappedType extends ObjectType {
source: Type;
mappedType: MappedType;
constraintType: IndexType;
}

/* @internal */
Expand Down
38 changes: 38 additions & 0 deletions tests/baselines/reference/checkJsxIntersectionElementPropsType.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//// [checkJsxIntersectionElementPropsType.tsx]
declare namespace JSX {
interface ElementAttributesProperty { props: {}; }
}

declare class Component<P> {
constructor(props: Readonly<P>);
readonly props: Readonly<P>;
}

class C<T> extends Component<{ x?: boolean; } & T> {}
const y = new C({foobar: "example"});
const x = <C foobar="example" />

//// [checkJsxIntersectionElementPropsType.jsx]
"use strict";
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var C = /** @class */ (function (_super) {
__extends(C, _super);
function C() {
return _super !== null && _super.apply(this, arguments) || this;
}
return C;
}(Component));
var y = new C({ foobar: "example" });
var x = <C foobar="example"/>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
=== tests/cases/conformance/jsx/checkJsxIntersectionElementPropsType.tsx ===
declare namespace JSX {
>JSX : Symbol(JSX, Decl(checkJsxIntersectionElementPropsType.tsx, 0, 0))

interface ElementAttributesProperty { props: {}; }
>ElementAttributesProperty : Symbol(ElementAttributesProperty, Decl(checkJsxIntersectionElementPropsType.tsx, 0, 23))
>props : Symbol(ElementAttributesProperty.props, Decl(checkJsxIntersectionElementPropsType.tsx, 1, 41))
}

declare class Component<P> {
>Component : Symbol(Component, Decl(checkJsxIntersectionElementPropsType.tsx, 2, 1))
>P : Symbol(P, Decl(checkJsxIntersectionElementPropsType.tsx, 4, 24))

constructor(props: Readonly<P>);
>props : Symbol(props, Decl(checkJsxIntersectionElementPropsType.tsx, 5, 14))
>Readonly : Symbol(Readonly, Decl(lib.es5.d.ts, --, --))
>P : Symbol(P, Decl(checkJsxIntersectionElementPropsType.tsx, 4, 24))

readonly props: Readonly<P>;
>props : Symbol(Component.props, Decl(checkJsxIntersectionElementPropsType.tsx, 5, 34))
>Readonly : Symbol(Readonly, Decl(lib.es5.d.ts, --, --))
>P : Symbol(P, Decl(checkJsxIntersectionElementPropsType.tsx, 4, 24))
}

class C<T> extends Component<{ x?: boolean; } & T> {}
>C : Symbol(C, Decl(checkJsxIntersectionElementPropsType.tsx, 7, 1))
>T : Symbol(T, Decl(checkJsxIntersectionElementPropsType.tsx, 9, 8))
>Component : Symbol(Component, Decl(checkJsxIntersectionElementPropsType.tsx, 2, 1))
>x : Symbol(x, Decl(checkJsxIntersectionElementPropsType.tsx, 9, 30))
>T : Symbol(T, Decl(checkJsxIntersectionElementPropsType.tsx, 9, 8))

const y = new C({foobar: "example"});
>y : Symbol(y, Decl(checkJsxIntersectionElementPropsType.tsx, 10, 5))
>C : Symbol(C, Decl(checkJsxIntersectionElementPropsType.tsx, 7, 1))
>foobar : Symbol(foobar, Decl(checkJsxIntersectionElementPropsType.tsx, 10, 17))

const x = <C foobar="example" />
>x : Symbol(x, Decl(checkJsxIntersectionElementPropsType.tsx, 11, 5))
>C : Symbol(C, Decl(checkJsxIntersectionElementPropsType.tsx, 7, 1))
>foobar : Symbol(foobar, Decl(checkJsxIntersectionElementPropsType.tsx, 11, 12))

Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
=== tests/cases/conformance/jsx/checkJsxIntersectionElementPropsType.tsx ===
declare namespace JSX {
interface ElementAttributesProperty { props: {}; }
>props : {}
}

declare class Component<P> {
>Component : Component<P>

constructor(props: Readonly<P>);
>props : Readonly<P>

readonly props: Readonly<P>;
>props : Readonly<P>
}

class C<T> extends Component<{ x?: boolean; } & T> {}
>C : C<T>
>Component : Component<{ x?: boolean | undefined; } & T>
>x : boolean | undefined

const y = new C({foobar: "example"});
>y : C<{ foobar: {}; }>
>new C({foobar: "example"}) : C<{ foobar: {}; }>
>C : typeof C
>{foobar: "example"} : { foobar: string; }
>foobar : string
>"example" : "example"

const x = <C foobar="example" />
>x : error
><C foobar="example" /> : error
>C : typeof C
>foobar : string

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// @jsx: preserve
// @strict: true
declare namespace JSX {
interface ElementAttributesProperty { props: {}; }
}

declare class Component<P> {
constructor(props: Readonly<P>);
readonly props: Readonly<P>;
}

class C<T> extends Component<{ x?: boolean; } & T> {}
const y = new C({foobar: "example"});
const x = <C foobar="example" />