Declaration emit for inlined mapped types preserves modifier-preserving behavior (#55054)

This commit is contained in:
Wesley Wigham 2023-07-24 11:57:36 -07:00 committed by GitHub
parent 34c144d493
commit 970733653c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 349 additions and 0 deletions

View File

@ -6641,6 +6641,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const questionToken = type.declaration.questionToken ? factory.createToken(type.declaration.questionToken.kind) as QuestionToken | PlusToken | MinusToken : undefined;
let appropriateConstraintTypeNode: TypeNode;
let newTypeVariable: TypeReferenceNode | undefined;
// If the mapped type isn't `keyof` constraint-declared, _but_ still has modifiers preserved, and its naive instantiation won't preserve modifiers because its constraint isn't `keyof` constrained, we have work to do
const needsModifierPreservingWrapper = !isMappedTypeWithKeyofConstraintDeclaration(type)
&& !(getModifiersTypeFromMappedType(type).flags & TypeFlags.Unknown)
&& context.flags & NodeBuilderFlags.GenerateNamesForShadowedTypeParams
&& !(getConstraintTypeFromMappedType(type).flags & TypeFlags.TypeParameter && getConstraintOfTypeParameter(getConstraintTypeFromMappedType(type))?.flags! & TypeFlags.Index);
if (isMappedTypeWithKeyofConstraintDeclaration(type)) {
// We have a { [P in keyof T]: X }
// We do this to ensure we retain the toplevel keyof-ness of the type which may be lost due to keyof distribution during `getConstraintTypeFromMappedType`
@ -6651,6 +6656,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
appropriateConstraintTypeNode = factory.createTypeOperatorNode(SyntaxKind.KeyOfKeyword, newTypeVariable || typeToTypeNodeHelper(getModifiersTypeFromMappedType(type), context));
}
else if (needsModifierPreservingWrapper) {
// So, step 1: new type variable
const newParam = createTypeParameter(createSymbol(SymbolFlags.TypeParameter, "T" as __String));
const name = typeParameterToName(newParam, context);
newTypeVariable = factory.createTypeReferenceNode(name);
// step 2: make that new type variable itself the constraint node, making the mapped type `{[K in T_1]: Template}`
appropriateConstraintTypeNode = newTypeVariable;
}
else {
appropriateConstraintTypeNode = typeToTypeNodeHelper(getConstraintTypeFromMappedType(type), context);
}
@ -6672,6 +6685,18 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
factory.createKeywordTypeNode(SyntaxKind.NeverKeyword)
);
}
else if (needsModifierPreservingWrapper) {
// and step 3: once the mapped type is reconstructed, create a `ConstraintType extends infer T_1 extends keyof ModifiersType ? {[K in T_1]: Template} : never`
// subtly different from the `keyof` constraint case, by including the `keyof` constraint on the `infer` type parameter, it doesn't rely on the constraint type being itself
// constrained to a `keyof` type to preserve its modifier-preserving behavior. This is all basically because we preserve modifiers for a wider set of mapped types than
// just homomorphic ones.
return factory.createConditionalTypeNode(
typeToTypeNodeHelper(getConstraintTypeFromMappedType(type), context),
factory.createInferTypeNode(factory.createTypeParameterDeclaration(/*modifiers*/ undefined, factory.cloneNode(newTypeVariable!.typeName) as Identifier, factory.createTypeOperatorNode(SyntaxKind.KeyOfKeyword, typeToTypeNodeHelper(getModifiersTypeFromMappedType(type), context)))),
result,
factory.createKeywordTypeNode(SyntaxKind.NeverKeyword)
);
}
return result;
}

View File

