Skip to content

Commit eb2046d

Browse files
jfet97Andaristsandersn
authored
Reverse mapped types with intersection constraint (#55811)
Co-authored-by: Mateusz Burzyński <[email protected]> Co-authored-by: Nathan Shively-Sanders <[email protected]>
1 parent 2c4cbd9 commit eb2046d

11 files changed

+1814
-2
lines changed

src/compiler/checker.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13647,14 +13647,41 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1364713647
return instantiateType(instantiable, createTypeMapper([type.indexType, type.objectType], [getNumberLiteralType(0), createTupleType([replacement])]));
1364813648
}
1364913649

13650+
// If the original mapped type had an intersection constraint we extract its components,
13651+
// and we make an attempt to do so even if the intersection has been reduced to a union.
13652+
// This entire process allows us to possibly retrieve the filtering type literals.
13653+
// e.g. { [K in keyof U & ("a" | "b") ] } -> "a" | "b"
13654+
function getLimitedConstraint(type: ReverseMappedType) {
13655+
const constraint = getConstraintTypeFromMappedType(type.mappedType);
13656+
if (!(constraint.flags & TypeFlags.Union || constraint.flags & TypeFlags.Intersection)) {
13657+
return;
13658+
}
13659+
const origin = (constraint.flags & TypeFlags.Union) ? (constraint as UnionType).origin : (constraint as IntersectionType);
13660+
if (!origin || !(origin.flags & TypeFlags.Intersection)) {
13661+
return;
13662+
}
13663+
const limitedConstraint = getIntersectionType((origin as IntersectionType).types.filter(t => t !== type.constraintType));
13664+
return limitedConstraint !== neverType ? limitedConstraint : undefined;
13665+
}
13666+
1365013667
function resolveReverseMappedTypeMembers(type: ReverseMappedType) {
1365113668
const indexInfo = getIndexInfoOfType(type.source, stringType);
1365213669
const modifiers = getMappedTypeModifiers(type.mappedType);
1365313670
const readonlyMask = modifiers & MappedTypeModifiers.IncludeReadonly ? false : true;
1365413671
const optionalMask = modifiers & MappedTypeModifiers.IncludeOptional ? 0 : SymbolFlags.Optional;
1365513672
const indexInfos = indexInfo ? [createIndexInfo(stringType, inferReverseMappedType(indexInfo.type, type.mappedType, type.constraintType), readonlyMask && indexInfo.isReadonly)] : emptyArray;
1365613673
const members = createSymbolTable();
13674+
const limitedConstraint = getLimitedConstraint(type);
1365713675
for (const prop of getPropertiesOfType(type.source)) {
13676+
// In case of a reverse mapped type with an intersection constraint, if we were able to
13677+
// extract the filtering type literals we skip those properties that are not assignable to them,
13678+
// because the extra properties wouldn't get through the application of the mapped type anyway
13679+
if (limitedConstraint) {
13680+
const propertyNameType = getLiteralTypeFromProperty(prop, TypeFlags.StringOrNumberLiteralOrUnique);
13681+
if (!isTypeAssignableTo(propertyNameType, limitedConstraint)) {
13682+
continue;
13683+
}
13684+
}
1365813685
const checkFlags = CheckFlags.ReverseMapped | (readonlyMask && isReadonlySymbol(prop) ? CheckFlags.Readonly : 0);
1365913686
const inferredProp = createSymbol(SymbolFlags.Property | prop.flags & optionalMask, prop.escapedName, checkFlags) as ReverseMappedSymbol;
1366013687
inferredProp.declarations = prop.declarations;
@@ -25665,9 +25692,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2566525692
}
2566625693

2566725694
function inferToMappedType(source: Type, target: MappedType, constraintType: Type): boolean {
25668-
if (constraintType.flags & TypeFlags.Union) {
25695+
if ((constraintType.flags & TypeFlags.Union) || (constraintType.flags & TypeFlags.Intersection)) {
2566925696
let result = false;
25670-
for (const type of (constraintType as UnionType).types) {
25697+
for (const type of (constraintType as (UnionType | IntersectionType)).types) {
2567125698
result = inferToMappedType(source, target, type) || result;
2567225699
}
2567325700
return result;
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
reverseMappedTypeIntersectionConstraint.ts(19,7): error TS2322: Type '"bar"' is not assignable to type '"foo"'.
2+
reverseMappedTypeIntersectionConstraint.ts(32,3): error TS2353: Object literal may only specify known properties, and 'extra' does not exist in type '{ entry: "foo"; states: { a: { entry: "foo"; }; }; }'.
3+
reverseMappedTypeIntersectionConstraint.ts(43,3): error TS2353: Object literal may only specify known properties, and 'z' does not exist in type '{ x: number; y: "y"; }'.
4+
reverseMappedTypeIntersectionConstraint.ts(59,7): error TS2322: Type '{ [K in keyof T & keyof Stuff]: T[K]; }' is not assignable to type 'T'.
5+
'{ [K in keyof T & keyof Stuff]: T[K]; }' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'Stuff'.
6+
reverseMappedTypeIntersectionConstraint.ts(63,49): error TS2353: Object literal may only specify known properties, and 'extra' does not exist in type '{ field: 1; anotherField: "a"; }'.
7+
reverseMappedTypeIntersectionConstraint.ts(69,7): error TS2322: Type '{ [K in keyof T & keyof Stuff]: T[K]; }[]' is not assignable to type 'T[]'.
8+
Type '{ [K in keyof T & keyof Stuff]: T[K]; }' is not assignable to type 'T'.
9+
'{ [K in keyof T & keyof Stuff]: T[K]; }' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'Stuff'.
10+
reverseMappedTypeIntersectionConstraint.ts(74,36): error TS2353: Object literal may only specify known properties, and 'extra' does not exist in type '{ field: 1; anotherField: "a"; }'.
11+
reverseMappedTypeIntersectionConstraint.ts(87,12): error TS2353: Object literal may only specify known properties, and 'y' does not exist in type '{ x: 1; }'.
12+
reverseMappedTypeIntersectionConstraint.ts(98,12): error TS2353: Object literal may only specify known properties, and 'z' does not exist in type '{ x: 1; }'.
13+
reverseMappedTypeIntersectionConstraint.ts(100,22): error TS2353: Object literal may only specify known properties, and 'z' does not exist in type '{ x: 1; y: "foo"; }'.
14+
reverseMappedTypeIntersectionConstraint.ts(113,67): error TS2353: Object literal may only specify known properties, and 'extra' does not exist in type '{ prop: "foo"; nested: { prop: string; }; }'.
15+
reverseMappedTypeIntersectionConstraint.ts(152,21): error TS2585: 'Promise' only refers to a type, but is being used as a value here. Do you need to change your target library? Try changing the 'lib' compiler option to es2015 or later.
16+
reverseMappedTypeIntersectionConstraint.ts(164,3): error TS2353: Object literal may only specify known properties, and 'extra' does not exist in type '{ types: { actors: { src: "str"; logic: () => any; }; }; invoke: { readonly src: "str"; }; }'.
17+
reverseMappedTypeIntersectionConstraint.ts(171,3): error TS2353: Object literal may only specify known properties, and 'extra' does not exist in type '{ invoke: { readonly src: "whatever"; }; }'.
18+
19+
20+
==== reverseMappedTypeIntersectionConstraint.ts (14 errors) ====
21+
type StateConfig<TAction extends string> = {
22+
entry?: TAction
23+
states?: Record<string, StateConfig<TAction>>;
24+
};
25+
26+
type StateSchema = {
27+
states?: Record<string, StateSchema>;
28+
};
29+
30+
declare function createMachine<
31+
TConfig extends StateConfig<TAction>,
32+
TAction extends string = TConfig["entry"] extends string ? TConfig["entry"] : string,
33+
>(config: { [K in keyof TConfig & keyof StateConfig<any>]: TConfig[K] }): [TAction, TConfig];
34+
35+
const inferredParams1 = createMachine({
36+
entry: "foo",
37+
states: {
38+
a: {
39+
entry: "bar",
40+
~~~~~
41+
!!! error TS2322: Type '"bar"' is not assignable to type '"foo"'.
42+
!!! related TS6500 reverseMappedTypeIntersectionConstraint.ts:2:3: The expected type comes from property 'entry' which is declared here on type 'StateConfig<"foo">'
43+
},
44+
},
45+
extra: 12,
46+
});
47+
48+
const inferredParams2 = createMachine({
49+
entry: "foo",
50+
states: {
51+
a: {
52+
entry: "foo",
53+
},
54+
},
55+
extra: 12,
56+
~~~~~
57+
!!! error TS2353: Object literal may only specify known properties, and 'extra' does not exist in type '{ entry: "foo"; states: { a: { entry: "foo"; }; }; }'.
58+
});
59+
60+
61+
// -----------------------------------------------------------------------------------------
62+
63+
const checkType = <T>() => <U extends T>(value: { [K in keyof U & keyof T]: U[K] }) => value;
64+
65+
const checked = checkType<{x: number, y: string}>()({
66+
x: 1 as number,
67+
y: "y",
68+
z: "z", // undesirable property z is *not* allowed
69+
~
70+
!!! error TS2353: Object literal may only specify known properties, and 'z' does not exist in type '{ x: number; y: "y"; }'.
71+
});
72+
73+
checked;
74+
75+
// -----------------------------------------------------------------------------------------
76+
77+
interface Stuff {
78+
field: number;
79+
anotherField: string;
80+
}
81+
82+
function doStuffWithStuff<T extends Stuff>(s: { [K in keyof T & keyof Stuff]: T[K] } ): T {
83+
if(Math.random() > 0.5) {
84+
return s as T
85+
} else {
86+
return s
87+
~~~~~~
88+
!!! error TS2322: Type '{ [K in keyof T & keyof Stuff]: T[K]; }' is not assignable to type 'T'.
89+
!!! error TS2322: '{ [K in keyof T & keyof Stuff]: T[K]; }' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'Stuff'.
90+
}
91+
}
92+
93+
doStuffWithStuff({ field: 1, anotherField: 'a', extra: 123 })
94+
~~~~~
95+
!!! error TS2353: Object literal may only specify known properties, and 'extra' does not exist in type '{ field: 1; anotherField: "a"; }'.
96+
97+
function doStuffWithStuffArr<T extends Stuff>(arr: { [K in keyof T & keyof Stuff]: T[K] }[]): T[] {
98+
if(Math.random() > 0.5) {
99+
return arr as T[]
100+
} else {
101+
return arr
102+
~~~~~~
103+
!!! error TS2322: Type '{ [K in keyof T & keyof Stuff]: T[K]; }[]' is not assignable to type 'T[]'.
104+
!!! error TS2322: Type '{ [K in keyof T & keyof Stuff]: T[K]; }' is not assignable to type 'T'.
105+
!!! error TS2322: '{ [K in keyof T & keyof Stuff]: T[K]; }' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'Stuff'.
106+
}
107+
}
108+
109+
doStuffWithStuffArr([
110+
{ field: 1, anotherField: 'a', extra: 123 },
111+
~~~~~
112+
!!! error TS2353: Object literal may only specify known properties, and 'extra' does not exist in type '{ field: 1; anotherField: "a"; }'.
113+
])
114+
115+
// -----------------------------------------------------------------------------------------
116+
117+
type XNumber = { x: number }
118+
119+
declare function foo<T extends XNumber>(props: {[K in keyof T & keyof XNumber]: T[K]}): void;
120+
121+
function bar(props: {x: number, y: string}) {
122+
return foo(props); // no error because lack of excess property check by design
123+
}
124+
125+
foo({x: 1, y: 'foo'});
126+
~
127+
!!! error TS2353: Object literal may only specify known properties, and 'y' does not exist in type '{ x: 1; }'.
128+
129+
foo({...{x: 1, y: 'foo'}}); // no error because lack of excess property check by design
130+
131+
// -----------------------------------------------------------------------------------------
132+
133+
type NoErrWithOptProps = { x: number, y?: string }
134+
135+
declare function baz<T extends NoErrWithOptProps>(props: {[K in keyof T & keyof NoErrWithOptProps]: T[K]}): void;
136+
137+
baz({x: 1});
138+
baz({x: 1, z: 123});
139+
~
140+
!!! error TS2353: Object literal may only specify known properties, and 'z' does not exist in type '{ x: 1; }'.
141+
baz({x: 1, y: 'foo'});
142+
baz({x: 1, y: 'foo', z: 123});
143+
~
144+
!!! error TS2353: Object literal may only specify known properties, and 'z' does not exist in type '{ x: 1; y: "foo"; }'.
145+
146+
// -----------------------------------------------------------------------------------------
147+
148+
interface WithNestedProp {
149+
prop: string;
150+
nested: {
151+
prop: string;
152+
}
153+
}
154+
155+
declare function withNestedProp<T extends WithNestedProp>(props: {[K in keyof T & keyof WithNestedProp]: T[K]}): T;
156+
157+
const wnp = withNestedProp({prop: 'foo', nested: { prop: 'bar' }, extra: 10 });
158+
~~~~~
159+
!!! error TS2353: Object literal may only specify known properties, and 'extra' does not exist in type '{ prop: "foo"; nested: { prop: string; }; }'.
160+
161+
// -----------------------------------------------------------------------------------------
162+
163+
type IsLiteralString<T extends string> = string extends T ? false : true;
164+
165+
type DeepWritable<T> = T extends Function ? T : { -readonly [K in keyof T]: DeepWritable<T[K]> }
166+
167+
interface ProvidedActor {
168+
src: string;
169+
logic: () => Promise<unknown>;
170+
}
171+
172+
type DistributeActors<TActor> = TActor extends { src: infer TSrc }
173+
? {
174+
src: TSrc;
175+
}
176+
: never;
177+
178+
interface MachineConfig<TActor extends ProvidedActor> {
179+
types?: {
180+
actors?: TActor;
181+
};
182+
invoke: IsLiteralString<TActor["src"]> extends true
183+
? DistributeActors<TActor>
184+
: {
185+
src: string;
186+
};
187+
}
188+
189+
type NoExtra<T> = {
190+
[K in keyof T]: K extends keyof MachineConfig<any> ? T[K] : never
191+
}
192+
193+
declare function createXMachine<
194+
const TConfig extends MachineConfig<TActor>,
195+
TActor extends ProvidedActor = TConfig extends { types: { actors: ProvidedActor} } ? TConfig["types"]["actors"] : ProvidedActor,
196+
>(config: {[K in keyof MachineConfig<any> & keyof TConfig]: TConfig[K] }): TConfig;
197+
198+
const child = () => Promise.resolve("foo");
199+
~~~~~~~
200+
!!! error TS2585: 'Promise' only refers to a type, but is being used as a value here. Do you need to change your target library? Try changing the 'lib' compiler option to es2015 or later.
201+
202+
const config = createXMachine({
203+
types: {} as {
204+
actors: {
205+
src: "str";
206+
logic: typeof child;
207+
};
208+
},
209+
invoke: {
210+
src: "str",
211+
},
212+
extra: 10
213+
~~~~~
214+
!!! error TS2353: Object literal may only specify known properties, and 'extra' does not exist in type '{ types: { actors: { src: "str"; logic: () => any; }; }; invoke: { readonly src: "str"; }; }'.
215+
});
216+
217+
const config2 = createXMachine({
218+
invoke: {
219+
src: "whatever",
220+
},
221+
extra: 10
222+
~~~~~
223+
!!! error TS2353: Object literal may only specify known properties, and 'extra' does not exist in type '{ invoke: { readonly src: "whatever"; }; }'.
224+
});
225+

0 commit comments

Comments
 (0)