#Bug with type narrowing for if check?

75 messages · Page 1 of 1 (latest)

wet coyote
#

Just wanted to confirm that this is actually a bug before I report it on github:

hushed jacinthBOT
#
_smidget#0

Preview:```ts
const record: Record<string, unknown> = {}

function foo<T extends string | false | null>(val: T) {
if (val) {
bar(val)
record[val] = 5
}
}

function bar(val: string) {}```

wet coyote
#

its strange that the bar call works but when you try to index or use it there as string it errors

#

pretty sure its beacuse its narrowing with NonNullable<T> but that doesnt exclude false

#

Seems like a very obvious oversight to me so I wanted to make sure it wasn't me missing something obvious

midnight shoal
#

lol yeah seems like a bug to me too

wet coyote
#

thought so. cool. I'll report it

#

What a strange behavior...

#

!close

midnight shoal
#

It's OK with null or undefined

#

oh I think I get what it is

#

it's a negated types issue

#

TS can't represent truthy values as a type

#

Because TS can't do unknown - false for example

#

@wet coyote

#

So it'll be a known but unfixable issue unless we get negated types

wet coyote
#

interesting. Yeah I find myself needed negated types quite a bit. Still think I should create an issue?

midnight shoal
#

There's gonna be an issue somewhere for it already

wet coyote
#

I guess I just dont understand why the compiler doesn't do:
Exclude<T, false | null | undefined | 0 | "">

ionic coral
#

but Exclude works on union members

#

string is not a union of its constituents, so Exclude<string, ""> is still string

#

you'd need negated types to represent that

hushed jacinthBOT
#
_smidget#0

Preview:```ts
const record: Record<string, unknown> = {}

function foo<T extends string | false | null>(val: T) {
if (val) {
bar(val)

const v2 = val as Exclude<
  T,
  false | null | undefined | "" | 0
>
record[v2] = 5

}
}

function bar(val: string)
...```

midnight shoal
#

Exclude<true | false, true is false, because it can build a new type false
Exclude<string, 'foo'> is string because the positive way to express this would be 'a' | 'b' | 'c' ... except 'foo' to infinity

#

And obviously TS can't general unions of infinite size 🙂

hushed jacinthBOT
#
that_guy977#0

Preview:ts declare const x: 0 | 1 if (x) { x } else { x } declare const y: "" | "a" if (y) { y } else { y } declare const z: true | false if (z) { z } else { z } declare const a: 0n | 1n if (a) { a } else { a }

midnight shoal
#

hmmmm

ionic coral
#

also boolean acts like true | false for these situations

midnight shoal
#

Yeah yknow if T is not unknown then why can't it narrow?

covert quiver
#

I'm not sure I understand the relevance of the Exclude stuff - I do think TS is in a sense correct that val being NonNullable<T> is not quite the same as string... but it does seem like there's some logic being applied on a function call to allow it that isn't being applied on the index

midnight shoal
#

Well, I think it shouldn't be NonNullable<T>

#

It should be Exclude<T, false | null | undefined | 0 | "">, but I'm wondering if it being NonNullable<T> is about lack of negated types

ionic coral
#

i think it's an issue of the else branch

covert quiver
#

Well NonNullable<T> is as much as can be expressed in the type system without relying hypothetical features like negated types, I guess that's where the tangent came from?

ionic coral
#

if you narrow completely to say that false | null are removed, leaving string, that kinda implies the else branch has false | null, without ""
ts doesn't have partial/one-sided predicates, i think this falls in the same vein

covert quiver
#

I think TS can still do one-sided if checks even if it doesn't narrow the else, though.

wet coyote
#

I was mentioning exclude because as a novice, it seems like the type representation of how i would expect it to be narrowed.
also it super strange to me that bar can still be called.

#

feel free to not explain if its complicated. probably wont understand anyways

covert quiver
#

TS can't do a one-sided custom type guard, because type-guards are always supposed to be symmetrical - but I don't think the same restriction applies to if/else and typescript's own narrowings.

hushed jacinthBOT
#
sandiford#0

Preview:```ts
const record: Record<string, unknown> = {}

function Truthy1(
v: unknown
): v is Exclude<
unknown,
false | null | undefined | "" | 0

{
return v ? true : false
}
function Truthy2(
v: unknown
): v is NonNullable<unknown> &
Exclude<unknown, false | null | undefined | "" |
...```