@ -0,0 +1,86 @@
//// [tests/cases/compiler/inlineMappedTypeModifierDeclarationEmit.ts] ////
//// [index.ts]
import { test1, test2 } from "./other";
export function wrappedTest1<T, K extends string>(obj: T, k: K) {
return test1(obj, k);
}
export function wrappedTest2<T, K extends string>(obj: T, k: K) {
return test2(obj, k);
}
export type Obj = {
a: number;
readonly foo: string;
};
export const processedInternally1 = wrappedTest1({} as Obj, "a");
export const processedInternally2 = wrappedTest2({} as Obj, "a");
//// [other.ts]
// how Omit from lib is defined
type OmitReal<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
// what we see when we hover it
type OmitUnveiled<T, K extends string | number | symbol> = {
[P in Exclude<keyof T, K>]: T[P];
};
export function test1<T, K extends string>(obj: T, k: K): OmitReal<T, K> {
return {} as any;
}
export function test2<T, K extends string>(obj: T, k: K): OmitUnveiled<T, K> {
return {} as any;
}
//// [other.js]
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.test2 = exports.test1 = void 0;
function test1(obj, k) {
return {};
}
exports.test1 = test1;
function test2(obj, k) {
return {};
}
exports.test2 = test2;
//// [index.js]
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.processedInternally2 = exports.processedInternally1 = exports.wrappedTest2 = exports.wrappedTest1 = void 0;
var other_1 = require("./other");
function wrappedTest1(obj, k) {
return (0, other_1.test1)(obj, k);
}
exports.wrappedTest1 = wrappedTest1;
function wrappedTest2(obj, k) {
return (0, other_1.test2)(obj, k);
}
exports.wrappedTest2 = wrappedTest2;
exports.processedInternally1 = wrappedTest1({}, "a");
exports.processedInternally2 = wrappedTest2({}, "a");
//// [other.d.ts]
type OmitReal<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
type OmitUnveiled<T, K extends string | number | symbol> = {
[P in Exclude<keyof T, K>]: T[P];
};
export declare function test1<T, K extends string>(obj: T, k: K): OmitReal<T, K>;
export declare function test2<T, K extends string>(obj: T, k: K): OmitUnveiled<T, K>;
export {};
//// [index.d.ts]
export declare function wrappedTest1<T, K extends string>(obj: T, k: K): Exclude<keyof T, K> extends infer T_1 extends keyof T ? { [P in T_1]: T[P]; } : never;
export declare function wrappedTest2<T, K extends string>(obj: T, k: K): { [P in Exclude<keyof T, K>]: T[P]; };
export type Obj = {
a: number;
readonly foo: string;
};
export declare const processedInternally1: {
readonly foo: string;
};
export declare const processedInternally2: {
foo: string;
};

View File

