From f701daf4e01468f990081ad6dea0649ccf161c09 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Mon, 22 Oct 2018 16:33:43 -0700 Subject: [PATCH] Infer over each mapped type constraint member if it is a union (#28006) --- src/compiler/checker.ts | 88 +++++++++++-------- src/compiler/types.ts | 2 + .../checkJsxIntersectionElementPropsType.js | 38 ++++++++ ...eckJsxIntersectionElementPropsType.symbols | 41 +++++++++ ...checkJsxIntersectionElementPropsType.types | 35 ++++++++ .../checkJsxIntersectionElementPropsType.tsx | 14 +++ 6 files changed, 182 insertions(+), 36 deletions(-) create mode 100644 tests/baselines/reference/checkJsxIntersectionElementPropsType.js create mode 100644 tests/baselines/reference/checkJsxIntersectionElementPropsType.symbols create mode 100644 tests/baselines/reference/checkJsxIntersectionElementPropsType.types create mode 100644 tests/cases/conformance/jsx/checkJsxIntersectionElementPropsType.tsx diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 9fab5821e4e..3ad881b9aff 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -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); @@ -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); @@ -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; @@ -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((source).typeArguments![0], target)); + return createArrayType(inferReverseMappedType((source).typeArguments![0], target, constraint)); } if (isReadonlyArrayType(source)) { - return createReadonlyArrayType(inferReverseMappedType((source).typeArguments![0], target)); + return createReadonlyArrayType(inferReverseMappedType((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); @@ -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 = getIndexedAccessType((getConstraintTypeFromMappedType(target)).type, getTypeParameterFromMappedType(target)); + function inferReverseMappedType(sourceType: Type, target: MappedType, constraint: IndexType): Type { + const typeParameter = getIndexedAccessType(constraint.type, getTypeParameterFromMappedType(target)); const templateType = getTemplateTypeFromMappedType(target); const inference = createInferenceInfo(typeParameter); inferTypes([inference], sourceType, templateType); @@ -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, for example. + const inference = getInferenceInfoForType((constraintType).type); + if (inference && !inference.isFixed) { + const inferredType = inferTypeForHomomorphicMappedType(source, 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(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 @@ -13850,31 +13890,7 @@ namespace ts { } if (getObjectFlags(target) & ObjectFlags.Mapped) { const constraintType = getConstraintTypeFromMappedType(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, for example. - const inference = getInferenceInfoForType((constraintType).type); - if (inference && !inference.isFixed) { - const inferredType = inferTypeForHomomorphicMappedType(source, 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(target)); + if (inferFromMappedTypeConstraint(source, target, constraintType)) { return; } } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 1c2009bc19f..21e09d799e9 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3682,6 +3682,7 @@ namespace ts { export interface ReverseMappedSymbol extends TransientSymbol { propertyType: Type; mappedType: MappedType; + constraintType: IndexType; } export const enum InternalSymbolName { @@ -4090,6 +4091,7 @@ namespace ts { export interface ReverseMappedType extends ObjectType { source: Type; mappedType: MappedType; + constraintType: IndexType; } /* @internal */ diff --git a/tests/baselines/reference/checkJsxIntersectionElementPropsType.js b/tests/baselines/reference/checkJsxIntersectionElementPropsType.js new file mode 100644 index 00000000000..cbdf2835c0b --- /dev/null +++ b/tests/baselines/reference/checkJsxIntersectionElementPropsType.js @@ -0,0 +1,38 @@ +//// [checkJsxIntersectionElementPropsType.tsx] +declare namespace JSX { + interface ElementAttributesProperty { props: {}; } +} + +declare class Component

{ + constructor(props: Readonly

); + readonly props: Readonly

; +} + +class C extends Component<{ x?: boolean; } & T> {} +const y = new C({foobar: "example"}); +const x = + +//// [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 = ; diff --git a/tests/baselines/reference/checkJsxIntersectionElementPropsType.symbols b/tests/baselines/reference/checkJsxIntersectionElementPropsType.symbols new file mode 100644 index 00000000000..aa0cf85044e --- /dev/null +++ b/tests/baselines/reference/checkJsxIntersectionElementPropsType.symbols @@ -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

{ +>Component : Symbol(Component, Decl(checkJsxIntersectionElementPropsType.tsx, 2, 1)) +>P : Symbol(P, Decl(checkJsxIntersectionElementPropsType.tsx, 4, 24)) + + constructor(props: Readonly

); +>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

; +>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 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 = +>x : Symbol(x, Decl(checkJsxIntersectionElementPropsType.tsx, 11, 5)) +>C : Symbol(C, Decl(checkJsxIntersectionElementPropsType.tsx, 7, 1)) +>foobar : Symbol(foobar, Decl(checkJsxIntersectionElementPropsType.tsx, 11, 12)) + diff --git a/tests/baselines/reference/checkJsxIntersectionElementPropsType.types b/tests/baselines/reference/checkJsxIntersectionElementPropsType.types new file mode 100644 index 00000000000..0e95dbed361 --- /dev/null +++ b/tests/baselines/reference/checkJsxIntersectionElementPropsType.types @@ -0,0 +1,35 @@ +=== tests/cases/conformance/jsx/checkJsxIntersectionElementPropsType.tsx === +declare namespace JSX { + interface ElementAttributesProperty { props: {}; } +>props : {} +} + +declare class Component

{ +>Component : Component

+ + constructor(props: Readonly

); +>props : Readonly

+ + readonly props: Readonly

; +>props : Readonly

+} + +class C extends Component<{ x?: boolean; } & T> {} +>C : C +>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 = +>x : error +> : error +>C : typeof C +>foobar : string + diff --git a/tests/cases/conformance/jsx/checkJsxIntersectionElementPropsType.tsx b/tests/cases/conformance/jsx/checkJsxIntersectionElementPropsType.tsx new file mode 100644 index 00000000000..3a7260a97c8 --- /dev/null +++ b/tests/cases/conformance/jsx/checkJsxIntersectionElementPropsType.tsx @@ -0,0 +1,14 @@ +// @jsx: preserve +// @strict: true +declare namespace JSX { + interface ElementAttributesProperty { props: {}; } +} + +declare class Component

{ + constructor(props: Readonly

); + readonly props: Readonly

; +} + +class C extends Component<{ x?: boolean; } & T> {} +const y = new C({foobar: "example"}); +const x = \ No newline at end of file