Skip to content

Commit 7c5c664

Browse files
authored
Merge pull request #12826 from Microsoft/mappedTypeModifiers2
Improve propagation of modifiers in mapped types
2 parents 57cb4ac + 7fdfcf1 commit 7c5c664

File tree

9 files changed

+627
-470
lines changed

9 files changed

+627
-470
lines changed

src/compiler/checker.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4521,12 +4521,11 @@ namespace ts {
45214521
// Resolve upfront such that recursive references see an empty object type.
45224522
setStructuredTypeMembers(type, emptySymbols, emptyArray, emptyArray, undefined, undefined);
45234523
// In { [P in K]: T }, we refer to P as the type parameter type, K as the constraint type,
4524-
// and T as the template type. If K is of the form 'keyof S', the mapped type and S are
4525-
// homomorphic and we copy property modifiers from corresponding properties in S.
4524+
// and T as the template type.
45264525
const typeParameter = getTypeParameterFromMappedType(type);
45274526
const constraintType = getConstraintTypeFromMappedType(type);
4528-
const homomorphicType = getHomomorphicTypeFromMappedType(type);
45294527
const templateType = getTemplateTypeFromMappedType(type);
4528+
const modifiersType = getModifiersTypeFromMappedType(type);
45304529
const templateReadonly = !!type.declaration.readonlyToken;
45314530
const templateOptional = !!type.declaration.questionToken;
45324531
// First, if the constraint type is a type parameter, obtain the base constraint. Then,
@@ -4545,11 +4544,11 @@ namespace ts {
45454544
// Otherwise, for type string create a string index signature.
45464545
if (t.flags & TypeFlags.StringLiteral) {
45474546
const propName = (<LiteralType>t).text;
4548-
const homomorphicProp = homomorphicType && getPropertyOfType(homomorphicType, propName);
4549-
const isOptional = templateOptional || !!(homomorphicProp && homomorphicProp.flags & SymbolFlags.Optional);
4547+
const modifiersProp = getPropertyOfType(modifiersType, propName);
4548+
const isOptional = templateOptional || !!(modifiersProp && modifiersProp.flags & SymbolFlags.Optional);
45504549
const prop = <TransientSymbol>createSymbol(SymbolFlags.Property | SymbolFlags.Transient | (isOptional ? SymbolFlags.Optional : 0), propName);
45514550
prop.type = propType;
4552-
prop.isReadonly = templateReadonly || homomorphicProp && isReadonlySymbol(homomorphicProp);
4551+
prop.isReadonly = templateReadonly || modifiersProp && isReadonlySymbol(modifiersProp);
45534552
members[propName] = prop;
45544553
}
45554554
else if (t.flags & TypeFlags.String) {
@@ -4576,9 +4575,16 @@ namespace ts {
45764575
unknownType);
45774576
}
45784577

4579-
function getHomomorphicTypeFromMappedType(type: MappedType) {
4580-
const constraint = getConstraintDeclaration(getTypeParameterFromMappedType(type));
4581-
return constraint.kind === SyntaxKind.TypeOperator ? instantiateType(getTypeFromTypeNode((<TypeOperatorNode>constraint).type), type.mapper || identityMapper) : undefined;
4578+
function getModifiersTypeFromMappedType(type: MappedType) {
4579+
if (!type.modifiersType) {
4580+
// If the mapped type was declared as { [P in keyof T]: X } or as { [P in K]: X }, where
4581+
// K is constrained to 'K extends keyof T', then we will copy property modifiers from T.
4582+
const declaredType = <MappedType>getTypeFromMappedTypeNode(type.declaration);
4583+
const constraint = getConstraintTypeFromMappedType(declaredType);
4584+
const extendedConstraint = constraint.flags & TypeFlags.TypeParameter ? getConstraintOfTypeParameter(<TypeParameter>constraint) : constraint;
4585+
type.modifiersType = extendedConstraint.flags & TypeFlags.Index ? instantiateType((<IndexType>extendedConstraint).type, type.mapper || identityMapper) : emptyObjectType;
4586+
}
4587+
return type.modifiersType;
45824588
}
45834589

45844590
function getErasedTemplateTypeFromMappedType(type: MappedType) {

src/compiler/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2933,6 +2933,7 @@ namespace ts {
29332933
typeParameter?: TypeParameter;
29342934
constraintType?: Type;
29352935
templateType?: Type;
2936+
modifiersType?: Type;
29362937
mapper?: TypeMapper; // Instantiation mapper
29372938
}
29382939

tests/baselines/reference/mappedTypeErrors.errors.txt

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,19 @@ tests/cases/conformance/types/mapped/mappedTypeErrors.ts(78,59): error TS2345: A
2626
Object literal may only specify known properties, and 'z' does not exist in type 'Readonly<{ x: number; y: number; }>'.
2727
tests/cases/conformance/types/mapped/mappedTypeErrors.ts(84,58): error TS2345: Argument of type '{ x: number; y: number; z: number; }' is not assignable to parameter of type 'Partial<{ x: number; y: number; }>'.
2828
Object literal may only specify known properties, and 'z' does not exist in type 'Partial<{ x: number; y: number; }>'.
29+
tests/cases/conformance/types/mapped/mappedTypeErrors.ts(106,15): error TS2345: Argument of type '{ a: undefined; }' is not assignable to parameter of type 'Pick<Foo, "a">'.
30+
Types of property 'a' are incompatible.
31+
Type 'undefined' is not assignable to type 'string'.
32+
tests/cases/conformance/types/mapped/mappedTypeErrors.ts(107,17): error TS2345: Argument of type '{ c: boolean; }' is not assignable to parameter of type 'Pick<Foo, "a" | "b">'.
33+
Object literal may only specify known properties, and 'c' does not exist in type 'Pick<Foo, "a" | "b">'.
34+
tests/cases/conformance/types/mapped/mappedTypeErrors.ts(124,12): error TS2345: Argument of type '{ a: undefined; }' is not assignable to parameter of type 'Pick<Foo, "a">'.
35+
Types of property 'a' are incompatible.
36+
Type 'undefined' is not assignable to type 'string'.
37+
tests/cases/conformance/types/mapped/mappedTypeErrors.ts(125,14): error TS2345: Argument of type '{ c: boolean; }' is not assignable to parameter of type 'Pick<Foo, "a" | "b">'.
38+
Object literal may only specify known properties, and 'c' does not exist in type 'Pick<Foo, "a" | "b">'.
2939

3040

31-
==== tests/cases/conformance/types/mapped/mappedTypeErrors.ts (17 errors) ====
41+
==== tests/cases/conformance/types/mapped/mappedTypeErrors.ts (21 errors) ====
3242

3343
interface Shape {
3444
name: string;
@@ -158,4 +168,59 @@ tests/cases/conformance/types/mapped/mappedTypeErrors.ts(84,58): error TS2345: A
158168
~~~~
159169
!!! error TS2345: Argument of type '{ x: number; y: number; z: number; }' is not assignable to parameter of type 'Partial<{ x: number; y: number; }>'.
160170
!!! error TS2345: Object literal may only specify known properties, and 'z' does not exist in type 'Partial<{ x: number; y: number; }>'.
161-
}
171+
}
172+
173+
// Verify use of Pick<T, K> for setState functions (#12793)
174+
175+
interface Foo {
176+
a: string;
177+
b?: number;
178+
}
179+
180+
function setState<T, K extends keyof T>(obj: T, props: Pick<T, K>) {
181+
for (let k in props) {
182+
obj[k] = props[k];
183+
}
184+
}
185+
186+
let foo: Foo = { a: "hello", b: 42 };
187+
setState(foo, { a: "test", b: 43 })
188+
setState(foo, { a: "hi" });
189+
setState(foo, { b: undefined });
190+
setState(foo, { });
191+
setState(foo, foo);
192+
setState(foo, { a: undefined }); // Error
193+
~~~~~~~~~~~~~~~~
194+
!!! error TS2345: Argument of type '{ a: undefined; }' is not assignable to parameter of type 'Pick<Foo, "a">'.
195+
!!! error TS2345: Types of property 'a' are incompatible.
196+
!!! error TS2345: Type 'undefined' is not assignable to type 'string'.
197+
setState(foo, { c: true }); // Error
198+
~~~~~~~
199+
!!! error TS2345: Argument of type '{ c: boolean; }' is not assignable to parameter of type 'Pick<Foo, "a" | "b">'.
200+
!!! error TS2345: Object literal may only specify known properties, and 'c' does not exist in type 'Pick<Foo, "a" | "b">'.
201+
202+
class C<T> {
203+
state: T;
204+
setState<K extends keyof T>(props: Pick<T, K>) {
205+
for (let k in props) {
206+
this.state[k] = props[k];
207+
}
208+
}
209+
}
210+
211+
let c = new C<Foo>();
212+
c.setState({ a: "test", b: 43 });
213+
c.setState({ a: "hi" });
214+
c.setState({ b: undefined });
215+
c.setState({ });
216+
c.setState(foo);
217+
c.setState({ a: undefined }); // Error
218+
~~~~~~~~~~~~~~~~
219+
!!! error TS2345: Argument of type '{ a: undefined; }' is not assignable to parameter of type 'Pick<Foo, "a">'.
220+
!!! error TS2345: Types of property 'a' are incompatible.
221+
!!! error TS2345: Type 'undefined' is not assignable to type 'string'.
222+
c.setState({ c: true }); // Error
223+
~~~~~~~
224+
!!! error TS2345: Argument of type '{ c: boolean; }' is not assignable to parameter of type 'Pick<Foo, "a" | "b">'.
225+
!!! error TS2345: Object literal may only specify known properties, and 'c' does not exist in type 'Pick<Foo, "a" | "b">'.
226+

tests/baselines/reference/mappedTypeErrors.js

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,48 @@ function f21() {
8383
let x1 = objAndPartial({ x: 0, y: 0 }, { x: 1 });
8484
let x2 = objAndPartial({ x: 0, y: 0 }, { x: 1, y: 1 });
8585
let x3 = objAndPartial({ x: 0, y: 0 }, { x: 1, y: 1, z: 1 }); // Error
86-
}
86+
}
87+
88+
// Verify use of Pick<T, K> for setState functions (#12793)
89+
90+
interface Foo {
91+
a: string;
92+
b?: number;
93+
}
94+
95+
function setState<T, K extends keyof T>(obj: T, props: Pick<T, K>) {
96+
for (let k in props) {
97+
obj[k] = props[k];
98+
}
99+
}
100+
101+
let foo: Foo = { a: "hello", b: 42 };
102+
setState(foo, { a: "test", b: 43 })
103+
setState(foo, { a: "hi" });
104+
setState(foo, { b: undefined });
105+
setState(foo, { });
106+
setState(foo, foo);
107+
setState(foo, { a: undefined }); // Error
108+
setState(foo, { c: true }); // Error
109+
110+
class C<T> {
111+
state: T;
112+
setState<K extends keyof T>(props: Pick<T, K>) {
113+
for (let k in props) {
114+
this.state[k] = props[k];
115+
}
116+
}
117+
}
118+
119+
let c = new C<Foo>();
120+
c.setState({ a: "test", b: 43 });
121+
c.setState({ a: "hi" });
122+
c.setState({ b: undefined });
123+
c.setState({ });
124+
c.setState(foo);
125+
c.setState({ a: undefined }); // Error
126+
c.setState({ c: true }); // Error
127+
87128

88129
//// [mappedTypeErrors.js]
89130
function f1(x) {
@@ -124,6 +165,37 @@ function f21() {
124165
var x2 = objAndPartial({ x: 0, y: 0 }, { x: 1, y: 1 });
125166
var x3 = objAndPartial({ x: 0, y: 0 }, { x: 1, y: 1, z: 1 }); // Error
126167
}
168+
function setState(obj, props) {
169+
for (var k in props) {
170+
obj[k] = props[k];
171+
}
172+
}
173+
var foo = { a: "hello", b: 42 };
174+
setState(foo, { a: "test", b: 43 });
175+
setState(foo, { a: "hi" });
176+
setState(foo, { b: undefined });
177+
setState(foo, {});
178+
setState(foo, foo);
179+
setState(foo, { a: undefined }); // Error
180+
setState(foo, { c: true }); // Error
181+
var C = (function () {
182+
function C() {
183+
}
184+
C.prototype.setState = function (props) {
185+
for (var k in props) {
186+
this.state[k] = props[k];
187+
}
188+
};
189+
return C;
190+
}());
191+
var c = new C();
192+
c.setState({ a: "test", b: 43 });
193+
c.setState({ a: "hi" });
194+
c.setState({ b: undefined });
195+
c.setState({});
196+
c.setState(foo);
197+
c.setState({ a: undefined }); // Error
198+
c.setState({ c: true }); // Error
127199

128200

129201
//// [mappedTypeErrors.d.ts]
@@ -168,3 +240,14 @@ declare function objAndReadonly<T>(primary: T, secondary: Readonly<T>): T;
168240
declare function objAndPartial<T>(primary: T, secondary: Partial<T>): T;
169241
declare function f20(): void;
170242
declare function f21(): void;
243+
interface Foo {
244+
a: string;
245+
b?: number;
246+
}
247+
declare function setState<T, K extends keyof T>(obj: T, props: Pick<T, K>): void;
248+
declare let foo: Foo;
249+
declare class C<T> {
250+
state: T;
251+
setState<K extends keyof T>(props: Pick<T, K>): void;
252+
}
253+
declare let c: C<Foo>;

0 commit comments

Comments
 (0)