Skip to content
Open
2 changes: 1 addition & 1 deletion src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24853,7 +24853,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const templateType = getTemplateTypeFromMappedType(target);
const inference = createInferenceInfo(typeParameter);
inferTypes([inference], sourceType, templateType);
return getTypeFromInference(inference) || unknownType;
return getTypeFromInference(inference) || getBaseConstraintOfType(typeParameter) || unknownType;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getBaseConstraintOfType recursively walks all constraints - so if T extends U and U extends V and V extends number, you get number out. Which means you skip inferences to those intermediate results. I think you'd want to use getConstraintOfType, right? Because, in this case, if you have T[K] extends U and U extends number, U is a better (and more specific) answer than number.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you'd want to use getConstraintOfType, right?

Sort of. It's true it won't immediately walk all constraints but at the same time, it's not inference-aware anyway. If I call it then I end up with T[keyof T] and when relating the inferred type to the instantiated constraint (from within getInferredType) we eventually simplify this to U and to its base constraint. So to some extent, this leads to the same thing and the same problems. To incorporate inferences of other type parameters I'd have to utilize context.nonFixingMapper.

It's not immediately obvious how to get access to this from within this function (inferReverseMappedType) but that certainly can be done. A bigger problem though is that inferReverseMappedType gets called at different times for object and array/tuple types - when dealing with the latter it's called eagerly~. So it gets called right from within inferFromTypes. This introduces differences in data availability between the object and the array/tuple case.

For instance, let's take a look at this:

declare function foo<T extends U[], U extends string | number | boolean>(
  b: {
    [K in keyof T]: (arg: T[K]) => void;
  },
  a: U,
): T;

declare const data: number | string;

const result = foo([(arg) => {}, () => {}], data);

Since inferReverseMappedType gets called eagerly here to create the reverse mapped tuple type it's not possible to observe the U's inferred type. That is different from the equivalent object variant, like:

declare function foo<
  T extends Record<PropertyKey, U>,
  U extends string | number | boolean,
>(
  b: {
    [K in keyof T]: (arg: T[K]) => void;
  },
  a: U,
): T;

declare const data: number | string;

const result = foo(
  {
    a: (arg) => {},
    b: () => {},
  },
  data,
);

This isn't exactly a new problem. I've encountered this already in the past at least once or twice.

I'd really like to solve this. It feels like a separate issue though - but then: what would be the acceptable state of this PR to get this one in?

  1. does anything have to be done to move this one forward?
  2. would making inferReverseMappedType aware of the inference context (to get access to its .nonFixingMapper) be enough?
  3. should the object/array/tuple be unified for the whole thing to work in the same predictable way?

}

function* getUnmatchedProperties(source: Type, target: Type, requireOptionalProperties: boolean, matchDiscriminantProperties: boolean): IterableIterator<Symbol> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
//// [tests/cases/compiler/reverseMappedDefaultInferenceToConstraint.ts] ////

=== reverseMappedDefaultInferenceToConstraint.ts ===
// https://github.com/microsoft/TypeScript/issues/56241

interface ParameterizedObject {
>ParameterizedObject : Symbol(ParameterizedObject, Decl(reverseMappedDefaultInferenceToConstraint.ts, 0, 0))

type: string;
>type : Symbol(ParameterizedObject.type, Decl(reverseMappedDefaultInferenceToConstraint.ts, 2, 31))

params?: Record<string, unknown>;
>params : Symbol(ParameterizedObject.params, Decl(reverseMappedDefaultInferenceToConstraint.ts, 3, 15))
>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --))
}

declare function setup<
>setup : Symbol(setup, Decl(reverseMappedDefaultInferenceToConstraint.ts, 5, 1))

TContext,
>TContext : Symbol(TContext, Decl(reverseMappedDefaultInferenceToConstraint.ts, 7, 23))