@ -0,0 +1,115 @@
//// [tests/cases/compiler/inlineMappedTypeModifierDeclarationEmit.ts] ////
=== index.ts ===
import { test1, test2 } from "./other";
>test1 : Symbol(test1, Decl(index.ts, 0, 8))
>test2 : Symbol(test2, Decl(index.ts, 0, 15))
export function wrappedTest1<T, K extends string>(obj: T, k: K) {
>wrappedTest1 : Symbol(wrappedTest1, Decl(index.ts, 0, 39))
>T : Symbol(T, Decl(index.ts, 2, 29))
>K : Symbol(K, Decl(index.ts, 2, 31))
>obj : Symbol(obj, Decl(index.ts, 2, 50))
>T : Symbol(T, Decl(index.ts, 2, 29))
>k : Symbol(k, Decl(index.ts, 2, 57))
>K : Symbol(K, Decl(index.ts, 2, 31))
return test1(obj, k);
>test1 : Symbol(test1, Decl(index.ts, 0, 8))
>obj : Symbol(obj, Decl(index.ts, 2, 50))
>k : Symbol(k, Decl(index.ts, 2, 57))
}
export function wrappedTest2<T, K extends string>(obj: T, k: K) {
>wrappedTest2 : Symbol(wrappedTest2, Decl(index.ts, 4, 1))
>T : Symbol(T, Decl(index.ts, 6, 29))
>K : Symbol(K, Decl(index.ts, 6, 31))
>obj : Symbol(obj, Decl(index.ts, 6, 50))
>T : Symbol(T, Decl(index.ts, 6, 29))
>k : Symbol(k, Decl(index.ts, 6, 57))
>K : Symbol(K, Decl(index.ts, 6, 31))
return test2(obj, k);
>test2 : Symbol(test2, Decl(index.ts, 0, 15))
>obj : Symbol(obj, Decl(index.ts, 6, 50))
>k : Symbol(k, Decl(index.ts, 6, 57))
}
export type Obj = {
>Obj : Symbol(Obj, Decl(index.ts, 8, 1))
a: number;
>a : Symbol(a, Decl(index.ts, 10, 19))
readonly foo: string;
>foo : Symbol(foo, Decl(index.ts, 11, 12))
};
export const processedInternally1 = wrappedTest1({} as Obj, "a");
>processedInternally1 : Symbol(processedInternally1, Decl(index.ts, 15, 12))
>wrappedTest1 : Symbol(wrappedTest1, Decl(index.ts, 0, 39))
>Obj : Symbol(Obj, Decl(index.ts, 8, 1))
export const processedInternally2 = wrappedTest2({} as Obj, "a");
>processedInternally2 : Symbol(processedInternally2, Decl(index.ts, 16, 12))
>wrappedTest2 : Symbol(wrappedTest2, Decl(index.ts, 4, 1))
>Obj : Symbol(Obj, Decl(index.ts, 8, 1))
=== other.ts ===
// how Omit from lib is defined
type OmitReal<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
>OmitReal : Symbol(OmitReal, Decl(other.ts, 0, 0))
>T : Symbol(T, Decl(other.ts, 1, 14))
>K : Symbol(K, Decl(other.ts, 1, 16))
>Pick : Symbol(Pick, Decl(lib.es5.d.ts, --, --))
>T : Symbol(T, Decl(other.ts, 1, 14))
>Exclude : Symbol(Exclude, Decl(lib.es5.d.ts, --, --))
>T : Symbol(T, Decl(other.ts, 1, 14))
>K : Symbol(K, Decl(other.ts, 1, 16))
// what we see when we hover it
type OmitUnveiled<T, K extends string | number | symbol> = {
>OmitUnveiled : Symbol(OmitUnveiled, Decl(other.ts, 1, 69))
>T : Symbol(T, Decl(other.ts, 3, 18))
>K : Symbol(K, Decl(other.ts, 3, 20))
[P in Exclude<keyof T, K>]: T[P];
>P : Symbol(P, Decl(other.ts, 4, 3))
>Exclude : Symbol(Exclude, Decl(lib.es5.d.ts, --, --))
>T : Symbol(T, Decl(other.ts, 3, 18))
>K : Symbol(K, Decl(other.ts, 3, 20))
>T : Symbol(T, Decl(other.ts, 3, 18))
>P : Symbol(P, Decl(other.ts, 4, 3))
};
export function test1<T, K extends string>(obj: T, k: K): OmitReal<T, K> {
>test1 : Symbol(test1, Decl(other.ts, 5, 2))
>T : Symbol(T, Decl(other.ts, 7, 22))
>K : Symbol(K, Decl(other.ts, 7, 24))
>obj : Symbol(obj, Decl(other.ts, 7, 43))
>T : Symbol(T, Decl(other.ts, 7, 22))
>k : Symbol(k, Decl(other.ts, 7, 50))
>K : Symbol(K, Decl(other.ts, 7, 24))
>OmitReal : Symbol(OmitReal, Decl(other.ts, 0, 0))
>T : Symbol(T, Decl(other.ts, 7, 22))
>K : Symbol(K, Decl(other.ts, 7, 24))
return {} as any;
}
export function test2<T, K extends string>(obj: T, k: K): OmitUnveiled<T, K> {
>test2 : Symbol(test2, Decl(other.ts, 9, 1))
>T : Symbol(T, Decl(other.ts, 11, 22))
>K : Symbol(K, Decl(other.ts, 11, 24))
>obj : Symbol(obj, Decl(other.ts, 11, 43))
>T : Symbol(T, Decl(other.ts, 11, 22))
>k : Symbol(k, Decl(other.ts, 11, 50))
>K : Symbol(K, Decl(other.ts, 11, 24))
>OmitUnveiled : Symbol(OmitUnveiled, Decl(other.ts, 1, 69))
>T : Symbol(T, Decl(other.ts, 11, 22))
>K : Symbol(K, Decl(other.ts, 11, 24))
return {} as any;
}

View File

