Skip to content

Reverse mapped type with intersection constraint #4

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

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
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
24 changes: 22 additions & 2 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13484,14 +13484,34 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return instantiateType(instantiable, createTypeMapper([type.indexType, type.objectType], [getNumberLiteralType(0), createTupleType([replacement])]));
}

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)) {
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 @@ -25343,9 +25363,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