Skip to content
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

Reverse mapped types with intersection constraint #55811

Merged
31 changes: 29 additions & 2 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13647,14 +13647,41 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return instantiateType(instantiable, createTypeMapper([type.indexType, type.objectType], [getNumberLiteralType(0), createTupleType([replacement])]));
}

// If the original mapped type had an intersection constraint we extract its components,
// and we make an attempt to do so even if the intersection has been reduced to a union.
// This entire process allows us to possibly retrieve the filtering type literals.
// e.g. { [K in keyof U & ("a" | "b") ] } -> "a" | "b"
function getLimitedConstraint(type: ReverseMappedType) {
const constraint = getConstraintTypeFromMappedType(type.mappedType);
if (!(constraint.flags & TypeFlags.Union || constraint.flags & TypeFlags.Intersection)) {
return;
}
const origin = (constraint.flags & TypeFlags.Union) ? (constraint as UnionType).origin : (constraint as IntersectionType);
if (!origin || !(origin.flags & TypeFlags.Intersection)) {
return;
}
const limitedConstraint = getIntersectionType((origin as IntersectionType).types.filter(t => t !== type.constraintType));
return limitedConstraint !== neverType ? limitedConstraint : undefined;
}

function resolveReverseMappedTypeMembers(type: ReverseMappedType) {
const indexInfo = getIndexInfoOfType(type.source, stringType);
const modifiers = getMappedTypeModifiers(type.mappedType);
const readonlyMask = modifiers & MappedTypeModifiers.IncludeReadonly ? false : true;
const optionalMask = modifiers & MappedTypeModifiers.IncludeOptional ? 0 : SymbolFlags.Optional;
const indexInfos = indexInfo ? [createIndexInfo(stringType, inferReverseMappedType(indexInfo.type, type.mappedType, type.constraintType), readonlyMask && indexInfo.isReadonly)] : emptyArray;
const members = createSymbolTable();
const limitedConstraint = getLimitedConstraint(type);
for (const prop of getPropertiesOfType(type.source)) {
// In case of a reverse mapped type with an intersection constraint, if we were able to
// extract the filtering type literals we skip those properties that are not assignable to them,
// because the extra properties wouldn't get through the application of the mapped type anyway
if (limitedConstraint) {
const propertyNameType = getLiteralTypeFromProperty(prop, TypeFlags.StringOrNumberLiteralOrUnique);
if (!isTypeAssignableTo(propertyNameType, limitedConstraint)) {
continue;
}
}
const checkFlags = CheckFlags.ReverseMapped | (readonlyMask && isReadonlySymbol(prop) ? CheckFlags.Readonly : 0);
const inferredProp = createSymbol(SymbolFlags.Property | prop.flags & optionalMask, prop.escapedName, checkFlags) as ReverseMappedSymbol;
inferredProp.declarations = prop.declarations;
Expand Down Expand Up @@ -25665,9 +25692,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

function inferToMappedType(source: Type, target: MappedType, constraintType: Type): boolean {
if (constraintType.flags & TypeFlags.Union) {
if ((constraintType.flags & TypeFlags.Union) || (constraintType.flags & TypeFlags.Intersection)) {
let result = false;
for (const type of (constraintType as UnionType).types) {
for (const type of (constraintType as (UnionType | IntersectionType)).types) {
result = inferToMappedType(source, target, type) || result;
}
return result;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
reverseMappedTypeIntersectionConstraint.ts(19,7): error TS2322: Type '"bar"' is not assignable to type '"foo"'.
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"; }; }; }'.
reverseMappedTypeIntersectionConstraint.ts(43,3): error TS2353: Object literal may only specify known properties, and 'z' does not exist in type '{ x: number; y: "y"; }'.
reverseMappedTypeIntersectionConstraint.ts(59,7): error TS2322: Type '{ [K in keyof T & keyof Stuff]: T[K]; }' is not assignable to type 'T'.
'{ [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'.
reverseMappedTypeIntersectionConstraint.ts(63,49): error TS2353: Object literal may only specify known properties, and 'extra' does not exist in type '{ field: 1; anotherField: "a"; }'.
reverseMappedTypeIntersectionConstraint.ts(69,7): error TS2322: Type '{ [K in keyof T & keyof Stuff]: T[K]; }[]' is not assignable to type 'T[]'.
Type '{ [K in keyof T & keyof Stuff]: T[K]; }' is not assignable to type 'T'.
'{ [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'.
reverseMappedTypeIntersectionConstraint.ts(74,36): error TS2353: Object literal may only specify known properties, and 'extra' does not exist in type '{ field: 1; anotherField: "a"; }'.
reverseMappedTypeIntersectionConstraint.ts(87,12): error TS2353: Object literal may only specify known properties, and 'y' does not exist in type '{ x: 1; }'.
reverseMappedTypeIntersectionConstraint.ts(98,12): error TS2353: Object literal may only specify known properties, and 'z' does not exist in type '{ x: 1; }'.
reverseMappedTypeIntersectionConstraint.ts(100,22): error TS2353: Object literal may only specify known properties, and 'z' does not exist in type '{ x: 1; y: "foo"; }'.
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; }; }'.
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.
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"; }; }'.
reverseMappedTypeIntersectionConstraint.ts(171,3): error TS2353: Object literal may only specify known properties, and 'extra' does not exist in type '{ invoke: { readonly src: "whatever"; }; }'.


==== reverseMappedTypeIntersectionConstraint.ts (14 errors) ====
type StateConfig<TAction extends string> = {
entry?: TAction
states?: Record<string, StateConfig<TAction>>;
};

type StateSchema = {
states?: Record<string, StateSchema>;
};

declare function createMachine<
TConfig extends StateConfig<TAction>,
TAction extends string = TConfig["entry"] extends string ? TConfig["entry"] : string,
>(config: { [K in keyof TConfig & keyof StateConfig<any>]: TConfig[K] }): [TAction, TConfig];

const inferredParams1 = createMachine({
entry: "foo",
states: {
a: {
entry: "bar",
~~~~~
!!! error TS2322: Type '"bar"' is not assignable to type '"foo"'.
!!! related TS6500 reverseMappedTypeIntersectionConstraint.ts:2:3: The expected type comes from property 'entry' which is declared here on type 'StateConfig<"foo">'
},
},
extra: 12,
});

const inferredParams2 = createMachine({
entry: "foo",
states: {
a: {
entry: "foo",
},
},
extra: 12,
~~~~~
!!! error TS2353: Object literal may only specify known properties, and 'extra' does not exist in type '{ entry: "foo"; states: { a: { entry: "foo"; }; }; }'.
});


// -----------------------------------------------------------------------------------------

const checkType = <T>() => <U extends T>(value: { [K in keyof U & keyof T]: U[K] }) => value;

const checked = checkType<{x: number, y: string}>()({
x: 1 as number,
y: "y",
z: "z", // undesirable property z is *not* allowed
~
!!! error TS2353: Object literal may only specify known properties, and 'z' does not exist in type '{ x: number; y: "y"; }'.
});

checked;

// -----------------------------------------------------------------------------------------

interface Stuff {
field: number;
anotherField: string;
}

function doStuffWithStuff<T extends Stuff>(s: { [K in keyof T & keyof Stuff]: T[K] } ): T {
if(Math.random() > 0.5) {
return s as T
} else {
return s
~~~~~~
!!! error TS2322: Type '{ [K in keyof T & keyof Stuff]: T[K]; }' is not assignable to type 'T'.
!!! 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'.
}
}

doStuffWithStuff({ field: 1, anotherField: 'a', extra: 123 })
~~~~~
!!! error TS2353: Object literal may only specify known properties, and 'extra' does not exist in type '{ field: 1; anotherField: "a"; }'.

function doStuffWithStuffArr<T extends Stuff>(arr: { [K in keyof T & keyof Stuff]: T[K] }[]): T[] {
if(Math.random() > 0.5) {
return arr as T[]
} else {
return arr
~~~~~~
!!! error TS2322: Type '{ [K in keyof T & keyof Stuff]: T[K]; }[]' is not assignable to type 'T[]'.
!!! error TS2322: Type '{ [K in keyof T & keyof Stuff]: T[K]; }' is not assignable to type 'T'.
!!! 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'.
}
}

doStuffWithStuffArr([
{ field: 1, anotherField: 'a', extra: 123 },
~~~~~
!!! error TS2353: Object literal may only specify known properties, and 'extra' does not exist in type '{ field: 1; anotherField: "a"; }'.
])

// -----------------------------------------------------------------------------------------

type XNumber = { x: number }

declare function foo<T extends XNumber>(props: {[K in keyof T & keyof XNumber]: T[K]}): void;

function bar(props: {x: number, y: string}) {
return foo(props); // no error because lack of excess property check by design
}

foo({x: 1, y: 'foo'});
~
!!! error TS2353: Object literal may only specify known properties, and 'y' does not exist in type '{ x: 1; }'.

foo({...{x: 1, y: 'foo'}}); // no error because lack of excess property check by design

// -----------------------------------------------------------------------------------------

type NoErrWithOptProps = { x: number, y?: string }

declare function baz<T extends NoErrWithOptProps>(props: {[K in keyof T & keyof NoErrWithOptProps]: T[K]}): void;

baz({x: 1});
baz({x: 1, z: 123});
~
!!! error TS2353: Object literal may only specify known properties, and 'z' does not exist in type '{ x: 1; }'.
baz({x: 1, y: 'foo'});
baz({x: 1, y: 'foo', z: 123});
~
!!! error TS2353: Object literal may only specify known properties, and 'z' does not exist in type '{ x: 1; y: "foo"; }'.

// -----------------------------------------------------------------------------------------

interface WithNestedProp {
prop: string;
nested: {
prop: string;
}
}

declare function withNestedProp<T extends WithNestedProp>(props: {[K in keyof T & keyof WithNestedProp]: T[K]}): T;

const wnp = withNestedProp({prop: 'foo', nested: { prop: 'bar' }, extra: 10 });
~~~~~
!!! error TS2353: Object literal may only specify known properties, and 'extra' does not exist in type '{ prop: "foo"; nested: { prop: string; }; }'.

// -----------------------------------------------------------------------------------------

type IsLiteralString<T extends string> = string extends T ? false : true;

type DeepWritable<T> = T extends Function ? T : { -readonly [K in keyof T]: DeepWritable<T[K]> }

interface ProvidedActor {
src: string;
logic: () => Promise<unknown>;
}

type DistributeActors<TActor> = TActor extends { src: infer TSrc }
? {
src: TSrc;
}
: never;

interface MachineConfig<TActor extends ProvidedActor> {
types?: {
actors?: TActor;
};
invoke: IsLiteralString<TActor["src"]> extends true
? DistributeActors<TActor>
: {
src: string;
};
}

type NoExtra<T> = {
[K in keyof T]: K extends keyof MachineConfig<any> ? T[K] : never
}

declare function createXMachine<
const TConfig extends MachineConfig<TActor>,
TActor extends ProvidedActor = TConfig extends { types: { actors: ProvidedActor} } ? TConfig["types"]["actors"] : ProvidedActor,
>(config: {[K in keyof MachineConfig<any> & keyof TConfig]: TConfig[K] }): TConfig;

const child = () => Promise.resolve("foo");
~~~~~~~
!!! 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.

const config = createXMachine({
types: {} as {
actors: {
src: "str";
logic: typeof child;
};
},
invoke: {
src: "str",
},
extra: 10
~~~~~
!!! 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"; }; }'.
});

const config2 = createXMachine({
invoke: {
src: "whatever",
},
extra: 10
~~~~~
!!! error TS2353: Object literal may only specify known properties, and 'extra' does not exist in type '{ invoke: { readonly src: "whatever"; }; }'.
});

Loading