Skip to content

Commit 0f86803

Browse files
authored
Constrain infer type parameters made to preserver distributivity for inlined homomorphic mapped types (#49793)
1 parent e2e3c12 commit 0f86803

5 files changed

+170
-3
lines changed

src/compiler/checker.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5217,6 +5217,11 @@ m2: ${(this.mapper2 as unknown as DebugTypeMapper).__debugToString().split("\n")
52175217
return typeToTypeNodeHelper(type, context);
52185218
}
52195219

5220+
function isHomomorphicMappedTypeWithNonHomomorphicInstantiation(type: MappedType) {
5221+
return isMappedTypeWithKeyofConstraintDeclaration(type)
5222+
&& !(getModifiersTypeFromMappedType(type).flags & TypeFlags.TypeParameter);
5223+
}
5224+
52205225
function createMappedTypeNodeFromType(type: MappedType) {
52215226
Debug.assert(!!(type.flags & TypeFlags.Object));
52225227
const readonlyToken = type.declaration.readonlyToken ? factory.createToken(type.declaration.readonlyToken.kind) as ReadonlyKeyword | PlusToken | MinusToken : undefined;
@@ -5226,7 +5231,7 @@ m2: ${(this.mapper2 as unknown as DebugTypeMapper).__debugToString().split("\n")
52265231
if (isMappedTypeWithKeyofConstraintDeclaration(type)) {
52275232
// We have a { [P in keyof T]: X }
52285233
// We do this to ensure we retain the toplevel keyof-ness of the type which may be lost due to keyof distribution during `getConstraintTypeFromMappedType`
5229-
if (!(getModifiersTypeFromMappedType(type).flags & TypeFlags.TypeParameter) && context.flags & NodeBuilderFlags.GenerateNamesForShadowedTypeParams) {
5234+
if (isHomomorphicMappedTypeWithNonHomomorphicInstantiation(type) && context.flags & NodeBuilderFlags.GenerateNamesForShadowedTypeParams) {
52305235
const newParam = createTypeParameter(createSymbol(SymbolFlags.TypeParameter, "T" as __String));
52315236
const name = typeParameterToName(newParam, context);
52325237
newTypeVariable = factory.createTypeReferenceNode(name);
@@ -5242,13 +5247,14 @@ m2: ${(this.mapper2 as unknown as DebugTypeMapper).__debugToString().split("\n")
52425247
const mappedTypeNode = factory.createMappedTypeNode(readonlyToken, typeParameterNode, nameTypeNode, questionToken, templateTypeNode, /*members*/ undefined);
52435248
context.approximateLength += 10;
52445249
const result = setEmitFlags(mappedTypeNode, EmitFlags.SingleLine);
5245-
if (isMappedTypeWithKeyofConstraintDeclaration(type) && !(getModifiersTypeFromMappedType(type).flags & TypeFlags.TypeParameter) && context.flags & NodeBuilderFlags.GenerateNamesForShadowedTypeParams) {
5250+
if (isHomomorphicMappedTypeWithNonHomomorphicInstantiation(type) && context.flags & NodeBuilderFlags.GenerateNamesForShadowedTypeParams) {
52465251
// homomorphic mapped type with a non-homomorphic naive inlining
52475252
// wrap it with a conditional like `SomeModifiersType extends infer U ? {..the mapped type...} : never` to ensure the resulting
52485253
// type stays homomorphic
5254+
const originalConstraint = instantiateType(getConstraintOfTypeParameter(getTypeFromTypeNode((type.declaration.typeParameter.constraint! as TypeOperatorNode).type) as TypeParameter) || unknownType, type.mapper);
52495255
return factory.createConditionalTypeNode(
52505256
typeToTypeNodeHelper(getModifiersTypeFromMappedType(type), context),
5251-
factory.createInferTypeNode(factory.createTypeParameterDeclaration(/*modifiers*/ undefined, factory.cloneNode(newTypeVariable!.typeName) as Identifier)),
5257+
factory.createInferTypeNode(factory.createTypeParameterDeclaration(/*modifiers*/ undefined, factory.cloneNode(newTypeVariable!.typeName) as Identifier, originalConstraint.flags & TypeFlags.Unknown ? undefined : typeToTypeNodeHelper(originalConstraint, context))),
52525258
result,
52535259
factory.createKeywordTypeNode(SyntaxKind.NeverKeyword)
52545260
);
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//// [tests/cases/compiler/declarationEmitMappedTypeDistributivityPreservesConstraints.ts] ////
2+
3+
//// [types.ts]
4+
type Fns = Record<string, (...params: unknown[]) => unknown>
5+
6+
type Map<T extends Fns> = { [K in keyof T]: T[K]; };
7+
8+
type AllArg<T extends Fns> = { [K in keyof T]: Parameters<T[K]> };
9+
10+
function fn<T extends { x: Map<T['x']> }>(sliceIndex: T): AllArg<T['x']> {
11+
return null!;
12+
}
13+
14+
export default { fn };
15+
16+
//// [reexport.ts]
17+
import test from "./types";
18+
export default { test };
19+
20+
//// [types.js]
21+
"use strict";
22+
exports.__esModule = true;
23+
function fn(sliceIndex) {
24+
return null;
25+
}
26+
exports["default"] = { fn: fn };
27+
//// [reexport.js]
28+
"use strict";
29+
exports.__esModule = true;
30+
var types_1 = require("./types");
31+
exports["default"] = { test: types_1["default"] };
32+
33+
34+
//// [types.d.ts]
35+
declare type Fns = Record<string, (...params: unknown[]) => unknown>;
36+
declare type Map<T extends Fns> = {
37+
[K in keyof T]: T[K];
38+
};
39+
declare type AllArg<T extends Fns> = {
40+
[K in keyof T]: Parameters<T[K]>;
41+
};
42+
declare function fn<T extends {
43+
x: Map<T['x']>;
44+
}>(sliceIndex: T): AllArg<T['x']>;
45+
declare const _default: {
46+
fn: typeof fn;
47+
};
48+
export default _default;
49+
//// [reexport.d.ts]
50+
declare const _default: {
51+
test: {
52+
fn: <T_1 extends {
53+
x: T_1["x"] extends infer T extends {
54+
[x: string]: (...params: unknown[]) => unknown;
55+
} ? { [K in keyof T]: T_1["x"][K]; } : never;
56+
}>(sliceIndex: T_1) => T_1["x"] extends infer T_2 extends {
57+
[x: string]: (...params: unknown[]) => unknown;
58+
} ? { [K_1 in keyof T_2]: Parameters<T_1["x"][K_1]>; } : never;
59+
};
60+
};
61+
export default _default;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
=== tests/cases/compiler/types.ts ===
2+
type Fns = Record<string, (...params: unknown[]) => unknown>
3+
>Fns : Symbol(Fns, Decl(types.ts, 0, 0))
4+
>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --))
5+
>params : Symbol(params, Decl(types.ts, 0, 27))
6+
7+
type Map<T extends Fns> = { [K in keyof T]: T[K]; };
8+
>Map : Symbol(Map, Decl(types.ts, 0, 60))
9+
>T : Symbol(T, Decl(types.ts, 2, 9))
10+
>Fns : Symbol(Fns, Decl(types.ts, 0, 0))
11+
>K : Symbol(K, Decl(types.ts, 2, 29))
12+
>T : Symbol(T, Decl(types.ts, 2, 9))
13+
>T : Symbol(T, Decl(types.ts, 2, 9))
14+
>K : Symbol(K, Decl(types.ts, 2, 29))
15+
16+
type AllArg<T extends Fns> = { [K in keyof T]: Parameters<T[K]> };
17+
>AllArg : Symbol(AllArg, Decl(types.ts, 2, 52))
18+
>T : Symbol(T, Decl(types.ts, 4, 12))
19+
>Fns : Symbol(Fns, Decl(types.ts, 0, 0))
20+
>K : Symbol(K, Decl(types.ts, 4, 32))
21+
>T : Symbol(T, Decl(types.ts, 4, 12))
22+
>Parameters : Symbol(Parameters, Decl(lib.es5.d.ts, --, --))
23+
>T : Symbol(T, Decl(types.ts, 4, 12))
24+
>K : Symbol(K, Decl(types.ts, 4, 32))
25+
26+
function fn<T extends { x: Map<T['x']> }>(sliceIndex: T): AllArg<T['x']> {
27+
>fn : Symbol(fn, Decl(types.ts, 4, 66))
28+
>T : Symbol(T, Decl(types.ts, 6, 12))
29+
>x : Symbol(x, Decl(types.ts, 6, 23))
30+
>Map : Symbol(Map, Decl(types.ts, 0, 60))
31+
>T : Symbol(T, Decl(types.ts, 6, 12))
32+
>sliceIndex : Symbol(sliceIndex, Decl(types.ts, 6, 42))
33+
>T : Symbol(T, Decl(types.ts, 6, 12))
34+
>AllArg : Symbol(AllArg, Decl(types.ts, 2, 52))
35+
>T : Symbol(T, Decl(types.ts, 6, 12))
36+
37+
return null!;
38+
}
39+
40+
export default { fn };
41+
>fn : Symbol(fn, Decl(types.ts, 10, 16))
42+
43+
=== tests/cases/compiler/reexport.ts ===
44+
import test from "./types";
45+
>test : Symbol(test, Decl(reexport.ts, 0, 6))
46+
47+
export default { test };
48+
>test : Symbol(test, Decl(reexport.ts, 1, 16))
49+
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
=== tests/cases/compiler/types.ts ===
2+
type Fns = Record<string, (...params: unknown[]) => unknown>
3+
>Fns : { [x: string]: (...params: unknown[]) => unknown; }
4+
>params : unknown[]
5+
6+
type Map<T extends Fns> = { [K in keyof T]: T[K]; };
7+
>Map : Map<T>
8+
9+
type AllArg<T extends Fns> = { [K in keyof T]: Parameters<T[K]> };
10+
>AllArg : AllArg<T>
11+
12+
function fn<T extends { x: Map<T['x']> }>(sliceIndex: T): AllArg<T['x']> {
13+
>fn : <T extends { x: Map<T['x']>; }>(sliceIndex: T) => AllArg<T['x']>
14+
>x : Map<T["x"]>
15+
>sliceIndex : T
16+
17+
return null!;
18+
>null! : null
19+
>null : null
20+
}
21+
22+
export default { fn };
23+
>{ fn } : { fn: <T extends { x: Map<T["x"]>; }>(sliceIndex: T) => AllArg<T["x"]>; }
24+
>fn : <T extends { x: Map<T["x"]>; }>(sliceIndex: T) => AllArg<T["x"]>
25+
26+
=== tests/cases/compiler/reexport.ts ===
27+
import test from "./types";
28+
>test : { fn: <T extends { x: { [K in keyof T["x"]]: T["x"][K]; }; }>(sliceIndex: T) => { [K in keyof T["x"]]: Parameters<T["x"][K]>; }; }
29+
30+
export default { test };
31+
>{ test } : { test: { fn: <T extends { x: { [K in keyof T["x"]]: T["x"][K]; }; }>(sliceIndex: T) => { [K in keyof T["x"]]: Parameters<T["x"][K]>; }; }; }
32+
>test : { fn: <T extends { x: { [K in keyof T["x"]]: T["x"][K]; }; }>(sliceIndex: T) => { [K in keyof T["x"]]: Parameters<T["x"][K]>; }; }
33+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// @declaration: true
2+
// @filename: types.ts
3+
type Fns = Record<string, (...params: unknown[]) => unknown>
4+
5+
type Map<T extends Fns> = { [K in keyof T]: T[K]; };
6+
7+
type AllArg<T extends Fns> = { [K in keyof T]: Parameters<T[K]> };
8+
9+
function fn<T extends { x: Map<T['x']> }>(sliceIndex: T): AllArg<T['x']> {
10+
return null!;
11+
}
12+
13+
export default { fn };
14+
15+
// @filename: reexport.ts
16+
17+
import test from "./types";
18+
export default { test };

0 commit comments

Comments
 (0)