TGuards extends Record<string, ParameterizedObject["params"] | undefined>,
>TGuards : Symbol(TGuards, Decl(reverseMappedDefaultInferenceToConstraint.ts, 8, 11))
>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --))
>ParameterizedObject : Symbol(ParameterizedObject, Decl(reverseMappedDefaultInferenceToConstraint.ts, 0, 0))

>(_: {
>_ : Symbol(_, Decl(reverseMappedDefaultInferenceToConstraint.ts, 10, 2))

types: {
>types : Symbol(types, Decl(reverseMappedDefaultInferenceToConstraint.ts, 10, 6))

context: TContext;
>context : Symbol(context, Decl(reverseMappedDefaultInferenceToConstraint.ts, 11, 10))
>TContext : Symbol(TContext, Decl(reverseMappedDefaultInferenceToConstraint.ts, 7, 23))

};
guards: {
>guards : Symbol(guards, Decl(reverseMappedDefaultInferenceToConstraint.ts, 13, 4))

[K in keyof TGuards]: (context: TContext, params: TGuards[K]) => void;
>K : Symbol(K, Decl(reverseMappedDefaultInferenceToConstraint.ts, 15, 5))
>TGuards : Symbol(TGuards, Decl(reverseMappedDefaultInferenceToConstraint.ts, 8, 11))
>context : Symbol(context, Decl(reverseMappedDefaultInferenceToConstraint.ts, 15, 27))
>TContext : Symbol(TContext, Decl(reverseMappedDefaultInferenceToConstraint.ts, 7, 23))
>params : Symbol(params, Decl(reverseMappedDefaultInferenceToConstraint.ts, 15, 45))
>TGuards : Symbol(TGuards, Decl(reverseMappedDefaultInferenceToConstraint.ts, 8, 11))
>K : Symbol(K, Decl(reverseMappedDefaultInferenceToConstraint.ts, 15, 5))

};
}): TGuards;
>TGuards : Symbol(TGuards, Decl(reverseMappedDefaultInferenceToConstraint.ts, 8, 11))

const result = setup({
>result : Symbol(result, Decl(reverseMappedDefaultInferenceToConstraint.ts, 19, 5))
>setup : Symbol(setup, Decl(reverseMappedDefaultInferenceToConstraint.ts, 5, 1))

types: {
>types : Symbol(types, Decl(reverseMappedDefaultInferenceToConstraint.ts, 19, 22))

context: {
>context : Symbol(context, Decl(reverseMappedDefaultInferenceToConstraint.ts, 20, 10))

count: 100,
>count : Symbol(count, Decl(reverseMappedDefaultInferenceToConstraint.ts, 21, 14))

},
},
guards: {
>guards : Symbol(guards, Decl(reverseMappedDefaultInferenceToConstraint.ts, 24, 4))

checkFoo: (_, { foo }: { foo: string }) => foo === "foo",
>checkFoo : Symbol(checkFoo, Decl(reverseMappedDefaultInferenceToConstraint.ts, 25, 11))
>_ : Symbol(_, Decl(reverseMappedDefaultInferenceToConstraint.ts, 26, 15))
>foo : Symbol(foo, Decl(reverseMappedDefaultInferenceToConstraint.ts, 26, 19))
>foo : Symbol(foo, Decl(reverseMappedDefaultInferenceToConstraint.ts, 26, 28))
>foo : Symbol(foo, Decl(reverseMappedDefaultInferenceToConstraint.ts, 26, 19))

alwaysTrue: (_) => true,
>alwaysTrue : Symbol(alwaysTrue, Decl(reverseMappedDefaultInferenceToConstraint.ts, 26, 61))
>_ : Symbol(_, Decl(reverseMappedDefaultInferenceToConstraint.ts, 27, 17))

},
});

Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//// [tests/cases/compiler/reverseMappedDefaultInferenceToConstraint.ts] ////

=== reverseMappedDefaultInferenceToConstraint.ts ===
// https://github.com/microsoft/TypeScript/issues/56241

interface ParameterizedObject {
type: string;
>type : string

params?: Record<string, unknown>;
>params : Record<string, unknown> | undefined
}

