Generic type narrowing for truthy check has inconsistent / incorrect behavior
🔎 Search Terms
generic truthy narrowing generic index
🕗 Version & Regression Information
This is the behavior in every version I tried, and I reviewed the FAQ for entries about narrowing truthy generic types
⏯ Playground Link
https://www.typescriptlang.org/play/?ts=5.7.0-dev.20240821&ssl=29&ssc=2&pln=1&pc=1#code/MYewdgzgLgBATgU1HAJgLhgJSSVAeaOASzAHMAaGAVzAGswQB3MAPhgF4YBvAXwG4AsAChhAMxrAoRcDFEgQeACowEADygIwKCDEIlSMAD6yAhgBsICIzDBUzZlgAoAbuYyKAlN2ExfMIqIwLuZeXD5+Ea5mgkIREQD08QB6APzhcTAARiZwwWYeMRm+iMgoANpRALocMACshX48KhZWYbFFUelxialdPML9IkLiYJLSYLLyAEx5GHpk1qLmlta29qFdAUFRG+0ZUQ0ZPWl7cdm5O4dxJbjlVTX1fc0rbUUwnacJyScRg4NiEikMnOs10UGIZF2EUQUCocAmBwGQA
💻 Code
const record: Record<string, unknown> = {};
function foo<T extends string | false | null>(val: T) {
if (val) {
val;
//^?
bar(val);
record[val] = 5;
} else {
val
//^?
}
}
function foo2(val: string | false | null) {
if (val) {
val;
//^?
bar(val);
record[val] = 5;
} else {
val
//^?
}
}
function bar(val: string) {
return val;
}
🙁 Actual behavior
Error on line 8, cannot use val to index object type with string key.
Val appears to be narrowed to NonNullable<T> yet I can use it as string when calling functions?
Note: foo2 does not use generics and has more expected bahavior.
🙂 Expected behavior
type Falsy = false | null | undefined | 0 | "";
On line 5 inside of the if check, I expect val to be narrowed to Exclude<T, Falsy> but instead, is incorrectly (imo) narrowed to NonNullable<T> which does not exclude false.
What is exceptionally strange, is that the type narrowing behavior seems to differ depending on how val is used. I can pass it to the function that takes a string parameter, but when I try to use it to index an object on line 8, I get an error.
Additional information about the issue
This issue was discussed in detail by others more experienced than me in this typescript discord thread: https://discord.com/channels/508357248330760243/1275942751724372110
Invite to server if you aren't a member: https://discord.com/invite/typescript
Relates to https://github.com/microsoft/TypeScript/pull/15576 and https://github.com/microsoft/TypeScript/pull/43183 , only expressions in the element access expressions are subject to constraint-based narrowing (you'd like to narrow argument expressions here). We can find this comment in the current implementation of isConstraintPosition:
// In an element access obj[x], we consider obj to be in a constraint position, except when obj is of
// a generic type without a nullable constraint and x is a generic type. This is because when both obj
// and x are of generic types T and K, we want the resulting type to be T[K].
@Andarist not sure if you are meaning this is intended behavior, or a bug. Is there anything else I need to do to prevent this from getting buried in the 5K+ other issues?
Posting what I believe is another way of reproducing this:
const foo = {
bar: ['hello', (value: 'hello' | 'bye') => {}],
baz: [3, (value: 3 | 5) => {}],
} as const;
function doWithFoo<T extends keyof typeof foo>(key: T) {
if (key === 'bar') {
const [value, setValue] = foo[key];
setValue('bye'); // error here
}
}
https://stackblitz.com/edit/typescript-repro-eqknuz?file=src%2Fmain.ts
@Lordfirespeed This issue is specifically about type narrowing excluding false for truthy checks. e.g. if (value) {}.
You should probably post another feature request asking for narrowing of keys in generics.
To help a bit: key is not being narrowed by the if statement (feature request). if you check the type of key after the if statement its still the union of all keys on foo.
When you index it, you're getting the intersection of (value: string) => void and (value: number) => void which results to (value: never) => void because string & number => never
You get "correct" behavior if you cast foo[key as "bar"], or just use foo.bar
@brandonryan thanks for clarifying! Do you know if there's an open feature request for narrowing key? If so, please could you link it for me? Don't worry about it if you're not sure ♥️
@Lordfirespeed Think this is what youre looking for https://github.com/microsoft/TypeScript/issues/50652