#Bug with type narrowing for if check?
75 messages · Page 1 of 1 (latest)
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) {}```
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
lol yeah seems like a bug to me too
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
interesting. Yeah I find myself needed negated types quite a bit. Still think I should create an issue?
There's gonna be an issue somewhere for it already
I guess I just dont understand why the compiler doesn't do:
Exclude<T, false | null | undefined | 0 | "">
it does do that
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
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)
...```
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 🙂
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 }
hmmmm
also boolean acts like true | false for these situations
Yeah yknow if T is not unknown then why can't it narrow?
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
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
i think it's an issue of the else branch
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?
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
I think TS can still do one-sided if checks even if it doesn't narrow the else, though.
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
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.
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 | "" |
...```
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
NonNullable<unknown> is just {}, btw.
unknown acts like {} | null | undefined for that
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 | "" |
...```
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.
(@midnight shoal in case you missed it)
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
}
}
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?
NonNullable<T> is equivalent to T & {}
My theory here is that:
NonNullable<T>is not generally considered assignable tostring, 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)- Enough people ran into a case like this with function calls that the TS compiler has a special case for it.
- Enough people have not run into a case like this for assignment that TS does not have a special case for it.
its useful for short circuting:
test(condition && value);
trying to type an existing api
bruh why not just condition && test(value) for that
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.
(NonNullable for 1?)
Right thanks
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>
its a method on a builder class, each method call changes the resulting type. Its for building OData queries.
If you really want to dig into it:
https://github.com/odata2ts/odata2ts/issues/107
My example above resulted in my trying to implement the "fluent" pattern the author was talking about. Then I ran into this.
I absolutely have a workaround, just wanted to investigate
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)
I looked.. just dont really have the vocabulary to properly search honestly
I searched too, no luck
looks like partially it's the generic too
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)
...```
yeah
Or is that just how TS displays the type? TS doesn't always explain all of the type info it has
@midnight shoal here is the explanation for the current behavior:
https://github.com/microsoft/TypeScript/issues/59717#issuecomment-2305128613