Skip to content

Type inference in generic return type #18839

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
Dante-101 opened this issue Sep 29, 2017 · 7 comments
Closed

Type inference in generic return type #18839

Dante-101 opened this issue Sep 29, 2017 · 7 comments
Labels
Duplicate An existing issue was already created

Comments

@Dante-101
Copy link

Dante-101 commented Sep 29, 2017

TypeScript Version: 2.5.3
(Tested at https://www.typescriptlang.org/play/)

Looks like Typescript is not correctly evaluating the generic return type where all the fields are optional.

Here is the minimal case.
Code

interface Base { id: string }
type Spec<T extends Base> = { [K in keyof T]?: T[K] }
export const makeSpec = <T extends Base>(id: string): Spec<T> => {
    return { id }
}

Expected behavior:
Should compile

Actual behavior:
It throws an error for second last line:
Type '{ id: string; }' is not assignable to type 'Spec<T>'.

The compiler doesn't error on this:

interface A extends Base { name: string }
const spec: Spec<A> = { id: 'test' }

I feel it is a bug but I may be misunderstanding something.

@Dante-101 Dante-101 changed the title Generic inference in a return type Type inference in generic return type Sep 29, 2017
@ghost
Copy link

ghost commented Sep 29, 2017

It looks like you're right; we do allow this for any individual type extending Base, but we don't allow it when you use a type parameter.

interface Base { id: string }
interface Sub extends Base { other: number }
export const makeSpec = (id: string): { [K in keyof Sub]?: Sub[K] } => {
    return { id } // Works...
}

@jcalz
Copy link
Contributor

jcalz commented Sep 29, 2017

Consider:

interface LiteralBase extends Base { id: "literal" }
const spec: Spec<LiteralBase> = makeSpec("whoops"); // no error!!

Your makeSpec() implementation isn't safe because you are promising to return a Spec<T> for any T that extends Base, including things like LiteralBase where the type of id is narrower than string. And you can't do that with a parameter of type string.

You could presumably do this:

export const makeSpec = <T extends Base>(id: T['id']): Spec<T> => {
    return { id } // still error
}
const spec: Spec<LiteralBase> = makeSpec("whoops"); // error, "whoops" is not compatible

which at least avoids the unsafeness of calling makeSpec(), but TypeScript still doesn't understand that {id: T['id']} is assignable to Spec<T> or Partial<T> in the implementation. The workaround is obvious:

export const makeSpec = <T extends Base>(id: T['id']): Spec<T> => {
    return { id } as Partial<T> // or Spec<T>
}

Not sure if the need for the assertion is a bug, design limitation, or some actual safety measure I'm overlooking.

@ghost
Copy link

ghost commented Sep 29, 2017

In your situation, you could also just describe exactly what the function does without involving Base, and then assign the output to whatever you want:

interface Base { id: string }
interface Sub extends Base { id: "sub", other: number }
export const makeSpec = <T extends string>(id: T): { id: T } => {
    return { id }
}
const s: Partial<Sub> = makeSpec("sub");

@Dante-101
Copy link
Author

Thanks, @jcalz for providing one possible edge case of string literals. I have been using the workaround to force the type but couldn't understand typescript's behaviour.

I use a complex, long chain of interfaces so not involving Base is impossible. I just wrote the smallest possible code to highlight the issue. Let's see what other members of TS team have to say.

@Dante-101
Copy link
Author

Dante-101 commented Sep 30, 2017

I found one more error case which may be manifested because of the same issue in typescript.

type Diff<T extends string, U extends string> = ({[P in T]: P } & {[P in U]: never } & { [x: string]: never })[T]
type Omit<T, K extends keyof T> = {[P in Diff<keyof T, K>]: T[P]}

interface Base { id: string, val: number }
const makeSpec = <T extends Base>(valObj: Omit<T, "id">) => {
    const value = valObj.val
    const valObj2: Omit<T, "id"> = { val: 456 }
}

It errors on third last line with the error Property 'val' does not exist on type 'Omit<T, "id">'.
and on the second last line with the error Type '{ val: number; }' is not assignable to type 'Omit<T, "id">'.

But this one complies fine

interface A extends Base { }
const valObj: Omit<A, "id"> = { val: 123 }

Can someone from TS team weigh in on how to resolve this one? Even being able to compile without removing Base and Omit from makeSpec will work for now.

@mhegazy
Copy link
Contributor

mhegazy commented Oct 31, 2017

Duplicate of #19388, #16356 and #12799

@mhegazy mhegazy added the Duplicate An existing issue was already created label Oct 31, 2017
@typescript-bot
Copy link
Collaborator

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

@microsoft microsoft locked and limited conversation to collaborators Jun 14, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

4 participants