@ -0,0 +1,89 @@
//// [tests/cases/compiler/inlineMappedTypeModifierDeclarationEmit.ts] ////
=== index.ts ===
import { test1, test2 } from "./other";
>test1 : <T, K extends string>(obj: T, k: K) => { [P in Exclude<keyof T, K>]: T[P]; }
>test2 : <T, K extends string>(obj: T, k: K) => { [P in Exclude<keyof T, K>]: T[P]; }
export function wrappedTest1<T, K extends string>(obj: T, k: K) {
>wrappedTest1 : <T, K extends string>(obj: T, k: K) => { [P in Exclude<keyof T, K>]: T[P]; }
>obj : T
>k : K
return test1(obj, k);
>test1(obj, k) : { [P in Exclude<keyof T, K>]: T[P]; }
>test1 : <T, K extends string>(obj: T, k: K) => { [P in Exclude<keyof T, K>]: T[P]; }
>obj : T
>k : K
}
export function wrappedTest2<T, K extends string>(obj: T, k: K) {
>wrappedTest2 : <T, K extends string>(obj: T, k: K) => { [P in Exclude<keyof T, K>]: T[P]; }
>obj : T
>k : K
return test2(obj, k);
>test2(obj, k) : { [P in Exclude<keyof T, K>]: T[P]; }
>test2 : <T, K extends string>(obj: T, k: K) => { [P in Exclude<keyof T, K>]: T[P]; }
>obj : T
>k : K
}
export type Obj = {
>Obj : { a: number; readonly foo: string; }
a: number;
>a : number
readonly foo: string;
>foo : string
};
export const processedInternally1 = wrappedTest1({} as Obj, "a");
>processedInternally1 : { readonly foo: string; }
>wrappedTest1({} as Obj, "a") : { readonly foo: string; }
>wrappedTest1 : <T, K extends string>(obj: T, k: K) => { [P in Exclude<keyof T, K>]: T[P]; }
>{} as Obj : Obj
>{} : {}
>"a" : "a"
export const processedInternally2 = wrappedTest2({} as Obj, "a");
>processedInternally2 : { foo: string; }
>wrappedTest2({} as Obj, "a") : { foo: string; }
>wrappedTest2 : <T, K extends string>(obj: T, k: K) => { [P in Exclude<keyof T, K>]: T[P]; }
>{} as Obj : Obj
>{} : {}
>"a" : "a"
=== other.ts ===
// how Omit from lib is defined
type OmitReal<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
>OmitReal : OmitReal<T, K>
// what we see when we hover it
type OmitUnveiled<T, K extends string | number | symbol> = {
>OmitUnveiled : OmitUnveiled<T, K>
[P in Exclude<keyof T, K>]: T[P];
};
export function test1<T, K extends string>(obj: T, k: K): OmitReal<T, K> {
>test1 : <T, K extends string>(obj: T, k: K) => OmitReal<T, K>
>obj : T
>k : K
return {} as any;
>{} as any : any
>{} : {}
}
export function test2<T, K extends string>(obj: T, k: K): OmitUnveiled<T, K> {
>test2 : <T, K extends string>(obj: T, k: K) => OmitUnveiled<T, K>
>obj : T
>k : K
return {} as any;
>{} as any : any
>{} : {}
}

View File

@ -0,0 +1,34 @@
// @declaration: true
// @filename: index.ts
import { test1, test2 } from "./other";
export function wrappedTest1<T, K extends string>(obj: T, k: K) {
return test1(obj, k);
}
export function wrappedTest2<T, K extends string>(obj: T, k: K) {
return test2(obj, k);
}
export type Obj = {
a: number;
readonly foo: string;
};
export const processedInternally1 = wrappedTest1({} as Obj, "a");
export const processedInternally2 = wrappedTest2({} as Obj, "a");
// @filename: other.ts
// how Omit from lib is defined
type OmitReal<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
// what we see when we hover it
type OmitUnveiled<T, K extends string | number | symbol> = {
[P in Exclude<keyof T, K>]: T[P];
};
export function test1<T, K extends string>(obj: T, k: K): OmitReal<T, K> {
return {} as any;
}
export function test2<T, K extends string>(obj: T, k: K): OmitUnveiled<T, K> {
return {} as any;
}