-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Unable to narrow union to Record in TS 4.8 #50671
Comments
Bonus attempt using conditional types, not much better though: type ExtractObject<T> =
T extends readonly unknown[] ? never
: {} extends T ? Record<string | number | symbol, unknown>
: T extends Record<string | number | symbol, unknown> ? T
: never;
function isObject<T>(it: T): it is T & ExtractObject<T> {
return Object.prototype.toString.call(it) === "[object Object]";
}
declare function test(arg: Record<string, any>): void
declare const arg: (string | number | { a: "b" });
if (isObject(arg)) {
// arg is:
// - TS 4.7: {a: "b"}
// - TS 4.8: (string | number | { a: "b" }) & { a: "b" }
test({ ...arg })
} |
Note that narrowing arrays broke in a similar fashion, but here I was able to work around it with a conditional type. Works in 4.8: type ExtractArray<T> =
T extends readonly unknown[] ? T
: {} extends T ? (T & unknown[])
: never;
export function isArray<T>(it: T): it is T & ExtractArray<T> {
if (Array.isArray != null) return Array.isArray(it);
return Object.prototype.toString.call(it) === "[object Array]";
} Used to work until 4.7: export function isArray<T>(it: T): it is T & unknown[] {
if (Array.isArray != null) return Array.isArray(it);
return Object.prototype.toString.call(it) === "[object Array]";
} |
or just
Does this work for you? |
Edit: ok, function isObject<T>(it: T): it is {} extends T
? // Narrow the `{}` type to an unspecified object
T & Record<string | number | symbol, unknown>
: unknown extends T
? // treat unknown like `{}`
T & Record<string | number | symbol, unknown>
: T extends object // function, array or actual object
? T extends readonly unknown[]
? never // not an array
: T extends (...args: any[]) => any
? never // not a function
: T // no, an actual object
: never {
// This is necessary because:
// typeof null === 'object'
// typeof [] === 'object'
// [] instanceof Object === true
return Object.prototype.toString.call(it) === "[object Object]";
} Note that this MUST be inlined or TS complains about the type possibly not being related to |
This comment was marked as off-topic.
This comment was marked as off-topic.
@AlCalzone can you provide some test cases that show why @whzx5bybβs solution of |
@andrewbranch Sure thing! Note the type in the first if-branch, and IntelliSense isn't working there either. function isObject<T>(it: T): it is T & object {
return Object.prototype.toString.call(it) === "[object Object]";
}
declare function test(arg: Record<string, any>): void
declare const arg: (() => {}) | { a: "b" } | [] | number | undefined;
if (isObject(arg)) {
// spreading works:
test({ ...arg });
arg.a; // error, shouldn't be
// type of arg1 is ([] & object) | ({a: "b"} & object) | ((() => {}) & object)
}
// β broken
//====================================================
// β works
function isObjectFixed<T>(it: T): it is {} extends T
? // Narrow the `{}` type to an unspecified object
T & Record<string | number | symbol, unknown>
: unknown extends T
? // treat unknown like `{}`
T & Record<string | number | symbol, unknown>
: T extends object // function, array or actual object
? T extends readonly unknown[]
? never // not an array
: T extends (...args: any[]) => any
? never // not a function
: T // no, an actual object
: never {
// This is necessary because:
// typeof null === 'object'
// typeof [] === 'object'
// [] instanceof Object === true
return Object.prototype.toString.call(it) === "[object Object]";
}
if (isObjectFixed(arg)) {
// spreading works:
test({ ...arg });
arg.a;
// this is now an error:
arg.concat; // concat does not exist on type {a: "b"}
} |
Sorry, had to edit this a few times, I messed up in between. |
Ah. Yeah, since functions and arrays are assignable to our |
Strangely Is it really supposed to be this complex to make sure one is dealing with an actual Record (or an actual array)? |
Whatβs an βactual Recordβ? Lots of things that are not made out of object literals have named properties that can be accessed, which is what the |
I don't disagree in an academical sense, but I also didn't see anything in this regard mentioned in the release notes, only for
Anything for which I guess what the functions above are supposed to do is distinguish those from other (structurally compatible) things without having to repeat the somewhat cumbersome check function isObject<T>(arg: unknown): arg is T & not nullish & not primitive & not array & not function; (some shortcuts taken for brevity). |
Iβm honestly shocked that the definition of type T1 = (string | number | { a: "b" }) & Record<string, unknown>; // (string | number | { a: "b" }) & Record<string, unknown>
declare let arg2: T1;
test({ ...arg2 }); // error I just inlined the intersection youβre performing in the type predicate. If you expect to get
Philosophically, this is just going to be a heavy lift, if not impossible, in a structural type system. TypeScript reasons much more about what you can do with a value than what it is. Whatβs returned by a valueβs |
Heh, I guess I just went along with what TS gave me when I wrote this function that way. That behavior was pretty stable since 3.3 (or earlier, dunno). |
@andrewbranch Technically I guess itβs not accurate to call |
Even |
This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes. |
Regardless of if this expected behaviour, it is a breaking change. Perhaps it should be rolled back and re introduced in a v5? in our case we are comparing primitives and getting this same error:
on v4.7 it worked fine but on v4.8.2 TS complains that a and b could be null on the last line. |
@alebrozzo The TypeScript project here do not use semantic versioning, so you need to consider each minor version as most likely "breaking" and not just major ... |
Bug Report
π Search Terms
union record narrow type guard
π Version & Regression Information
β― Playground Link
Playground Link
π» Code
π Actual behavior
arg
stays as some intersection type, which TS does not recognize as an object type.π Expected behavior
arg
gets narrowed to{a: "b"}
The text was updated successfully, but these errors were encountered: