Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6616,7 +6616,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {

function isHomomorphicMappedTypeWithNonHomomorphicInstantiation(type: MappedType) {
return isMappedTypeWithKeyofConstraintDeclaration(type)
&& !(getModifiersTypeFromMappedType(type).flags & TypeFlags.TypeParameter);
&& !(getModifiersTypeFromMappedType(type).flags & TypeFlags.TypeParameter)
&& !!type.aliasSymbol;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't have anything to do with the alias symbol as by the time we're trying to print with this function, the alias symbol has already been discarded as an option for printing the mapped type. Instead, what we care about is if the type will be picked up as a homomorphic type variable (or not). Type parameters always are, so were a conservative initial choice - it's generally safe to wrap anything else which is still recognized as a homomorphic type variable up and present it as a type parameter, too - all you get is unnecessarily verbose printback sometimes. Instead of checking the alias symbol here (which seems pretty unrelated), you want to check, rather than if the modifiers type is still a type parameter, if an index on the modifiers type still produces a homomorphic type variable. In this case, an index of a mapped type just returns the inner mapped type's constraint type, which can then be unwrapped, so it is recognized as homomorphic. So, instead of the simple, conservative !(getModifiersTypeFromMappedType(type).flags & TypeFlags.TypeParameter) check, you want something to more closely mirror getHomomorphicTypeVariable's conditions like

                let index: Type;
                return isMappedTypeWithKeyofConstraintDeclaration(type)
                    && (index = getIndexType(getModifiersTypeFromMappedType(type)), !(index.flags & TypeFlags.Index && (index as IndexType).type.flags & TypeFlags.TypeParameter)))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I applied the suggested change but it reverted the change to the emit of this thing (TS playground):

export declare type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
export declare type PartialProperties<T, K extends keyof T> = Partial<Pick<T, K>> & Omit<T, K>;
export function doSomething_Actual<T extends {
    prop: string;
}>(a: T) {
    const x: { [P in keyof PartialProperties<T, "prop">]: PartialProperties<T, "prop">[P]; } = null as any;
    return x;
}

Does this one have to be rewritten? 🤔

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not, since, in that case, I don't think PartialProperties is homomorphic, so the wrapping mapped type shouldn't be either. Right now we're using isMappedTypeWithKeyofConstraintDeclaration as a proxy for was-this-declared-as-homomorphic - we probably need to swap that out for something a bit more accurate, too. Potentially the same check we're doing on type but on type.target (and negated)?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, it's not actually homomorphic -

doSomething_Actual<{prop: string, propb: string} | {prop: string, propa: string}>(null as any);

ends up with type

{
    prop?: string | undefined;
}

so we just need to tighten up that side of the check, too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This particular mapped type doesn't have a .target property - I might have misunderstood your recommendation for the proposed changes but for now, I've chosen to just avoid rewriting in case of !type.target. I think this is correct (it yields the expected result for this case and the rest of the test suite) but maybe I'm missing some further details.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, that's perfect - if the mapped type doesn't have a .target, then leaving it as-is is always correct - it's still it's declared form, uninstantiated.

}

function createMappedTypeNodeFromType(type: MappedType) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//// [mappedTypeGenericInstantiationPreservesInlineForm.ts]
// repro from #53109

export const test1 = <T = Record<string, never>>(schema: {
[K in keyof Required<T>]: T[K];
}) => {}

export function test2<T = Record<string, never>>(schema: {
[K in keyof Required<T>]: T[K];
}) {};




//// [mappedTypeGenericInstantiationPreservesInlineForm.d.ts]
export declare const test1: <T = Record<string, never>>(schema: { [K in keyof Required<T>]: T[K]; }) => void;
export declare function test2<T = Record<string, never>>(schema: {
[K in keyof Required<T>]: T[K];
}): void;
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
=== tests/cases/compiler/mappedTypeGenericInstantiationPreservesInlineForm.ts ===
// repro from #53109

