Skip to content
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

Distribute mapped types over array/tuple intersections #57801

Merged
merged 2 commits into from
Mar 18, 2024
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
53 changes: 29 additions & 24 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14569,14 +14569,18 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const constraint = getConstraintTypeFromMappedType(type);
if (constraint.flags & TypeFlags.Index) {
const baseConstraint = getBaseConstraintOfType((constraint as IndexType).type);
if (baseConstraint && everyType(baseConstraint, isArrayOrTupleType)) {
if (baseConstraint && everyType(baseConstraint, t => isArrayOrTupleType(t) || isArrayOrTupleOrIntersection(t))) {
return instantiateType(target, prependTypeMapping(typeVariable, baseConstraint, type.mapper));
}
}
}
return type;
}

function isArrayOrTupleOrIntersection(type: Type) {
return !!(type.flags & TypeFlags.Intersection) && every((type as IntersectionType).types, isArrayOrTupleType);
}

function isMappedTypeGenericIndexedAccess(type: Type) {
let objectType;
return !!(type.flags & TypeFlags.IndexedAccess && getObjectFlags(objectType = (type as IndexedAccessType).objectType) & ObjectFlags.Mapped &&
Expand Down Expand Up @@ -19767,6 +19771,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// * If T is a union type we distribute the mapped type over the union.
// * If T is an array we map to an array where the element type has been transformed.
// * If T is a tuple we map to a tuple where the element types have been transformed.
// * If T is an intersection of array or tuple types we map to an intersection of transformed array or tuple types.
// * Otherwise we map to an object type where the type of each property has been transformed.
// For example, when T is instantiated to a union type A | B, we produce { [P in keyof A]: X } |
// { [P in keyof B]: X }, and when when T is instantiated to a union type A | undefined, we produce
Expand All @@ -19775,33 +19780,33 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (typeVariable) {
const mappedTypeVariable = instantiateType(typeVariable, mapper);
if (typeVariable !== mappedTypeVariable) {
return mapTypeWithAlias(
getReducedType(mappedTypeVariable),
t => {
if (t.flags & (TypeFlags.AnyOrUnknown | TypeFlags.InstantiableNonPrimitive | TypeFlags.Object | TypeFlags.Intersection) && t !== wildcardType && !isErrorType(t)) {
if (!type.declaration.nameType) {
let constraint;
if (
isArrayType(t) || t.flags & TypeFlags.Any && findResolutionCycleStartIndex(typeVariable, TypeSystemPropertyName.ImmediateBaseConstraint) < 0 &&
(constraint = getConstraintOfTypeParameter(typeVariable)) && everyType(constraint, isArrayOrTupleType)
) {
return instantiateMappedArrayType(t, type, prependTypeMapping(typeVariable, t, mapper));
}
if (isTupleType(t)) {
return instantiateMappedTupleType(t, type, typeVariable, mapper);
}
}
return instantiateAnonymousType(type, prependTypeMapping(typeVariable, t, mapper));
}
return t;
},
aliasSymbol,
aliasTypeArguments,
);
return mapTypeWithAlias(getReducedType(mappedTypeVariable), instantiateConstituent, aliasSymbol, aliasTypeArguments);
}
}
// If the constraint type of the instantiation is the wildcard type, return the wildcard type.
return instantiateType(getConstraintTypeFromMappedType(type), mapper) === wildcardType ? wildcardType : instantiateAnonymousType(type, mapper, aliasSymbol, aliasTypeArguments);

function instantiateConstituent(t: Type): Type {
if (t.flags & (TypeFlags.AnyOrUnknown | TypeFlags.InstantiableNonPrimitive | TypeFlags.Object | TypeFlags.Intersection) && t !== wildcardType && !isErrorType(t)) {
if (!type.declaration.nameType) {
let constraint;
if (
isArrayType(t) || t.flags & TypeFlags.Any && findResolutionCycleStartIndex(typeVariable!, TypeSystemPropertyName.ImmediateBaseConstraint) < 0 &&
(constraint = getConstraintOfTypeParameter(typeVariable!)) && everyType(constraint, isArrayOrTupleType)
) {
return instantiateMappedArrayType(t, type, prependTypeMapping(typeVariable!, t, mapper));
}
if (isTupleType(t)) {
return instantiateMappedTupleType(t, type, typeVariable!, mapper);
}
if (isArrayOrTupleOrIntersection(t)) {
return getIntersectionType(map((t as IntersectionType).types, instantiateConstituent));
}
}
return instantiateAnonymousType(type, prependTypeMapping(typeVariable!, t, mapper));
}
return t;
}
}

function getModifiedReadonlyState(state: boolean, modifiers: MappedTypeModifiers) {
Expand Down
62 changes: 62 additions & 0 deletions tests/baselines/reference/mappedArrayTupleIntersections.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//// [tests/cases/compiler/mappedArrayTupleIntersections.ts] ////

=== mappedArrayTupleIntersections.ts ===
type Box<T> = { value: T };
>Box : Symbol(Box, Decl(mappedArrayTupleIntersections.ts, 0, 0))
>T : Symbol(T, Decl(mappedArrayTupleIntersections.ts, 0, 9))
>value : Symbol(value, Decl(mappedArrayTupleIntersections.ts, 0, 15))
>T : Symbol(T, Decl(mappedArrayTupleIntersections.ts, 0, 9))

type Boxify<T> = { [K in keyof T]: Box<T[K]> };
>Boxify : Symbol(Boxify, Decl(mappedArrayTupleIntersections.ts, 0, 27))
>T : Symbol(T, Decl(mappedArrayTupleIntersections.ts, 1, 12))
>K : Symbol(K, Decl(mappedArrayTupleIntersections.ts, 1, 20))
>T : Symbol(T, Decl(mappedArrayTupleIntersections.ts, 1, 12))
>Box : Symbol(Box, Decl(mappedArrayTupleIntersections.ts, 0, 0))
>T : Symbol(T, Decl(mappedArrayTupleIntersections.ts, 1, 12))
>K : Symbol(K, Decl(mappedArrayTupleIntersections.ts, 1, 20))

type T1 = Boxify<string[]>;
>T1 : Symbol(T1, Decl(mappedArrayTupleIntersections.ts, 1, 47))
>Boxify : Symbol(Boxify, Decl(mappedArrayTupleIntersections.ts, 0, 27))

type T2 = Boxify<[string, string]>;
>T2 : Symbol(T2, Decl(mappedArrayTupleIntersections.ts, 3, 27))
>Boxify : Symbol(Boxify, Decl(mappedArrayTupleIntersections.ts, 0, 27))

type T3 = Boxify<string[] & unknown[]>;
>T3 : Symbol(T3, Decl(mappedArrayTupleIntersections.ts, 4, 35))
>Boxify : Symbol(Boxify, Decl(mappedArrayTupleIntersections.ts, 0, 27))

type T4 = Boxify<string[] & [string, string]>;
>T4 : Symbol(T4, Decl(mappedArrayTupleIntersections.ts, 5, 39))
>Boxify : Symbol(Boxify, Decl(mappedArrayTupleIntersections.ts, 0, 27))

type T5 = Boxify<string[] & { x: string }>;
>T5 : Symbol(T5, Decl(mappedArrayTupleIntersections.ts, 6, 46))
>Boxify : Symbol(Boxify, Decl(mappedArrayTupleIntersections.ts, 0, 27))
>x : Symbol(x, Decl(mappedArrayTupleIntersections.ts, 7, 29))

// https://github.com/microsoft/TypeScript/issues/57744

type MustBeArray<T extends any[]> = T;
>MustBeArray : Symbol(MustBeArray, Decl(mappedArrayTupleIntersections.ts, 7, 43))
>T : Symbol(T, Decl(mappedArrayTupleIntersections.ts, 11, 17))
>T : Symbol(T, Decl(mappedArrayTupleIntersections.ts, 11, 17))

type Hmm<T extends any[]> = T extends number[] ?
>Hmm : Symbol(Hmm, Decl(mappedArrayTupleIntersections.ts, 11, 38))
>T : Symbol(T, Decl(mappedArrayTupleIntersections.ts, 13, 9))
>T : Symbol(T, Decl(mappedArrayTupleIntersections.ts, 13, 9))

MustBeArray<{ [I in keyof T]: 1 }> :
>MustBeArray : Symbol(MustBeArray, Decl(mappedArrayTupleIntersections.ts, 7, 43))
>I : Symbol(I, Decl(mappedArrayTupleIntersections.ts, 14, 19))
>T : Symbol(T, Decl(mappedArrayTupleIntersections.ts, 13, 9))

never;

type X = Hmm<[3, 4, 5]>;
>X : Symbol(X, Decl(mappedArrayTupleIntersections.ts, 15, 10))
>Hmm : Symbol(Hmm, Decl(mappedArrayTupleIntersections.ts, 11, 38))

40 changes: 40 additions & 0 deletions tests/baselines/reference/mappedArrayTupleIntersections.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//// [tests/cases/compiler/mappedArrayTupleIntersections.ts] ////

=== mappedArrayTupleIntersections.ts ===
type Box<T> = { value: T };
>Box : Box<T>
>value : T

type Boxify<T> = { [K in keyof T]: Box<T[K]> };
>Boxify : Boxify<T>

type T1 = Boxify<string[]>;
>T1 : Box<string>[]

type T2 = Boxify<[string, string]>;
>T2 : [Box<string>, Box<string>]

type T3 = Boxify<string[] & unknown[]>;
>T3 : Box<string>[] & Box<unknown>[]

type T4 = Boxify<string[] & [string, string]>;
>T4 : Box<string>[] & [Box<string>, Box<string>]

type T5 = Boxify<string[] & { x: string }>;
>T5 : Boxify<string[] & { x: string; }>
>x : string

// https://github.com/microsoft/TypeScript/issues/57744

type MustBeArray<T extends any[]> = T;
>MustBeArray : T

type Hmm<T extends any[]> = T extends number[] ?
>Hmm : Hmm<T>

MustBeArray<{ [I in keyof T]: 1 }> :
never;

type X = Hmm<[3, 4, 5]>;
>X : [1, 1, 1]

21 changes: 21 additions & 0 deletions tests/cases/compiler/mappedArrayTupleIntersections.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// @strict: true
// @noEmit: true

type Box<T> = { value: T };
type Boxify<T> = { [K in keyof T]: Box<T[K]> };

type T1 = Boxify<string[]>;
type T2 = Boxify<[string, string]>;
type T3 = Boxify<string[] & unknown[]>;
type T4 = Boxify<string[] & [string, string]>;
type T5 = Boxify<string[] & { x: string }>;

// https://github.com/microsoft/TypeScript/issues/57744

type MustBeArray<T extends any[]> = T;

type Hmm<T extends any[]> = T extends number[] ?
MustBeArray<{ [I in keyof T]: 1 }> :
never;

type X = Hmm<[3, 4, 5]>;