-
Notifications
You must be signed in to change notification settings - Fork 12.9k
Declaration emit for inlined mapped types preserves modifier-preserving behavior #55054
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. dprint when (this is hard to read with 2 predicates per line, each of which are so long that they wrap on my screen) |
||||||
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) | ||||||
); | ||||||
} | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
double-checking: is the effect of this PR that we now preserve modifiers for these mapped types, and didn't before? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. without reading the code of the fix itself - yes, the issue that I reported is specifically about the fact that modifiers are lost but they shouldn't be so the fix for that issue should preserve them |
||||||
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; | ||||||
} | ||||||
|
||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
>{} : {} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.