diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index a5b9299d1ca..62fc1a7cc41 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -3158,7 +3158,16 @@ namespace ts { Debug.assert(!!(type.flags & TypeFlags.Object)); const readonlyToken = type.declaration.readonlyToken ? createToken(type.declaration.readonlyToken.kind) : undefined; const questionToken = type.declaration.questionToken ? createToken(type.declaration.questionToken.kind) : undefined; - const typeParameterNode = typeParameterToDeclaration(getTypeParameterFromMappedType(type), context, getConstraintTypeFromMappedType(type)); + let appropriateConstraintTypeNode: TypeNode; + 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` + appropriateConstraintTypeNode = createTypeOperatorNode(typeToTypeNodeHelper(getModifiersTypeFromMappedType(type), context)); + } + else { + appropriateConstraintTypeNode = typeToTypeNodeHelper(getConstraintTypeFromMappedType(type), context); + } + const typeParameterNode = typeParameterToDeclarationWithConstraint(getTypeParameterFromMappedType(type), context, appropriateConstraintTypeNode); const templateTypeNode = typeToTypeNodeHelper(getTemplateTypeFromMappedType(type), context); const mappedTypeNode = createMappedTypeNode(readonlyToken, typeParameterNode, questionToken, templateTypeNode); return setEmitFlags(mappedTypeNode, EmitFlags.SingleLine); @@ -3534,17 +3543,21 @@ namespace ts { return createSignatureDeclaration(kind, typeParameters, parameters, returnTypeNode, typeArguments); } - function typeParameterToDeclaration(type: TypeParameter, context: NodeBuilderContext, constraint = getConstraintFromTypeParameter(type)): TypeParameterDeclaration { + function typeParameterToDeclarationWithConstraint(type: TypeParameter, context: NodeBuilderContext, constraintNode: TypeNode): TypeParameterDeclaration { const savedContextFlags = context.flags; context.flags &= ~NodeBuilderFlags.WriteTypeParametersInQualifiedName; // Avoids potential infinite loop when building for a claimspace with a generic const name = symbolToName(type.symbol, context, SymbolFlags.Type, /*expectsIdentifier*/ true); - const constraintNode = constraint && typeToTypeNodeHelper(constraint, context); const defaultParameter = getDefaultFromTypeParameter(type); const defaultParameterNode = defaultParameter && typeToTypeNodeHelper(defaultParameter, context); context.flags = savedContextFlags; return createTypeParameterDeclaration(name, constraintNode, defaultParameterNode); } + function typeParameterToDeclaration(type: TypeParameter, context: NodeBuilderContext, constraint = getConstraintFromTypeParameter(type)): TypeParameterDeclaration { + const constraintNode = constraint && typeToTypeNodeHelper(constraint, context); + return typeParameterToDeclarationWithConstraint(type, context, constraintNode); + } + function symbolToParameterDeclaration(parameterSymbol: Symbol, context: NodeBuilderContext, preserveModifierFlags?: boolean): ParameterDeclaration { const parameterDeclaration = getDeclarationOfKind(parameterSymbol, SyntaxKind.Parameter); Debug.assert(!!parameterDeclaration || isTransientSymbol(parameterSymbol) && !!parameterSymbol.isRestParameter); @@ -6221,10 +6234,8 @@ namespace ts { const templateType = getTemplateTypeFromMappedType(type.target || type); const modifiersType = getApparentType(getModifiersTypeFromMappedType(type)); // The 'T' in 'keyof T' const templateModifiers = getMappedTypeModifiers(type); - const constraintDeclaration = type.declaration.typeParameter.constraint; const include = keyofStringsOnly ? TypeFlags.StringLiteral : TypeFlags.StringOrNumberLiteralOrUnique; - if (constraintDeclaration.kind === SyntaxKind.TypeOperator && - (constraintDeclaration).operator === SyntaxKind.KeyOfKeyword) { + if (isMappedTypeWithKeyofConstraintDeclaration(type)) { // We have a { [P in keyof T]: X } for (const prop of getPropertiesOfType(modifiersType)) { addMemberForKeyType(getLiteralTypeFromPropertyName(prop, include), /*_index*/ undefined, prop); @@ -6301,15 +6312,23 @@ namespace ts { unknownType); } + function getConstraintDeclarationForMappedType(type: MappedType) { + return type.declaration.typeParameter.constraint; + } + + function isMappedTypeWithKeyofConstraintDeclaration(type: MappedType) { + const constraintDeclaration = getConstraintDeclarationForMappedType(type); + return constraintDeclaration.kind === SyntaxKind.TypeOperator && + (constraintDeclaration).operator === SyntaxKind.KeyOfKeyword; + } + function getModifiersTypeFromMappedType(type: MappedType) { if (!type.modifiersType) { - const constraintDeclaration = type.declaration.typeParameter.constraint; - if (constraintDeclaration.kind === SyntaxKind.TypeOperator && - (constraintDeclaration).operator === SyntaxKind.KeyOfKeyword) { + if (isMappedTypeWithKeyofConstraintDeclaration(type)) { // If the constraint declaration is a 'keyof T' node, the modifiers type is T. We check // AST nodes here because, when T is a non-generic type, the logic below eagerly resolves // 'keyof T' to a literal union type and we can't recover T from that type. - type.modifiersType = instantiateType(getTypeFromTypeNode((constraintDeclaration).type), type.mapper || identityMapper); + type.modifiersType = instantiateType(getTypeFromTypeNode((getConstraintDeclarationForMappedType(type)).type), type.mapper || identityMapper); } else { // Otherwise, get the declared constraint type, and if the constraint type is a type parameter, diff --git a/tests/baselines/reference/mappedTypeUnionConstraintInferences.js b/tests/baselines/reference/mappedTypeUnionConstraintInferences.js new file mode 100644 index 00000000000..944b43479c7 --- /dev/null +++ b/tests/baselines/reference/mappedTypeUnionConstraintInferences.js @@ -0,0 +1,51 @@ +//// [mappedTypeUnionConstraintInferences.ts] +export declare type Omit = Pick>; +export declare type PartialProperties = Partial> & Omit; +export function doSomething_Actual(a: T) { + const x: { [P in keyof PartialProperties]: PartialProperties[P]; } = null as any; + return x; +} +export declare function doSomething_Expected(a: T): { [P in keyof PartialProperties]: PartialProperties[P]; }; + +export let a = doSomething_Actual({ prop: "test" }); +a = {} // should be fine, equivalent to below + +export let b = doSomething_Expected({ prop: "test" }); +b = {} // fine + + +//// [mappedTypeUnionConstraintInferences.js] +"use strict"; +exports.__esModule = true; +function doSomething_Actual(a) { + var x = null; + return x; +} +exports.doSomething_Actual = doSomething_Actual; +exports.a = doSomething_Actual({ prop: "test" }); +exports.a = {}; // should be fine, equivalent to below +exports.b = doSomething_Expected({ prop: "test" }); +exports.b = {}; // fine + + +//// [mappedTypeUnionConstraintInferences.d.ts] +export declare type Omit = Pick>; +export declare type PartialProperties = Partial> & Omit; +export declare function doSomething_Actual(a: T): { [P in keyof PartialProperties]: PartialProperties[P]; }; +export declare function doSomething_Expected(a: T): { + [P in keyof PartialProperties]: PartialProperties[P]; +}; +export declare let a: { + prop?: string; +}; +export declare let b: { + prop?: string; +}; diff --git a/tests/baselines/reference/mappedTypeUnionConstraintInferences.symbols b/tests/baselines/reference/mappedTypeUnionConstraintInferences.symbols new file mode 100644 index 00000000000..10a998ace63 --- /dev/null +++ b/tests/baselines/reference/mappedTypeUnionConstraintInferences.symbols @@ -0,0 +1,81 @@ +=== tests/cases/compiler/mappedTypeUnionConstraintInferences.ts === +export declare type Omit = Pick>; +>Omit : Symbol(Omit, Decl(mappedTypeUnionConstraintInferences.ts, 0, 0)) +>T : Symbol(T, Decl(mappedTypeUnionConstraintInferences.ts, 0, 25)) +>K : Symbol(K, Decl(mappedTypeUnionConstraintInferences.ts, 0, 27)) +>T : Symbol(T, Decl(mappedTypeUnionConstraintInferences.ts, 0, 25)) +>Pick : Symbol(Pick, Decl(lib.d.ts, --, --)) +>T : Symbol(T, Decl(mappedTypeUnionConstraintInferences.ts, 0, 25)) +>Exclude : Symbol(Exclude, Decl(lib.d.ts, --, --)) +>T : Symbol(T, Decl(mappedTypeUnionConstraintInferences.ts, 0, 25)) +>K : Symbol(K, Decl(mappedTypeUnionConstraintInferences.ts, 0, 27)) + +export declare type PartialProperties = Partial> & Omit; +>PartialProperties : Symbol(PartialProperties, Decl(mappedTypeUnionConstraintInferences.ts, 0, 78)) +>T : Symbol(T, Decl(mappedTypeUnionConstraintInferences.ts, 1, 38)) +>K : Symbol(K, Decl(mappedTypeUnionConstraintInferences.ts, 1, 40)) +>T : Symbol(T, Decl(mappedTypeUnionConstraintInferences.ts, 1, 38)) +>Partial : Symbol(Partial, Decl(lib.d.ts, --, --)) +>Pick : Symbol(Pick, Decl(lib.d.ts, --, --)) +>T : Symbol(T, Decl(mappedTypeUnionConstraintInferences.ts, 1, 38)) +>K : Symbol(K, Decl(mappedTypeUnionConstraintInferences.ts, 1, 40)) +>Omit : Symbol(Omit, Decl(mappedTypeUnionConstraintInferences.ts, 0, 0)) +>T : Symbol(T, Decl(mappedTypeUnionConstraintInferences.ts, 1, 38)) +>K : Symbol(K, Decl(mappedTypeUnionConstraintInferences.ts, 1, 40)) + +export function doSomething_ActualdoSomething_Actual : Symbol(doSomething_Actual, Decl(mappedTypeUnionConstraintInferences.ts, 1, 95)) +>T : Symbol(T, Decl(mappedTypeUnionConstraintInferences.ts, 2, 35)) + + prop: string; +>prop : Symbol(prop, Decl(mappedTypeUnionConstraintInferences.ts, 2, 46)) + +}>(a: T) { +>a : Symbol(a, Decl(mappedTypeUnionConstraintInferences.ts, 4, 3)) +>T : Symbol(T, Decl(mappedTypeUnionConstraintInferences.ts, 2, 35)) + + const x: { [P in keyof PartialProperties]: PartialProperties[P]; } = null as any; +>x : Symbol(x, Decl(mappedTypeUnionConstraintInferences.ts, 5, 9)) +>P : Symbol(P, Decl(mappedTypeUnionConstraintInferences.ts, 5, 16)) +>PartialProperties : Symbol(PartialProperties, Decl(mappedTypeUnionConstraintInferences.ts, 0, 78)) +>T : Symbol(T, Decl(mappedTypeUnionConstraintInferences.ts, 2, 35)) +>PartialProperties : Symbol(PartialProperties, Decl(mappedTypeUnionConstraintInferences.ts, 0, 78)) +>T : Symbol(T, Decl(mappedTypeUnionConstraintInferences.ts, 2, 35)) +>P : Symbol(P, Decl(mappedTypeUnionConstraintInferences.ts, 5, 16)) + + return x; +>x : Symbol(x, Decl(mappedTypeUnionConstraintInferences.ts, 5, 9)) +} +export declare function doSomething_ExpecteddoSomething_Expected : Symbol(doSomething_Expected, Decl(mappedTypeUnionConstraintInferences.ts, 7, 1)) +>T : Symbol(T, Decl(mappedTypeUnionConstraintInferences.ts, 8, 45)) + + prop: string; +>prop : Symbol(prop, Decl(mappedTypeUnionConstraintInferences.ts, 8, 56)) + +}>(a: T): { [P in keyof PartialProperties]: PartialProperties[P]; }; +>a : Symbol(a, Decl(mappedTypeUnionConstraintInferences.ts, 10, 3)) +>T : Symbol(T, Decl(mappedTypeUnionConstraintInferences.ts, 8, 45)) +>P : Symbol(P, Decl(mappedTypeUnionConstraintInferences.ts, 10, 13)) +>PartialProperties : Symbol(PartialProperties, Decl(mappedTypeUnionConstraintInferences.ts, 0, 78)) +>T : Symbol(T, Decl(mappedTypeUnionConstraintInferences.ts, 8, 45)) +>PartialProperties : Symbol(PartialProperties, Decl(mappedTypeUnionConstraintInferences.ts, 0, 78)) +>T : Symbol(T, Decl(mappedTypeUnionConstraintInferences.ts, 8, 45)) +>P : Symbol(P, Decl(mappedTypeUnionConstraintInferences.ts, 10, 13)) + +export let a = doSomething_Actual({ prop: "test" }); +>a : Symbol(a, Decl(mappedTypeUnionConstraintInferences.ts, 12, 10)) +>doSomething_Actual : Symbol(doSomething_Actual, Decl(mappedTypeUnionConstraintInferences.ts, 1, 95)) +>prop : Symbol(prop, Decl(mappedTypeUnionConstraintInferences.ts, 12, 35)) + +a = {} // should be fine, equivalent to below +>a : Symbol(a, Decl(mappedTypeUnionConstraintInferences.ts, 12, 10)) + +export let b = doSomething_Expected({ prop: "test" }); +>b : Symbol(b, Decl(mappedTypeUnionConstraintInferences.ts, 15, 10)) +>doSomething_Expected : Symbol(doSomething_Expected, Decl(mappedTypeUnionConstraintInferences.ts, 7, 1)) +>prop : Symbol(prop, Decl(mappedTypeUnionConstraintInferences.ts, 15, 37)) + +b = {} // fine +>b : Symbol(b, Decl(mappedTypeUnionConstraintInferences.ts, 15, 10)) + diff --git a/tests/baselines/reference/mappedTypeUnionConstraintInferences.types b/tests/baselines/reference/mappedTypeUnionConstraintInferences.types new file mode 100644 index 00000000000..ea88f4de6dc --- /dev/null +++ b/tests/baselines/reference/mappedTypeUnionConstraintInferences.types @@ -0,0 +1,93 @@ +=== tests/cases/compiler/mappedTypeUnionConstraintInferences.ts === +export declare type Omit = Pick>; +>Omit : Pick> +>T : T +>K : K +>T : T +>Pick : Pick +>T : T +>Exclude : Exclude +>T : T +>K : K + +export declare type PartialProperties = Partial> & Omit; +>PartialProperties : PartialProperties +>T : T +>K : K +>T : T +>Partial : Partial +>Pick : Pick +>T : T +>K : K +>Omit : Pick> +>T : T +>K : K + +export function doSomething_ActualdoSomething_Actual : (a: T) => { [P in keyof PartialProperties]: PartialProperties[P]; } +>T : T + + prop: string; +>prop : string + +}>(a: T) { +>a : T +>T : T + + const x: { [P in keyof PartialProperties]: PartialProperties[P]; } = null as any; +>x : { [P in keyof PartialProperties]: PartialProperties[P]; } +>P : P +>PartialProperties : PartialProperties +>T : T +>PartialProperties : PartialProperties +>T : T +>P : P +>null as any : any +>null : null + + return x; +>x : { [P in keyof PartialProperties]: PartialProperties[P]; } +} +export declare function doSomething_ExpecteddoSomething_Expected : (a: T) => { [P in keyof PartialProperties]: PartialProperties[P]; } +>T : T + + prop: string; +>prop : string + +}>(a: T): { [P in keyof PartialProperties]: PartialProperties[P]; }; +>a : T +>T : T +>P : P +>PartialProperties : PartialProperties +>T : T +>PartialProperties : PartialProperties +>T : T +>P : P + +export let a = doSomething_Actual({ prop: "test" }); +>a : { prop?: string; } +>doSomething_Actual({ prop: "test" }) : { prop?: string; } +>doSomething_Actual : (a: T) => { [P in keyof PartialProperties]: PartialProperties[P]; } +>{ prop: "test" } : { prop: string; } +>prop : string +>"test" : "test" + +a = {} // should be fine, equivalent to below +>a = {} : {} +>a : { prop?: string; } +>{} : {} + +export let b = doSomething_Expected({ prop: "test" }); +>b : { prop?: string; } +>doSomething_Expected({ prop: "test" }) : { prop?: string; } +>doSomething_Expected : (a: T) => { [P in keyof PartialProperties]: PartialProperties[P]; } +>{ prop: "test" } : { prop: string; } +>prop : string +>"test" : "test" + +b = {} // fine +>b = {} : {} +>b : { prop?: string; } +>{} : {} + diff --git a/tests/cases/compiler/mappedTypeUnionConstraintInferences.ts b/tests/cases/compiler/mappedTypeUnionConstraintInferences.ts new file mode 100644 index 00000000000..bcc8e8e556f --- /dev/null +++ b/tests/cases/compiler/mappedTypeUnionConstraintInferences.ts @@ -0,0 +1,19 @@ +// @declaration: true +// @keyofStringsOnly: true +export declare type Omit = Pick>; +export declare type PartialProperties = Partial> & Omit; +export function doSomething_Actual(a: T) { + const x: { [P in keyof PartialProperties]: PartialProperties[P]; } = null as any; + return x; +} +export declare function doSomething_Expected(a: T): { [P in keyof PartialProperties]: PartialProperties[P]; }; + +export let a = doSomething_Actual({ prop: "test" }); +a = {} // should be fine, equivalent to below + +export let b = doSomething_Expected({ prop: "test" }); +b = {} // fine