Skip to content

Commit

Permalink
Defer index types on remapping mapped types (#55140)
Browse files Browse the repository at this point in the history
Co-authored-by: Nathan Shively-Sanders <[email protected]>
  • Loading branch information
Andarist and sandersn authored Nov 30, 2023
1 parent 3258d75 commit 2c4cbd9
Show file tree
Hide file tree
Showing 6 changed files with 478 additions and 247 deletions.
25 changes: 19 additions & 6 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1304,6 +1304,12 @@ const enum MappedTypeModifiers {
ExcludeOptional = 1 << 3,
}

const enum MappedTypeNameTypeKind {
None,
Filtering,
Remapping,
}

const enum ExpandingFlags {
None = 0,
Source = 1,
Expand Down Expand Up @@ -13741,7 +13747,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const constraintType = getConstraintTypeFromMappedType(type);
const mappedType = (type.target as MappedType) || type;
const nameType = getNameTypeFromMappedType(mappedType);
const shouldLinkPropDeclarations = !nameType || isFilteringMappedType(mappedType);
const shouldLinkPropDeclarations = getMappedTypeNameTypeKind(mappedType) !== MappedTypeNameTypeKind.Remapping;
const templateType = getTemplateTypeFromMappedType(mappedType);
const modifiersType = getApparentType(getModifiersTypeFromMappedType(type)); // The 'T' in 'keyof T'
const templateModifiers = getMappedTypeModifiers(type);
Expand Down Expand Up @@ -13923,9 +13929,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return false;
}

function isFilteringMappedType(type: MappedType): boolean {
function getMappedTypeNameTypeKind(type: MappedType): MappedTypeNameTypeKind {
const nameType = getNameTypeFromMappedType(type);
return !!nameType && isTypeAssignableTo(nameType, getTypeParameterFromMappedType(type));
if (!nameType) {
return MappedTypeNameTypeKind.None;
}
return isTypeAssignableTo(nameType, getTypeParameterFromMappedType(type)) ? MappedTypeNameTypeKind.Filtering : MappedTypeNameTypeKind.Remapping;
}

function resolveStructuredTypeMembers(type: StructuredType): ResolvedType {
Expand Down Expand Up @@ -17700,7 +17709,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
function shouldDeferIndexType(type: Type, indexFlags = IndexFlags.None) {
return !!(type.flags & TypeFlags.InstantiableNonPrimitive ||
isGenericTupleType(type) ||
isGenericMappedType(type) && !hasDistributiveNameType(type) ||
isGenericMappedType(type) && (!hasDistributiveNameType(type) || getMappedTypeNameTypeKind(type) === MappedTypeNameTypeKind.Remapping) ||
type.flags & TypeFlags.Union && !(indexFlags & IndexFlags.NoReducibleCheck) && isGenericReducibleType(type) ||
type.flags & TypeFlags.Intersection && maybeTypeOfKind(type, TypeFlags.Instantiable) && some((type as IntersectionType).types, isEmptyAnonymousObjectType));
}
Expand Down Expand Up @@ -18282,7 +18291,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// K is generic and N is assignable to P, instantiate E using a mapper that substitutes the index type for P.
// For example, for an index access { [P in K]: Box<T[P]> }[X], we construct the type Box<T[X]>.
if (isGenericMappedType(objectType)) {
if (!getNameTypeFromMappedType(objectType) || isFilteringMappedType(objectType)) {
if (getMappedTypeNameTypeKind(objectType) !== MappedTypeNameTypeKind.Remapping) {
return type[cache] = mapType(substituteIndexedMappedType(objectType, type.indexType), t => getSimplifiedType(t, writing));
}
}
Expand Down Expand Up @@ -40108,7 +40117,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// Check if the index type is assignable to 'keyof T' for the object type.
const objectType = (type as IndexedAccessType).objectType;
const indexType = (type as IndexedAccessType).indexType;
if (isTypeAssignableTo(indexType, getIndexType(objectType, IndexFlags.None))) {
// skip index type deferral on remapping mapped types
const objectIndexType = isGenericMappedType(objectType) && getMappedTypeNameTypeKind(objectType) === MappedTypeNameTypeKind.Remapping
? getIndexTypeForMappedType(objectType, IndexFlags.None)
: getIndexType(objectType, IndexFlags.None);
if (isTypeAssignableTo(indexType, objectIndexType)) {
if (
accessNode.kind === SyntaxKind.ElementAccessExpression && isAssignmentTarget(accessNode) &&
getObjectFlags(objectType) & ObjectFlags.Mapped && getMappedTypeModifiers(objectType as MappedType) & MappedTypeModifiers.IncludeReadonly
Expand Down
28 changes: 26 additions & 2 deletions tests/baselines/reference/mappedTypeAsClauses.errors.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
mappedTypeAsClauses.ts(130,3): error TS2345: Argument of type '"a"' is not assignable to parameter of type '"b"'.
mappedTypeAsClauses.ts(131,3): error TS2345: Argument of type '"a"' is not assignable to parameter of type '"b"'.


==== mappedTypeAsClauses.ts (1 errors) ====
Expand Down Expand Up @@ -30,7 +30,8 @@ mappedTypeAsClauses.ts(130,3): error TS2345: Argument of type '"a"' is not assig
type DoubleProp<T> = { [P in keyof T & string as `${P}1` | `${P}2`]: T[P] }
type TD1 = DoubleProp<{ a: string, b: number }>; // { a1: string, a2: string, b1: number, b2: number }
type TD2 = keyof TD1; // 'a1' | 'a2' | 'b1' | 'b2'
type TD3<U> = keyof DoubleProp<U>; // `${keyof U & string}1` | `${keyof U & string}2`
type TD3<U> = keyof DoubleProp<U>; // keyof DoubleProp<U>
type TD4 = TD3<{ a: string, b: number }>; // 'a1' | 'a2' | 'b1' | 'b2'

// Repro from #40619

Expand Down Expand Up @@ -155,4 +156,27 @@ mappedTypeAsClauses.ts(130,3): error TS2345: Argument of type '"a"' is not assig
type TN3<T> = keyof { [P in keyof T as Exclude<Exclude<Exclude<P, 'c'>, 'b'>, 'a'>]: string };
type TN4<T, U> = keyof { [K in keyof T as (K extends U ? T[K] : never) extends T[K] ? K : never]: string };
type TN5<T, U> = keyof { [K in keyof T as keyof { [P in K as T[P] extends U ? K : never]: true }]: string };

// repro from https://github.com/microsoft/TypeScript/issues/55129
type Fruit =
| {
name: "apple";
color: "red";
}
| {
name: "banana";
color: "yellow";
}
| {
name: "orange";
color: "orange";
};
type Result1<T extends {name: string | number; color: string | number }> = {
[Key in T as `${Key['name']}:${Key['color']}`]: unknown
};
type Result2<T extends {name: string | number; color: string | number }> = keyof {
[Key in T as `${Key['name']}:${Key['color']}`]: unknown
}
type Test1 = keyof Result1<Fruit> // "apple:red" | "banana:yellow" | "orange:orange"
type Test2 = Result2<Fruit> // "apple:red" | "banana:yellow" | "orange:orange"

54 changes: 53 additions & 1 deletion tests/baselines/reference/mappedTypeAsClauses.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ type TM1 = Methods<{ foo(): number, bar(x: string): boolean, baz: string | numbe
type DoubleProp<T> = { [P in keyof T & string as `${P}1` | `${P}2`]: T[P] }
type TD1 = DoubleProp<{ a: string, b: number }>; // { a1: string, a2: string, b1: number, b2: number }
type TD2 = keyof TD1; // 'a1' | 'a2' | 'b1' | 'b2'
type TD3<U> = keyof DoubleProp<U>; // `${keyof U & string}1` | `${keyof U & string}2`
type TD3<U> = keyof DoubleProp<U>; // keyof DoubleProp<U>
type TD4 = TD3<{ a: string, b: number }>; // 'a1' | 'a2' | 'b1' | 'b2'

// Repro from #40619

Expand Down Expand Up @@ -152,6 +153,29 @@ type TN2<T> = keyof { [P in keyof T as 'a' extends P ? 'x' : 'y']: string };
type TN3<T> = keyof { [P in keyof T as Exclude<Exclude<Exclude<P, 'c'>, 'b'>, 'a'>]: string };
type TN4<T, U> = keyof { [K in keyof T as (K extends U ? T[K] : never) extends T[K] ? K : never]: string };
type TN5<T, U> = keyof { [K in keyof T as keyof { [P in K as T[P] extends U ? K : never]: true }]: string };

// repro from https://github.com/microsoft/TypeScript/issues/55129
type Fruit =
| {
name: "apple";
color: "red";
}
| {
name: "banana";
color: "yellow";
}
| {
name: "orange";
color: "orange";
};
type Result1<T extends {name: string | number; color: string | number }> = {
[Key in T as `${Key['name']}:${Key['color']}`]: unknown
};
type Result2<T extends {name: string | number; color: string | number }> = keyof {
[Key in T as `${Key['name']}:${Key['color']}`]: unknown
}
type Test1 = keyof Result1<Fruit> // "apple:red" | "banana:yellow" | "orange:orange"
type Test2 = Result2<Fruit> // "apple:red" | "banana:yellow" | "orange:orange"


//// [mappedTypeAsClauses.js]
Expand Down Expand Up @@ -217,6 +241,10 @@ type TD1 = DoubleProp<{
}>;
type TD2 = keyof TD1;
type TD3<U> = keyof DoubleProp<U>;
type TD4 = TD3<{
a: string;
b: number;
}>;
type Lazyify<T> = {
[K in keyof T as `get${Capitalize<K & string>}`]: () => T[K];
};
Expand Down Expand Up @@ -337,3 +365,27 @@ type TN5<T, U> = keyof {
[P in K as T[P] extends U ? K : never]: true;
}]: string;
};
type Fruit = {
name: "apple";
color: "red";
} | {
name: "banana";
color: "yellow";
} | {
name: "orange";
color: "orange";
};
type Result1<T extends {
name: string | number;
color: string | number;
}> = {
[Key in T as `${Key['name']}:${Key['color']}`]: unknown;
};
type Result2<T extends {
name: string | number;
color: string | number;
}> = keyof {
[Key in T as `${Key['name']}:${Key['color']}`]: unknown;
};
type Test1 = keyof Result1<Fruit>;
type Test2 = Result2<Fruit>;
Loading

0 comments on commit 2c4cbd9

Please sign in to comment.