Skip to content

function that returns union with void can be incorrectly narrowed #46837

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
DetachHead opened this issue Nov 17, 2021 · 11 comments
Closed

function that returns union with void can be incorrectly narrowed #46837

DetachHead opened this issue Nov 17, 2021 · 11 comments
Labels
Duplicate An existing issue was already created

Comments

@DetachHead
Copy link
Contributor

Bug Report

πŸ”Ž Search Terms

void narrow

πŸ•— Version & Regression Information

4.6.0-20211117

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

const a: (value: number) => void = (i) => i
const b: (value: number) => void | string = a

const v = b(1)
if (v) {
    v.toUpperCase() //runtime error: v.toUpperCase is not a function 
}

πŸ™ Actual behavior

no error

πŸ™‚ Expected behavior

value doesn't get narrowed. perhaps void should not be falsy because you can't be sure its value will be falsy at runtime

@MartinJohns
Copy link
Contributor

Related: #42709

@RyanCavanaugh RyanCavanaugh added In Discussion Not yet reached consensus Suggestion An idea for TypeScript labels Nov 18, 2021
@RyanCavanaugh
Copy link
Member

I think we should do some patchwork and just disallow writing void | T in --strict. It's never what you want.

@KotlinIsland
Copy link

@RyanCavanaugh based

@mdbetancourt
Copy link

I think we should do some patchwork and just disallow writing void | T in --strict. It's never what you want.

i don't think so void !== undefined

function convert<T>(t: T): void | T { // ok
  if(typeof t === 'string') {
    return t
  }
}
function convert2<T>(t: T): undefined | T { // error here "A function whose declared type is neither 'void' nor 'any' must return a value"
  if(typeof t === 'string') {
    return t
  }
}

@RyanCavanaugh
Copy link
Member

@mdbetancourt noImplicitReturns means no implicit returns. If you don't like the error in the second case, you should turn that option off.

@mdbetancourt
Copy link

mdbetancourt commented Nov 18, 2021

what about this case extending his case

const a = (i: number[]) => i.forEach(console.log)

const b: (value?: number) => void | false = (v) => v ? a([v]) : false
//                             ^ undefined or anything else is not gonna work

@RyanCavanaugh
Copy link
Member

unknown is equally correct there. We could change forEach to return undefined and then it can be undefined | false.

@mdbetancourt
Copy link

mdbetancourt commented Nov 18, 2021

unknown is equally correct there.

@RyanCavanaugh unknown dont feel right, unknown means you don't know the value but you do also it absorbs the false type it's like an any

const a = (i: number[]) => i.forEach(console.log)

const b: (value?: number) => unknown | false = (v) => v ? a([v]) : false
const value = b() // unknown
if(typeof value === 'string') {
  value // type is "string" you think this is ok?
}

const b: (value?: number) => void | false = (v) => v ? a([v]) : false
const value = b() // void | false
if(typeof value === 'string') {
  value // type is "never" much better isn't?
}

We could change forEach to return undefined and then it can be undefined | false

but that could break some codes isn't? e.g:

function voidFn() {}

const log = [].forEach as () => undefined
const h: typeof log = voidFn
//    ^ Type 'void' is not assignable to type 'undefined'

a real case could be

function altLogger(text: string) {}

// if you change forEach signature you have to change every void signature as well, then
const log = console.log as (text: string) => undefined
const h: typeof log = altLogger
//    ^ Type 'void' is not assignable to type 'undefined'

@marcospgp
Copy link

marcospgp commented May 24, 2023

what about this case extending his case

const a = (i: number[]) => i.forEach(console.log)

const b: (value?: number) => void | false = (v) => v ? a([v]) : false
//                             ^ undefined or anything else is not gonna work

I think this should error, and a function should not be able to return a union that includes void. Perhaps only in strict mode.

There already is "Not all code paths return a value. ts(7030)"

@robbiespeed
Copy link

Here's another example of the issue, this time without direct use of void | T:

function run (fn: (() => void) | (() => number)): number {
  const v = fn(); // results in void | number
  // void behaving like undefined
  if (typeof v === "object") {
    v; // never
    return -1;
  }
  if (v !== undefined) {
    return v; // TS thinks it's a number (but could really be anything)
  }

  return -1;
}

const foo = () => "hello";

const num: number = run(foo); // allowed because void allows any return value but ends up returning a string that thinks it's a number

typeof num === "string"; 

playground

I think we should do some patchwork and just disallow writing void | T in --strict. It's never what you want.

@RyanCavanaugh I don't think that will cut it. In the example above you don't need to write it, TS will infer it. I suppose you could disallow any union containing void somewhere no matter how deep, that seems complicated though and might be overly limiting. What about any time TS encounters void | T either explicitly or inferred it converts it to unknown?

@RyanCavanaugh RyanCavanaugh added Duplicate An existing issue was already created and removed Suggestion An idea for TypeScript In Discussion Not yet reached consensus labels Aug 16, 2024
@RyanCavanaugh
Copy link
Member

I agree with @robbiespeed 's analysis. Really the only "good" fix here is #42709, so let's just track this there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

7 participants