ionic coral
#

i think overall it's not so much a bug as a combination of limitations with the current implementation and some really specific behavior that might be hard to explain on their own

ionic coral
# hushed jacinth

NonNullable<unknown> is just {}, btw.
unknown acts like {} | null | undefined for that

hushed jacinthBOT
#
sandiford#0

Preview:```ts
const record: Record<string, unknown> = {}

function Truthy1(
v: unknown
): v is Exclude<
unknown,
false | null | undefined | "" | 0

{
return v ? true : false
}
function Truthy2(
v: unknown
): v is NonNullable<unknown> &
Exclude<unknown, false | null | undefined | "" |
...```

midnight shoal
#

this has the same type guards on T extends unknown

#

I dunno. I'd like to know the explanation. Seems to obvious not to be something that is known.

ionic coral
hushed jacinthBOT
#
function Truthy5<T>(v: T): v is NonNullable<T> {
    return (v) ? true : false
}
function foo<T>(val1: T, val2: unknown) {

    if (Truthy5(val)) {
//              ^^^
// Cannot find name 'val'. Did you mean 'val1'?
        val1
//      ^? - (parameter) val1: T
    } else {
        val1
//      ^? - (parameter) val1: T
    }

    if (Truthy5(val)) {
//              ^^^
// Cannot find name 'val'. Did you mean 'val1'?
        val2
//      ^? - (parameter) val2: unknown
    } else {
        val2
//      ^? - (parameter) val2: unknown
    }
    
    if (Truthy5(val1)) {
        val1
//      ^? - (parameter) val1: NonNullable<T>
    } else {
        val1
//      ^? - (parameter) val1: T
    }

    if (Truthy5(val2)) {
        val2
//      ^? - (parameter) val2: {}
    } else {
        val2
//      ^? - (parameter) val2: unknown
    }
}
midnight shoal
#

I definitely don't understand it

#

I would say though, I don't really like truthy checks to begin with. So maybe just don't use truthy checks?

ionic coral
#

NonNullable<T> is equivalent to T & {}

covert quiver
#

My theory here is that:

  1. NonNullable<T> is not generally considered assignable to string, even though the constraint means that, in practice, it is. (TS's type system doesn't do a lot of 'constraint solving' and I think most of it's "narrowing of generics" is just specific edge-cases built into the type checker)
  2. Enough people ran into a case like this with function calls that the TS compiler has a special case for it.
  3. Enough people have not run into a case like this for assignment that TS does not have a special case for it.
wet coyote
ionic coral
covert quiver
#

It's possible that there's something else going on here, maybe there really is something different about assignment that makes it special compared to function calls, but I wouldn't be surprised if this is just an edge case.

covert quiver
#

Right thanks

midnight shoal
#

I wouldn't mid seeing a ticket on it (new or existing as needed) to see what the experts say

#

It does feel like inner T could be your Exclude<T> or ExcludeT<> & NonNullable<T>

wet coyote
covert quiver
#

Well, I guess this works, though, so it's not just a "function calls edge case":

if (val) {
   const _val: string = val;
   record[_val] = 5;
}
#

But yeah, weird enough it's probably something that a ticket could be filed for (if there's not one already that can be found)

wet coyote
midnight shoal
#

I searched too, no luck

ionic coral
#

looks like partially it's the generic too

hushed jacinthBOT
#
that_guy977#0

Preview:```ts
function f(x: 0 | 1) {
x ? x : x
}

function g(x: "" | "a") {
x ? x : x
}

function h(x: true | false) {
x ? x : x
}

function j(x: 0n | 1n) {
x ? x : x
}

function ft<T extends 0 | 1>(x: T) {
x ? x : x
}

function gt<T extends "" | "a">(x: T)
...```

midnight shoal
#

Or is that just how TS displays the type? TS doesn't always explain all of the type info it has

wet coyote
#

Feel free to add any more relevant info that I may have missed.

wet coyote
midnight shoal
#

Yeah I saw it

#

I need to look through it in detail to understand it

#

Feels like the type should be Exclude<...> & T[K] not just T[K], but I don't really understand this area of TypeScript

#

actually relates to a problem/bug I've had recently too