Skip to content

Inferred type of generic parameter wrong in complex case involving mapped types #33568

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
aslatter opened this issue Sep 23, 2019 · 5 comments · Fixed by #35199
Closed

Inferred type of generic parameter wrong in complex case involving mapped types #33568

aslatter opened this issue Sep 23, 2019 · 5 comments · Fixed by #35199
Assignees
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue

Comments

@aslatter
Copy link

TypeScript Version: 3.5.1 (also nightly, per playground)

Search Terms: mapped type interface generic parameter

Code

Sorry for the size of the repro. In TypeScript 3.3.3 this example-code type-checked without errors. As of (at least?) 3.5.1 and later I get the error:

function save(_response: IResponse<string>): void
Argument of type '(_response: IResponse<string>) => void' is not assignable to parameter of type 'IExportCallback<unknown>'.
  Types of parameters '_response' and 'response' are incompatible.
    Type 'IResponse<unknown>' is not assignable to type 'IResponse<string>'.
      Type 'unknown' is not assignable to type 'string'.
export function save(_response: IRootResponse<string>): void {}

exportCommand(save);

declare function exportCommand<TResponse>(functionToCall: IExportCallback<TResponse>): void;

interface IExportCallback<TResponse> {
	(response: IRootResponse<TResponse>): void;
}

type IRootResponse<TResponse> =
	TResponse extends IRecord ? IRecordResponse<TResponse> : IResponse<TResponse>;

interface IRecord {
	readonly Id: string;
}

declare type IRecordResponse<T extends IRecord> = IResponse<T> & {
	sendRecord(): void;
};

declare type IResponse<T> = {
	sendValue(name: keyof GetAllPropertiesOfType<T, string>): void;
};

/**
 * Get the list of property names on type T that are of a given type
 */
declare type GetPropertyNamesOfType<T, RestrictToType> = {
	[PropertyName in Extract<keyof T, string>]: T[PropertyName] extends RestrictToType ? PropertyName : never
}[Extract<keyof T, string>];
/**
 * Get all the properties on type T that are of a given type
 */
declare type GetAllPropertiesOfType<T, RestrictToType> = Pick<
	T,
	GetPropertyNamesOfType<Required<T>, RestrictToType>
>;

Expected behavior:

The example program to type-check.

Actual behavior:

Example program does not type-check.

We can work-around this by supplying type-parameters to the invocation of exportCommand, however the (non-simplified) version of this bug comes up multiple times in our code-base, and it would be nice to infer the type-parameters from the passed-in callback (like we could prior to v3.5).

For the work-around, change the invocation of exportCommand exportCommand<string>(save); (from exportCommand(save);).

Playground Link:

Related Issues:

The auto-issue searcher thing in GitHub found issues that sounded similar but are listed as affecting versions prior to 3.3 (where this code worked):

@aslatter
Copy link
Author

What makes this bug more painful for us is that our real case involves a function with multiple type-parameters, the others of which are inferred correctly (and there's no way to skip type-parameter inference for only some type-parameters).

@aslatter
Copy link
Author

This bug is related to the same infrastructure as #32608, but I don't think the bugs are actually related. We're just using enough "features" to be sensitive to type-checker changes, I guess.

@Nathan-Fenner
Copy link
Contributor

I was able to get it to compile without errors by just changing the type for IRootResponse:

type IRootResponse<TResponse> =
	(TResponse extends IRecord ? { sendRecord:() => void } : unknown) & IResponse<TResponse>;

It's not clear to me whether this solves your problem in general, though. It seems to mee that this should be an equivalent definition, but it seems to make the typechecker happier.

@aslatter
Copy link
Author

Thanks @Nathan-Fenner - that work-around seems to work great in TypeScript 3.6.

@ahejlsberg
Copy link
Member

This is caused by #30287. Simpler repro:

declare function ff(x: Foo<string>): void;
declare function gg<T>(f: (x: Foo<T>) => void): void;
type Foo<T> = T extends number ? { n: T } : { x: T };
gg(ff);  // Error

Above we make no inferences for T because it occurs in a conditional type in a contravariant position. As mentioned in #30287 we were trying to avoid (yet another) inference priority level, but I guess we now have an example that needs one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants