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

Inferred return type is incomplete #43331

Closed
jakubnavratil opened this issue Mar 21, 2021 · 9 comments
Closed

Inferred return type is incomplete #43331

jakubnavratil opened this issue Mar 21, 2021 · 9 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@jakubnavratil
Copy link

jakubnavratil commented Mar 21, 2021

Bug Report

πŸ”Ž Search Terms

Infer, union, return type

πŸ•— Version & Regression Information

  • Tested on 3.8.2 - 4.2.3

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

type TestA = { foo: boolean };
type TestB = { foo: boolean; bar: number }; // diffrence in exta prop

const fn = (val1: TestA, val2: TestB) => {
  if (Math.random() > 0.5) {
    return val1;
  }

  return val2;
};

πŸ™ Actual behavior

fn return type is TestA

πŸ™‚ Expected behavior

fn return type is TestA | TestB

This happens when TestB have some similarity with TestA. If I remove foo from TestB then it works ok.
This is simplified example. What I want is union of all posible types.

Or is there other way how to extract that union type?

@jakubnavratil jakubnavratil changed the title Infering from function return type missing type from union Infering from function return type causes missing type from union Mar 21, 2021
@jakubnavratil jakubnavratil changed the title Infering from function return type causes missing type from union Inferred return type is incomplete Mar 21, 2021
@ritschwumm
Copy link

well, TestA and TestA|TestB are completely equivalent here, right?

@jakubnavratil
Copy link
Author

well, TestA and TestA|TestB are completely equivalent here, right?

Nope, TestB has extra property bar.

@MartinJohns
Copy link
Contributor

See #35499 / #32237. Used search terms: return type inferred property union

@jakubnavratil
Copy link
Author

That's a bummer. Is there a way around againts this subtype reduction? Is it even corrent to reduce this?
My types are user generated and after executing fn they loose all that or get inconsistent results.

@MartinJohns
Copy link
Contributor

Is there a way around againts this subtype reduction?

Don't use inference and instead provide a type annotation for the return type.

Is it even corrent to reduce this?

Sure, it is. Both returned objects fulfill the requirements of TestA. Additional properties don't matter much, given that TypeScripts type-system is structurally typed.

@jakubnavratil
Copy link
Author

Is there a way around againts this subtype reduction?

Don't use inference and instead provide a type annotation for the return type.

I can't, its all generics and auto generated. This is just simplified version.
Consider:

declare function randomData<R>(generator: () => R): R;

const res = randomData(() => {
  if (Math.random() > 0.5) {
    return (null as unknown) as DataA;
  } else if (Math.random() > 0.2) {
    return (null as unknown) as DataB;
  } else {
    return (null as unknown) as DataC;
  }
});

Here user provides data and I don't know types in advance (also simplified version). User actually can return something like Container<T> where T is data I care about. Result of this is passed to other methods, where user should get type of data he can expect.
With this subtype reduction thing, user actually dont know he will get bar property too.

@jakubnavratil
Copy link
Author

For example something like this is similar what I need to do:

interface Response<T> {
  data: T;
}

interface SimpleData {
  foo: boolean;
}
interface FullData {
  foo: boolean;
  bar: number;
  other: string;
}

declare function fetchSimple(): Response<SimpleData>;
declare function fetchFull(): Response<FullData>;

declare function fetchAndExtract<R extends Response<any>>(fetcher: () => R): R extends Response<infer T> ? T : never;
const data = fetchAndExtract(() => { // user provide inner callback
  if (Math.random() > 0.5) {
    return fetchSimple(); // user provide this fn
  }

  return fetchFull(); // user provide this fn
});

// typeof data = SimpleData ... instead of SimpleData | FullData

So this kind of thing is not possible?

@RyanCavanaugh RyanCavanaugh added the Question An issue which isn't directly actionable in code label Mar 22, 2021
@jonmellman
Copy link

+1, this is confusing behavior. It seems incorrect to reduce here, since that erases extra properties that the developer may need.

Here's my example showing it results in unexpected compiler errors (playground):

type Foo = {
  isFoo: true
}

type SpecialFoo = Foo & {
  isSpecialFoo: true
}

type fooMap = {
  [fooId: string]: Foo | SpecialFoo
}

const getFooById = (fooMap: fooMap, fooId: string) => { // inferred return type: Foo | null
  if (Math.random()) {
    return null
  }

  const foo = fooMap[fooId]
  return foo // Foo | SpecialFoo
}

const getSpecialFoo = (state: fooMap, fooId: string) => {
  const foo = getFooById(state, fooId) // Foo | null

  if (!foo) {
    return null
  }

  if (!('isSpecialFoo' in foo)) {
    return null
  }

  return foo.isSpecialFoo // Property 'isSpecialFoo' does not exist on type 'never'.ts(2339)
}

@typescript-bot
Copy link
Collaborator

This issue has been marked as 'Question' and has seen no recent activity. It has been automatically closed for house-keeping purposes. If you're still waiting on a response, questions are usually better suited to stackoverflow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

6 participants