export const test1 = <T = Record<string, never>>(schema: {
>test1 : Symbol(test1, Decl(mappedTypeGenericInstantiationPreservesInlineForm.ts, 2, 12))
>T : Symbol(T, Decl(mappedTypeGenericInstantiationPreservesInlineForm.ts, 2, 22))
>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --))
>schema : Symbol(schema, Decl(mappedTypeGenericInstantiationPreservesInlineForm.ts, 2, 49))

[K in keyof Required<T>]: T[K];
>K : Symbol(K, Decl(mappedTypeGenericInstantiationPreservesInlineForm.ts, 3, 5))
>Required : Symbol(Required, Decl(lib.es5.d.ts, --, --))
>T : Symbol(T, Decl(mappedTypeGenericInstantiationPreservesInlineForm.ts, 2, 22))
>T : Symbol(T, Decl(mappedTypeGenericInstantiationPreservesInlineForm.ts, 2, 22))
>K : Symbol(K, Decl(mappedTypeGenericInstantiationPreservesInlineForm.ts, 3, 5))

}) => {}

export function test2<T = Record<string, never>>(schema: {
>test2 : Symbol(test2, Decl(mappedTypeGenericInstantiationPreservesInlineForm.ts, 4, 8))
>T : Symbol(T, Decl(mappedTypeGenericInstantiationPreservesInlineForm.ts, 6, 22))
>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --))
>schema : Symbol(schema, Decl(mappedTypeGenericInstantiationPreservesInlineForm.ts, 6, 49))

[K in keyof Required<T>]: T[K];
>K : Symbol(K, Decl(mappedTypeGenericInstantiationPreservesInlineForm.ts, 7, 5))
>Required : Symbol(Required, Decl(lib.es5.d.ts, --, --))
>T : Symbol(T, Decl(mappedTypeGenericInstantiationPreservesInlineForm.ts, 6, 22))
>T : Symbol(T, Decl(mappedTypeGenericInstantiationPreservesInlineForm.ts, 6, 22))
>K : Symbol(K, Decl(mappedTypeGenericInstantiationPreservesInlineForm.ts, 7, 5))

}) {};

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
=== tests/cases/compiler/mappedTypeGenericInstantiationPreservesInlineForm.ts ===
// repro from #53109

export const test1 = <T = Record<string, never>>(schema: {
>test1 : <T = Record<string, never>>(schema: { [K in keyof Required<T>]: T[K]; }) => void
><T = Record<string, never>>(schema: { [K in keyof Required<T>]: T[K];}) => {} : <T = Record<string, never>>(schema: { [K in keyof Required<T>]: T[K]; }) => void
>schema : { [K in keyof Required<T>]: T[K]; }

[K in keyof Required<T>]: T[K];
}) => {}

export function test2<T = Record<string, never>>(schema: {
>test2 : <T = Record<string, never>>(schema: { [K in keyof Required<T>]: T[K]; }) => void
>schema : { [K in keyof Required<T>]: T[K]; }

[K in keyof Required<T>]: T[K];
}) {};

Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export declare type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
export declare type PartialProperties<T, K extends keyof T> = Partial<Pick<T, K>> & Omit<T, K>;
export declare function doSomething_Actual<T extends {
prop: string;
}>(a: T): PartialProperties<T, "prop"> extends infer T_1 ? { [P in keyof T_1]: PartialProperties<T, "prop">[P]; } : never;
}>(a: T): { [P in keyof PartialProperties<T, "prop">]: PartialProperties<T, "prop">[P]; };
export declare function doSomething_Expected<T extends {
prop: string;
}>(a: T): {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// @strict: true
// @declaration: true
// @emitDeclarationOnly: true

// repro from #53109

export const test1 = <T = Record<string, never>>(schema: {
[K in keyof Required<T>]: T[K];
}) => {}

export function test2<T = Record<string, never>>(schema: {
[K in keyof Required<T>]: T[K];
}) {};