From ce345d00a2a1369c18ff8689d5a41f8f36ace7db Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Mon, 17 Jul 2023 12:01:37 -0700 Subject: [PATCH 1/2] Declaration emit for inlined mapped types preserves modifier-preserving behavior --- src/compiler/checker.ts | 23 ++++ ...inlineMappedTypeModifierDeclarationEmit.js | 86 +++++++++++++ ...eMappedTypeModifierDeclarationEmit.symbols | 115 ++++++++++++++++++ ...ineMappedTypeModifierDeclarationEmit.types | 89 ++++++++++++++ ...inlineMappedTypeModifierDeclarationEmit.ts | 34 ++++++ 5 files changed, 347 insertions(+) create mode 100644 tests/baselines/reference/inlineMappedTypeModifierDeclarationEmit.js create mode 100644 tests/baselines/reference/inlineMappedTypeModifierDeclarationEmit.symbols create mode 100644 tests/baselines/reference/inlineMappedTypeModifierDeclarationEmit.types create mode 100644 tests/cases/compiler/inlineMappedTypeModifierDeclarationEmit.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index f1324425367bb..548cb08a3ab6f 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -6639,6 +6639,9 @@ 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` @@ -6649,6 +6652,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); } @@ -6670,6 +6681,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 it's 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; } diff --git a/tests/baselines/reference/inlineMappedTypeModifierDeclarationEmit.js b/tests/baselines/reference/inlineMappedTypeModifierDeclarationEmit.js new file mode 100644 index 0000000000000..507e148b2bb70 --- /dev/null +++ b/tests/baselines/reference/inlineMappedTypeModifierDeclarationEmit.js @@ -0,0 +1,86 @@ +//// [tests/cases/compiler/inlineMappedTypeModifierDeclarationEmit.ts] //// + +//// [index.ts] +import { test1, test2 } from "./other"; + +export function wrappedTest1(obj: T, k: K) { + return test1(obj, k); +} + +export function wrappedTest2(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 = Pick>; +// what we see when we hover it +type OmitUnveiled = { + [P in Exclude]: T[P]; +}; + +export function test1(obj: T, k: K): OmitReal { + return {} as any; +} + +export function test2(obj: T, k: K): OmitUnveiled { + 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 = Pick>; +type OmitUnveiled = { + [P in Exclude]: T[P]; +}; +export declare function test1(obj: T, k: K): OmitReal; +export declare function test2(obj: T, k: K): OmitUnveiled; +export {}; +//// [index.d.ts] +export declare function wrappedTest1(obj: T, k: K): Exclude extends infer T_1 extends keyof T ? { [P in T_1]: T[P]; } : never; +export declare function wrappedTest2(obj: T, k: K): { [P in Exclude]: T[P]; }; +export type Obj = { + a: number; + readonly foo: string; +}; +export declare const processedInternally1: { + readonly foo: string; +}; +export declare const processedInternally2: { + foo: string; +}; diff --git a/tests/baselines/reference/inlineMappedTypeModifierDeclarationEmit.symbols b/tests/baselines/reference/inlineMappedTypeModifierDeclarationEmit.symbols new file mode 100644 index 0000000000000..2bc21303c0c8d --- /dev/null +++ b/tests/baselines/reference/inlineMappedTypeModifierDeclarationEmit.symbols @@ -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(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(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 = Pick>; +>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 = { +>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]: 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(obj: T, k: K): OmitReal { +>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(obj: T, k: K): OmitUnveiled { +>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; +} diff --git a/tests/baselines/reference/inlineMappedTypeModifierDeclarationEmit.types b/tests/baselines/reference/inlineMappedTypeModifierDeclarationEmit.types new file mode 100644 index 0000000000000..3119c709d5d0a --- /dev/null +++ b/tests/baselines/reference/inlineMappedTypeModifierDeclarationEmit.types @@ -0,0 +1,89 @@ +//// [tests/cases/compiler/inlineMappedTypeModifierDeclarationEmit.ts] //// + +=== index.ts === +import { test1, test2 } from "./other"; +>test1 : (obj: T, k: K) => { [P in Exclude]: T[P]; } +>test2 : (obj: T, k: K) => { [P in Exclude]: T[P]; } + +export function wrappedTest1(obj: T, k: K) { +>wrappedTest1 : (obj: T, k: K) => { [P in Exclude]: T[P]; } +>obj : T +>k : K + + return test1(obj, k); +>test1(obj, k) : { [P in Exclude]: T[P]; } +>test1 : (obj: T, k: K) => { [P in Exclude]: T[P]; } +>obj : T +>k : K +} + +export function wrappedTest2(obj: T, k: K) { +>wrappedTest2 : (obj: T, k: K) => { [P in Exclude]: T[P]; } +>obj : T +>k : K + + return test2(obj, k); +>test2(obj, k) : { [P in Exclude]: T[P]; } +>test2 : (obj: T, k: K) => { [P in Exclude]: 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 : (obj: T, k: K) => { [P in Exclude]: T[P]; } +>{} as Obj : Obj +>{} : {} +>"a" : "a" + +export const processedInternally2 = wrappedTest2({} as Obj, "a"); +>processedInternally2 : { foo: string; } +>wrappedTest2({} as Obj, "a") : { foo: string; } +>wrappedTest2 : (obj: T, k: K) => { [P in Exclude]: T[P]; } +>{} as Obj : Obj +>{} : {} +>"a" : "a" + +=== other.ts === +// how Omit from lib is defined +type OmitReal = Pick>; +>OmitReal : OmitReal + +// what we see when we hover it +type OmitUnveiled = { +>OmitUnveiled : OmitUnveiled + + [P in Exclude]: T[P]; +}; + +export function test1(obj: T, k: K): OmitReal { +>test1 : (obj: T, k: K) => OmitReal +>obj : T +>k : K + + return {} as any; +>{} as any : any +>{} : {} +} + +export function test2(obj: T, k: K): OmitUnveiled { +>test2 : (obj: T, k: K) => OmitUnveiled +>obj : T +>k : K + + return {} as any; +>{} as any : any +>{} : {} +} diff --git a/tests/cases/compiler/inlineMappedTypeModifierDeclarationEmit.ts b/tests/cases/compiler/inlineMappedTypeModifierDeclarationEmit.ts new file mode 100644 index 0000000000000..275345f28d6f8 --- /dev/null +++ b/tests/cases/compiler/inlineMappedTypeModifierDeclarationEmit.ts @@ -0,0 +1,34 @@ +// @declaration: true +// @filename: index.ts +import { test1, test2 } from "./other"; + +export function wrappedTest1(obj: T, k: K) { + return test1(obj, k); +} + +export function wrappedTest2(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 = Pick>; +// what we see when we hover it +type OmitUnveiled = { + [P in Exclude]: T[P]; +}; + +export function test1(obj: T, k: K): OmitReal { + return {} as any; +} + +export function test2(obj: T, k: K): OmitUnveiled { + return {} as any; +} \ No newline at end of file From 73e156838ab6f6733bceab98b59c4151c6b163e5 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Mon, 24 Jul 2023 11:37:43 -0700 Subject: [PATCH 2/2] Fix style nits --- src/compiler/checker.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 93a502158413d..b206773b6df7a 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -6641,8 +6641,10 @@ 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 + // 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 } @@ -6686,7 +6688,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { 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 it's modifier-preserving behavior. This is all basically because we preserve modifiers for a wider set of mapped types than + // 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),