diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 1d2887da015d3..3f20314adbb65 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -14131,16 +14131,18 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return modifiers & MappedTypeModifiers.ExcludeOptional ? -1 : modifiers & MappedTypeModifiers.IncludeOptional ? 1 : 0; } - function getModifiersTypeOptionality(type: Type): number { - return type.flags & TypeFlags.Intersection ? Math.max(...map((type as IntersectionType).types, getModifiersTypeOptionality)) : - getObjectFlags(type) & ObjectFlags.Mapped ? getCombinedMappedTypeOptionality(type as MappedType) : - 0; - } - // Return -1, 0, or 1, for stripped, unchanged, or added optionality respectively. When a homomorphic mapped type doesn't // modify optionality, recursively consult the optionality of the type being mapped over to see if it strips or adds optionality. - function getCombinedMappedTypeOptionality(type: MappedType): number { - return getMappedTypeOptionality(type) || getModifiersTypeOptionality(getModifiersTypeFromMappedType(type)); + // For intersections, return -1 or 1 when all constituents strip or add optionality, otherwise return 0. + function getCombinedMappedTypeOptionality(type: Type): number { + if (getObjectFlags(type) & ObjectFlags.Mapped) { + return getMappedTypeOptionality(type as MappedType) || getCombinedMappedTypeOptionality(getModifiersTypeFromMappedType(type as MappedType)); + } + if (type.flags & TypeFlags.Intersection) { + const optionality = getCombinedMappedTypeOptionality((type as IntersectionType).types[0]); + return every((type as IntersectionType).types, (t, i) => i === 0 || getCombinedMappedTypeOptionality(t) === optionality) ? optionality : 0; + } + return 0; } function isPartialMappedType(type: Type) { @@ -18671,11 +18673,27 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return !!(getUnionType([intersectTypes(type1, type2), neverType]).flags & TypeFlags.Never); } + // Given an indexed access on a mapped type of the form { [P in K]: E }[X], return an instantiation of E where P is + // replaced with X. Since this simplification doesn't account for mapped type modifiers, add 'undefined' to the + // resulting type if the mapped type includes a '?' modifier or if the modifiers type indicates that some properties + // are optional. If the modifiers type is generic, conservatively estimate optionality by recursively looking for + // mapped types that include '?' modifiers. function substituteIndexedMappedType(objectType: MappedType, index: Type) { const mapper = createTypeMapper([getTypeParameterFromMappedType(objectType)], [index]); const templateMapper = combineTypeMappers(objectType.mapper, mapper); const instantiatedTemplateType = instantiateType(getTemplateTypeFromMappedType(objectType.target as MappedType || objectType), templateMapper); - return addOptionality(instantiatedTemplateType, /*isProperty*/ true, getCombinedMappedTypeOptionality(objectType) > 0); + const isOptional = getMappedTypeOptionality(objectType) > 0 || (isGenericType(objectType) ? + getCombinedMappedTypeOptionality(getModifiersTypeFromMappedType(objectType)) > 0 : + couldAccessOptionalProperty(objectType, index)); + return addOptionality(instantiatedTemplateType, /*isProperty*/ true, isOptional); + } + + // Return true if an indexed access with the given object and index types could access an optional property. + function couldAccessOptionalProperty(objectType: Type, indexType: Type) { + const indexConstraint = getBaseConstraintOfType(indexType); + return !!indexConstraint && some(getPropertiesOfType(objectType), p => + !!(p.flags & SymbolFlags.Optional) && + isTypeAssignableTo(getLiteralTypeFromProperty(p, TypeFlags.StringOrNumberLiteralOrUnique), indexConstraint)); } function getIndexedAccessType(objectType: Type, indexType: Type, accessFlags = AccessFlags.None, accessNode?: ElementAccessExpression | IndexedAccessTypeNode | PropertyName | BindingName | SyntheticExpression, aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[]): Type { diff --git a/tests/baselines/reference/mappedTypeIndexedAccessConstraint.errors.txt b/tests/baselines/reference/mappedTypeIndexedAccessConstraint.errors.txt index a04e49d09b272..39b15dcee0e0e 100644 --- a/tests/baselines/reference/mappedTypeIndexedAccessConstraint.errors.txt +++ b/tests/baselines/reference/mappedTypeIndexedAccessConstraint.errors.txt @@ -72,4 +72,36 @@ mappedTypeIndexedAccessConstraint.ts(53,34): error TS2722: Cannot invoke an obje const resolveMapper2 = ( key: K, o: MapperArgs) => mapper[key]?.(o) + + // Repro from #57860 + + type Obj1 = { + a: string; + b: number; + }; + + type Obj2 = { + b: number; + c: boolean; + }; + + declare const mapIntersection: { + [K in keyof (Partial & Required)]: number; + }; + + const accessMapped = (key: K) => mapIntersection[key].toString(); + + declare const resolved: { a?: number | undefined; b: number; c: number }; + + const accessResolved = (key: K) => resolved[key].toString(); + + // Additional repro from #57860 + + type Foo = { + prop: string; + } + + function test(obj: Pick & Partial, K>, key: K) { + obj[key].length; + } \ No newline at end of file diff --git a/tests/baselines/reference/mappedTypeIndexedAccessConstraint.symbols b/tests/baselines/reference/mappedTypeIndexedAccessConstraint.symbols index 96d7dd12eb001..a8ba03f424eca 100644 --- a/tests/baselines/reference/mappedTypeIndexedAccessConstraint.symbols +++ b/tests/baselines/reference/mappedTypeIndexedAccessConstraint.symbols @@ -225,3 +225,97 @@ const resolveMapper2 = ( >key : Symbol(key, Decl(mappedTypeIndexedAccessConstraint.ts, 54, 55)) >o : Symbol(o, Decl(mappedTypeIndexedAccessConstraint.ts, 55, 11)) +// Repro from #57860 + +type Obj1 = { +>Obj1 : Symbol(Obj1, Decl(mappedTypeIndexedAccessConstraint.ts, 55, 49)) + + a: string; +>a : Symbol(a, Decl(mappedTypeIndexedAccessConstraint.ts, 59, 13)) + + b: number; +>b : Symbol(b, Decl(mappedTypeIndexedAccessConstraint.ts, 60, 14)) + +}; + +type Obj2 = { +>Obj2 : Symbol(Obj2, Decl(mappedTypeIndexedAccessConstraint.ts, 62, 2)) + + b: number; +>b : Symbol(b, Decl(mappedTypeIndexedAccessConstraint.ts, 64, 13)) + + c: boolean; +>c : Symbol(c, Decl(mappedTypeIndexedAccessConstraint.ts, 65, 14)) + +}; + +declare const mapIntersection: { +>mapIntersection : Symbol(mapIntersection, Decl(mappedTypeIndexedAccessConstraint.ts, 69, 13)) + + [K in keyof (Partial & Required)]: number; +>K : Symbol(K, Decl(mappedTypeIndexedAccessConstraint.ts, 70, 5)) +>Partial : Symbol(Partial, Decl(lib.es5.d.ts, --, --)) +>Obj1 : Symbol(Obj1, Decl(mappedTypeIndexedAccessConstraint.ts, 55, 49)) +>Required : Symbol(Required, Decl(lib.es5.d.ts, --, --)) +>Obj2 : Symbol(Obj2, Decl(mappedTypeIndexedAccessConstraint.ts, 62, 2)) + +}; + +const accessMapped = (key: K) => mapIntersection[key].toString(); +>accessMapped : Symbol(accessMapped, Decl(mappedTypeIndexedAccessConstraint.ts, 73, 5)) +>K : Symbol(K, Decl(mappedTypeIndexedAccessConstraint.ts, 73, 22)) +>Obj2 : Symbol(Obj2, Decl(mappedTypeIndexedAccessConstraint.ts, 62, 2)) +>key : Symbol(key, Decl(mappedTypeIndexedAccessConstraint.ts, 73, 44)) +>K : Symbol(K, Decl(mappedTypeIndexedAccessConstraint.ts, 73, 22)) +>mapIntersection[key].toString : Symbol(Number.toString, Decl(lib.es5.d.ts, --, --)) +>mapIntersection : Symbol(mapIntersection, Decl(mappedTypeIndexedAccessConstraint.ts, 69, 13)) +>key : Symbol(key, Decl(mappedTypeIndexedAccessConstraint.ts, 73, 44)) +>toString : Symbol(Number.toString, Decl(lib.es5.d.ts, --, --)) + +declare const resolved: { a?: number | undefined; b: number; c: number }; +>resolved : Symbol(resolved, Decl(mappedTypeIndexedAccessConstraint.ts, 75, 13)) +>a : Symbol(a, Decl(mappedTypeIndexedAccessConstraint.ts, 75, 25)) +>b : Symbol(b, Decl(mappedTypeIndexedAccessConstraint.ts, 75, 49)) +>c : Symbol(c, Decl(mappedTypeIndexedAccessConstraint.ts, 75, 60)) + +const accessResolved = (key: K) => resolved[key].toString(); +>accessResolved : Symbol(accessResolved, Decl(mappedTypeIndexedAccessConstraint.ts, 77, 5)) +>K : Symbol(K, Decl(mappedTypeIndexedAccessConstraint.ts, 77, 24)) +>Obj2 : Symbol(Obj2, Decl(mappedTypeIndexedAccessConstraint.ts, 62, 2)) +>key : Symbol(key, Decl(mappedTypeIndexedAccessConstraint.ts, 77, 46)) +>K : Symbol(K, Decl(mappedTypeIndexedAccessConstraint.ts, 77, 24)) +>resolved[key].toString : Symbol(Number.toString, Decl(lib.es5.d.ts, --, --)) +>resolved : Symbol(resolved, Decl(mappedTypeIndexedAccessConstraint.ts, 75, 13)) +>key : Symbol(key, Decl(mappedTypeIndexedAccessConstraint.ts, 77, 46)) +>toString : Symbol(Number.toString, Decl(lib.es5.d.ts, --, --)) + +// Additional repro from #57860 + +type Foo = { +>Foo : Symbol(Foo, Decl(mappedTypeIndexedAccessConstraint.ts, 77, 82)) + + prop: string; +>prop : Symbol(prop, Decl(mappedTypeIndexedAccessConstraint.ts, 81, 12)) +} + +function test(obj: Pick & Partial, K>, key: K) { +>test : Symbol(test, Decl(mappedTypeIndexedAccessConstraint.ts, 83, 1)) +>K : Symbol(K, Decl(mappedTypeIndexedAccessConstraint.ts, 85, 14)) +>Foo : Symbol(Foo, Decl(mappedTypeIndexedAccessConstraint.ts, 77, 82)) +>obj : Symbol(obj, Decl(mappedTypeIndexedAccessConstraint.ts, 85, 35)) +>Pick : Symbol(Pick, Decl(lib.es5.d.ts, --, --)) +>Required : Symbol(Required, Decl(lib.es5.d.ts, --, --)) +>Foo : Symbol(Foo, Decl(mappedTypeIndexedAccessConstraint.ts, 77, 82)) +>Partial : Symbol(Partial, Decl(lib.es5.d.ts, --, --)) +>Foo : Symbol(Foo, Decl(mappedTypeIndexedAccessConstraint.ts, 77, 82)) +>K : Symbol(K, Decl(mappedTypeIndexedAccessConstraint.ts, 85, 14)) +>key : Symbol(key, Decl(mappedTypeIndexedAccessConstraint.ts, 85, 78)) +>K : Symbol(K, Decl(mappedTypeIndexedAccessConstraint.ts, 85, 14)) + + obj[key].length; +>obj[key].length : Symbol(String.length, Decl(lib.es5.d.ts, --, --)) +>obj : Symbol(obj, Decl(mappedTypeIndexedAccessConstraint.ts, 85, 35)) +>key : Symbol(key, Decl(mappedTypeIndexedAccessConstraint.ts, 85, 78)) +>length : Symbol(String.length, Decl(lib.es5.d.ts, --, --)) +} + diff --git a/tests/baselines/reference/mappedTypeIndexedAccessConstraint.types b/tests/baselines/reference/mappedTypeIndexedAccessConstraint.types index 2842c9e1c6fc7..36ab99da95ca3 100644 --- a/tests/baselines/reference/mappedTypeIndexedAccessConstraint.types +++ b/tests/baselines/reference/mappedTypeIndexedAccessConstraint.types @@ -307,3 +307,122 @@ const resolveMapper2 = ( >o : MapperArgs > : ^^^^^^^^^^^^^ +// Repro from #57860 + +type Obj1 = { +>Obj1 : Obj1 +> : ^^^^ + + a: string; +>a : string +> : ^^^^^^ + + b: number; +>b : number +> : ^^^^^^ + +}; + +type Obj2 = { +>Obj2 : Obj2 +> : ^^^^ + + b: number; +>b : number +> : ^^^^^^ + + c: boolean; +>c : boolean +> : ^^^^^^^ + +}; + +declare const mapIntersection: { +>mapIntersection : { a?: number | undefined; b: number; c: number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + [K in keyof (Partial & Required)]: number; +}; + +const accessMapped = (key: K) => mapIntersection[key].toString(); +>accessMapped : (key: K) => string +> : ^ ^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^ +>(key: K) => mapIntersection[key].toString() : (key: K) => string +> : ^ ^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^ +>key : K +> : ^ +>mapIntersection[key].toString() : string +> : ^^^^^^ +>mapIntersection[key].toString : (radix?: number | undefined) => string +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>mapIntersection[key] : { a?: number | undefined; b: number; c: number; }[K] +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>mapIntersection : { a?: number | undefined; b: number; c: number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>key : K +> : ^ +>toString : (radix?: number | undefined) => string +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +declare const resolved: { a?: number | undefined; b: number; c: number }; +>resolved : { a?: number | undefined; b: number; c: number; } +> : ^^^^^^ ^^^^^ ^^^^^ ^^^ +>a : number | undefined +> : ^^^^^^^^^^^^^^^^^^ +>b : number +> : ^^^^^^ +>c : number +> : ^^^^^^ + +const accessResolved = (key: K) => resolved[key].toString(); +>accessResolved : (key: K) => string +> : ^ ^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^ +>(key: K) => resolved[key].toString() : (key: K) => string +> : ^ ^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^ +>key : K +> : ^ +>resolved[key].toString() : string +> : ^^^^^^ +>resolved[key].toString : (radix?: number | undefined) => string +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>resolved[key] : { a?: number | undefined; b: number; c: number; }[K] +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>resolved : { a?: number | undefined; b: number; c: number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>key : K +> : ^ +>toString : (radix?: number | undefined) => string +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +// Additional repro from #57860 + +type Foo = { +>Foo : Foo +> : ^^^ + + prop: string; +>prop : string +> : ^^^^^^ +} + +function test(obj: Pick & Partial, K>, key: K) { +>test : (obj: Pick & Partial, K>, key: K) => void +> : ^ ^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^ ^^^^^^^^^ +>obj : Pick & Partial, K> +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>key : K +> : ^ + + obj[key].length; +>obj[key].length : number +> : ^^^^^^ +>obj[key] : Pick & Partial, K>[K] +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>obj : Pick & Partial, K> +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>key : K +> : ^ +>length : number +> : ^^^^^^ +} + diff --git a/tests/cases/compiler/mappedTypeIndexedAccessConstraint.ts b/tests/cases/compiler/mappedTypeIndexedAccessConstraint.ts index bf03035870f0b..893334f51dce3 100644 --- a/tests/cases/compiler/mappedTypeIndexedAccessConstraint.ts +++ b/tests/cases/compiler/mappedTypeIndexedAccessConstraint.ts @@ -57,3 +57,35 @@ const resolveMapper1 = ( const resolveMapper2 = ( key: K, o: MapperArgs) => mapper[key]?.(o) + +// Repro from #57860 + +type Obj1 = { + a: string; + b: number; +}; + +type Obj2 = { + b: number; + c: boolean; +}; + +declare const mapIntersection: { + [K in keyof (Partial & Required)]: number; +}; + +const accessMapped = (key: K) => mapIntersection[key].toString(); + +declare const resolved: { a?: number | undefined; b: number; c: number }; + +const accessResolved = (key: K) => resolved[key].toString(); + +// Additional repro from #57860 + +type Foo = { + prop: string; +} + +function test(obj: Pick & Partial, K>, key: K) { + obj[key].length; +}