declare function setup<
>setup : <TContext, TGuards extends Record<string, Record<string, unknown> | undefined>>(_: { types: { context: TContext;}; guards: { [K in keyof TGuards]: (context: TContext, params: TGuards[K]) => void; }; }) => TGuards

TContext,
TGuards extends Record<string, ParameterizedObject["params"] | undefined>,
>(_: {
>_ : { types: { context: TContext;}; guards: { [K in keyof TGuards]: (context: TContext, params: TGuards[K]) => void; }; }

types: {
>types : { context: TContext; }

context: TContext;
>context : TContext

};
guards: {
>guards : { [K in keyof TGuards]: (context: TContext, params: TGuards[K]) => void; }

[K in keyof TGuards]: (context: TContext, params: TGuards[K]) => void;
>context : TContext
>params : TGuards[K]

};
}): TGuards;

const result = setup({
>result : { checkFoo: { foo: string; }; alwaysTrue: Record<string, unknown> | undefined; }
>setup({ types: { context: { count: 100, }, }, guards: { checkFoo: (_, { foo }: { foo: string }) => foo === "foo", alwaysTrue: (_) => true, },}) : { checkFoo: { foo: string; }; alwaysTrue: Record<string, unknown> | undefined; }
>setup : <TContext, TGuards extends Record<string, Record<string, unknown> | undefined>>(_: { types: { context: TContext; }; guards: { [K in keyof TGuards]: (context: TContext, params: TGuards[K]) => void; }; }) => TGuards
>{ types: { context: { count: 100, }, }, guards: { checkFoo: (_, { foo }: { foo: string }) => foo === "foo", alwaysTrue: (_) => true, },} : { types: { context: { count: number; }; }; guards: { checkFoo: (_: { count: number; }, { foo }: { foo: string; }) => boolean; alwaysTrue: (_: { count: number; }) => boolean; }; }

types: {
>types : { context: { count: number; }; }
>{ context: { count: 100, }, } : { context: { count: number; }; }

context: {
>context : { count: number; }
>{ count: 100, } : { count: number; }

count: 100,
>count : number
>100 : 100

},
},
guards: {
>guards : { checkFoo: (_: { count: number; }, { foo }: { foo: string; }) => boolean; alwaysTrue: (_: { count: number; }) => boolean; }
>{ checkFoo: (_, { foo }: { foo: string }) => foo === "foo", alwaysTrue: (_) => true, } : { checkFoo: (_: { count: number; }, { foo }: { foo: string; }) => boolean; alwaysTrue: (_: { count: number; }) => boolean; }

checkFoo: (_, { foo }: { foo: string }) => foo === "foo",
>checkFoo : (_: { count: number; }, { foo }: { foo: string; }) => boolean
>(_, { foo }: { foo: string }) => foo === "foo" : (_: { count: number; }, { foo }: { foo: string; }) => boolean
>_ : { count: number; }
>foo : string
>foo : string
>foo === "foo" : boolean
>foo : string
>"foo" : "foo"

alwaysTrue: (_) => true,
>alwaysTrue : (_: { count: number; }) => boolean
>(_) => true : (_: { count: number; }) => boolean
>_ : { count: number; }
>true : true

},
});

33 changes: 33 additions & 0 deletions tests/cases/compiler/reverseMappedDefaultInferenceToConstraint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// @strict: true
// @noEmit: true

// https://github.com/microsoft/TypeScript/issues/56241

interface ParameterizedObject {
type: string;
params?: Record<string, unknown>;
}

declare function setup<
TContext,
TGuards extends Record<string, ParameterizedObject["params"] | undefined>,
>(_: {
types: {
context: TContext;
};
guards: {
[K in keyof TGuards]: (context: TContext, params: TGuards[K]) => void;
};
}): TGuards;

const result = setup({
types: {
context: {
count: 100,
},
},
guards: {
checkFoo: (_, { foo }: { foo: string }) => foo === "foo",
alwaysTrue: (_) => true,
},
});