From 8d329ae026d1d930bb6479141a272f2462700a5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Wed, 11 May 2022 12:12:43 +0200 Subject: [PATCH] Fixed an issue with a concrete object not being assignable to a generic mapped type with a partially concrete constraint --- src/compiler/checker.ts | 13 +- ...eteConstraintAcceptsConcreteObject.symbols | 141 ++++++++++++++++++ ...creteConstraintAcceptsConcreteObject.types | 108 ++++++++++++++ ...ConcreteConstraintAcceptsConcreteObject.ts | 50 +++++++ 4 files changed, 310 insertions(+), 2 deletions(-) create mode 100644 tests/baselines/reference/mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.symbols create mode 100644 tests/baselines/reference/mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.types create mode 100644 tests/cases/compiler/mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 025eeecc30624..e23e25eb0b331 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -19519,8 +19519,17 @@ namespace ts { ? getIntersectionType([filteredByApplicability, typeParameter]) : typeParameter; const indexedAccessType = getIndexedAccessType(source, indexingType); - // Compare `S[indexingType]` to `T`, where `T` is the type of a property of the target type. - if (result = isRelatedTo(indexedAccessType, templateType, RecursionFlags.Both, reportErrors)) { + + if (filteredByApplicability && isTypeAssignableTo(filteredByApplicability, target.constraintType!)) { + const mapper = appendTypeMapping(target.mapper, getTypeParameterFromMappedType(target), filteredByApplicability) + const propType = instantiateType(templateType, mapper); + if (result = isRelatedTo(indexedAccessType, propType)) { + return result; + } + } else if ( + // Compare `S[indexingType]` to `T`, where `T` is the type of a property of the target type. + result = isRelatedTo(indexedAccessType, templateType, RecursionFlags.Both, reportErrors) + ) { return result; } } diff --git a/tests/baselines/reference/mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.symbols b/tests/baselines/reference/mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.symbols new file mode 100644 index 0000000000000..976fd21424fbe --- /dev/null +++ b/tests/baselines/reference/mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.symbols @@ -0,0 +1,141 @@ +=== tests/cases/compiler/mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts === +type ExtractEvent< +>ExtractEvent : Symbol(ExtractEvent, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 0, 0)) + + TEvent extends { type: string }, +>TEvent : Symbol(TEvent, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 0, 18)) +>type : Symbol(type, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 1, 18)) + + TEventType extends TEvent["type"] +>TEventType : Symbol(TEventType, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 1, 34)) +>TEvent : Symbol(TEvent, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 0, 18)) + +> = TEvent extends { +>TEvent : Symbol(TEvent, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 0, 18)) + + type: TEventType; +>type : Symbol(type, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 3, 20)) +>TEventType : Symbol(TEventType, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 1, 34)) +} + ? TEvent +>TEvent : Symbol(TEvent, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 0, 18)) + + : never; + +type TransitionConfig = { +>TransitionConfig : Symbol(TransitionConfig, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 7, 10)) +>TContext : Symbol(TContext, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 9, 22)) +>TEvent : Symbol(TEvent, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 9, 31)) +>type : Symbol(type, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 9, 48)) + + actions?: { +>actions : Symbol(actions, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 9, 68)) + + type: string; +>type : Symbol(type, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 10, 13)) + + }; +}; + +type IntersectedTransitionConfigMap = { +>IntersectedTransitionConfigMap : Symbol(IntersectedTransitionConfigMap, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 13, 2)) +>TContext : Symbol(TContext, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 15, 36)) +>TEvent : Symbol(TEvent, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 15, 45)) +>type : Symbol(type, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 15, 62)) + + [K in TEvent["type"]]?: TransitionConfig>; +>K : Symbol(K, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 16, 3)) +>TEvent : Symbol(TEvent, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 15, 45)) +>TransitionConfig : Symbol(TransitionConfig, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 7, 10)) +>TContext : Symbol(TContext, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 15, 36)) +>ExtractEvent : Symbol(ExtractEvent, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 0, 0)) +>TEvent : Symbol(TEvent, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 15, 45)) +>K : Symbol(K, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 16, 3)) + +} & { + "*": TransitionConfig; +>"*" : Symbol("*", Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 17, 5)) +>TransitionConfig : Symbol(TransitionConfig, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 7, 10)) +>TContext : Symbol(TContext, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 15, 36)) +>TEvent : Symbol(TEvent, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 15, 45)) + +}; + +type TransitionConfigMap = { +>TransitionConfigMap : Symbol(TransitionConfigMap, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 19, 2)) +>TContext : Symbol(TContext, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 21, 25)) +>TEvent : Symbol(TEvent, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 21, 34)) +>type : Symbol(type, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 21, 51)) + + [K in TEvent["type"] | "*"]?: K extends "*" +>K : Symbol(K, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 22, 3)) +>TEvent : Symbol(TEvent, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 21, 34)) +>K : Symbol(K, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 22, 3)) + + ? TransitionConfig +>TransitionConfig : Symbol(TransitionConfig, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 7, 10)) +>TContext : Symbol(TContext, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 21, 25)) +>TEvent : Symbol(TEvent, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 21, 34)) + + : TransitionConfig>; +>TransitionConfig : Symbol(TransitionConfig, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 7, 10)) +>TContext : Symbol(TContext, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 21, 25)) +>ExtractEvent : Symbol(ExtractEvent, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 0, 0)) +>TEvent : Symbol(TEvent, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 21, 34)) +>K : Symbol(K, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 22, 3)) + +}; + +export function genericFn() { +>genericFn : Symbol(genericFn, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 25, 2)) +>TEvent : Symbol(TEvent, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 27, 26)) +>type : Symbol(type, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 27, 42)) + + const wildcardTransitionConfig = { +>wildcardTransitionConfig : Symbol(wildcardTransitionConfig, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 28, 7)) + + "*": { actions: { type: "someAction" } }, +>"*" : Symbol("*", Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 28, 36)) +>actions : Symbol(actions, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 29, 10)) +>type : Symbol(type, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 29, 21)) + + } as const; +>const : Symbol(const) + + // this should be assignable, in the same way as the following assignment is OK + let test: TransitionConfigMap< +>test : Symbol(test, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 33, 5)) +>TransitionConfigMap : Symbol(TransitionConfigMap, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 19, 2)) + + { counter: number }, +>counter : Symbol(counter, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 34, 5)) + + { type: TEvent["type"] } +>type : Symbol(type, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 35, 5)) +>TEvent : Symbol(TEvent, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 27, 26)) + + > = {} as typeof wildcardTransitionConfig; +>wildcardTransitionConfig : Symbol(wildcardTransitionConfig, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 28, 7)) + + // concrete prop is assignable to the concrete prop of this mapped type + test["*"] = {} as typeof wildcardTransitionConfig["*"]; +>test : Symbol(test, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 33, 5)) +>"*" : Symbol("*") +>wildcardTransitionConfig : Symbol(wildcardTransitionConfig, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 28, 7)) + + // similar intersected type accepts this concrete object + let test2: IntersectedTransitionConfigMap< +>test2 : Symbol(test2, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 42, 5)) +>IntersectedTransitionConfigMap : Symbol(IntersectedTransitionConfigMap, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 13, 2)) + + { counter: number }, +>counter : Symbol(counter, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 43, 5)) + + { type: TEvent["type"] } +>type : Symbol(type, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 44, 5)) +>TEvent : Symbol(TEvent, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 27, 26)) + + > = {} as typeof wildcardTransitionConfig; +>wildcardTransitionConfig : Symbol(wildcardTransitionConfig, Decl(mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts, 28, 7)) +} + diff --git a/tests/baselines/reference/mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.types b/tests/baselines/reference/mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.types new file mode 100644 index 0000000000000..f3a184fc985e2 --- /dev/null +++ b/tests/baselines/reference/mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.types @@ -0,0 +1,108 @@ +=== tests/cases/compiler/mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts === +type ExtractEvent< +>ExtractEvent : ExtractEvent + + TEvent extends { type: string }, +>type : string + + TEventType extends TEvent["type"] +> = TEvent extends { + type: TEventType; +>type : TEventType +} + ? TEvent + : never; + +type TransitionConfig = { +>TransitionConfig : TransitionConfig +>type : string + + actions?: { +>actions : { type: string; } | undefined + + type: string; +>type : string + + }; +}; + +type IntersectedTransitionConfigMap = { +>IntersectedTransitionConfigMap : IntersectedTransitionConfigMap +>type : string + + [K in TEvent["type"]]?: TransitionConfig>; +} & { + "*": TransitionConfig; +>"*" : TransitionConfig + +}; + +type TransitionConfigMap = { +>TransitionConfigMap : TransitionConfigMap +>type : string + + [K in TEvent["type"] | "*"]?: K extends "*" + ? TransitionConfig + : TransitionConfig>; +}; + +export function genericFn() { +>genericFn : () => void +>type : string + + const wildcardTransitionConfig = { +>wildcardTransitionConfig : { readonly "*": { readonly actions: { readonly type: "someAction"; }; }; } +>{ "*": { actions: { type: "someAction" } }, } as const : { readonly "*": { readonly actions: { readonly type: "someAction"; }; }; } +>{ "*": { actions: { type: "someAction" } }, } : { readonly "*": { readonly actions: { readonly type: "someAction"; }; }; } + + "*": { actions: { type: "someAction" } }, +>"*" : { readonly actions: { readonly type: "someAction"; }; } +>{ actions: { type: "someAction" } } : { readonly actions: { readonly type: "someAction"; }; } +>actions : { readonly type: "someAction"; } +>{ type: "someAction" } : { readonly type: "someAction"; } +>type : "someAction" +>"someAction" : "someAction" + + } as const; + + // this should be assignable, in the same way as the following assignment is OK + let test: TransitionConfigMap< +>test : TransitionConfigMap<{ counter: number; }, { type: TEvent["type"]; }> + + { counter: number }, +>counter : number + + { type: TEvent["type"] } +>type : TEvent["type"] + + > = {} as typeof wildcardTransitionConfig; +>{} as typeof wildcardTransitionConfig : { readonly "*": { readonly actions: { readonly type: "someAction"; }; }; } +>{} : {} +>wildcardTransitionConfig : { readonly "*": { readonly actions: { readonly type: "someAction"; }; }; } + + // concrete prop is assignable to the concrete prop of this mapped type + test["*"] = {} as typeof wildcardTransitionConfig["*"]; +>test["*"] = {} as typeof wildcardTransitionConfig["*"] : { readonly actions: { readonly type: "someAction"; }; } +>test["*"] : TransitionConfig<{ counter: number; }, { type: TEvent["type"]; }> | undefined +>test : TransitionConfigMap<{ counter: number; }, { type: TEvent["type"]; }> +>"*" : "*" +>{} as typeof wildcardTransitionConfig["*"] : { readonly actions: { readonly type: "someAction"; }; } +>{} : {} +>wildcardTransitionConfig : { readonly "*": { readonly actions: { readonly type: "someAction"; }; }; } + + // similar intersected type accepts this concrete object + let test2: IntersectedTransitionConfigMap< +>test2 : IntersectedTransitionConfigMap<{ counter: number; }, { type: TEvent["type"]; }> + + { counter: number }, +>counter : number + + { type: TEvent["type"] } +>type : TEvent["type"] + + > = {} as typeof wildcardTransitionConfig; +>{} as typeof wildcardTransitionConfig : { readonly "*": { readonly actions: { readonly type: "someAction"; }; }; } +>{} : {} +>wildcardTransitionConfig : { readonly "*": { readonly actions: { readonly type: "someAction"; }; }; } +} + diff --git a/tests/cases/compiler/mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts b/tests/cases/compiler/mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts new file mode 100644 index 0000000000000..498d2a1f1eb58 --- /dev/null +++ b/tests/cases/compiler/mappedTypeWithPartiallyConcreteConstraintAcceptsConcreteObject.ts @@ -0,0 +1,50 @@ +// @strict: true +// @noEmit: true + +type ExtractEvent< + TEvent extends { type: string }, + TEventType extends TEvent["type"] +> = TEvent extends { + type: TEventType; +} + ? TEvent + : never; + +type TransitionConfig = { + actions?: { + type: string; + }; +}; + +type IntersectedTransitionConfigMap = { + [K in TEvent["type"]]?: TransitionConfig>; +} & { + "*": TransitionConfig; +}; + +type TransitionConfigMap = { + [K in TEvent["type"] | "*"]?: K extends "*" + ? TransitionConfig + : TransitionConfig>; +}; + +export function genericFn() { + const wildcardTransitionConfig = { + "*": { actions: { type: "someAction" } }, + } as const; + + // this should be assignable, in the same way as the following assignment is OK + let test: TransitionConfigMap< + { counter: number }, + { type: TEvent["type"] } + > = {} as typeof wildcardTransitionConfig; + + // concrete prop is assignable to the concrete prop of this mapped type + test["*"] = {} as typeof wildcardTransitionConfig["*"]; + + // similar intersected type accepts this concrete object + let test2: IntersectedTransitionConfigMap< + { counter: number }, + { type: TEvent["type"] } + > = {} as typeof wildcardTransitionConfig; +}