Skip to content

Custom type guard fails for supertype in union type #61597

Closed
@otomad

Description

@otomad

🔎 Search Terms

"type guard", "is object", "not function", "negated types", "union type", "supertype", "exclude"

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about Exclude Isn't Type Negation, Primitives are { }, and { } Doesn't Mean object, Negated types

⏯ Playground Link

https://www.typescriptlang.org/play/?ts=5.9.0-dev.20250420#code/C4TwDgpgBAcg9sAYgVwHYGNgEs6oDwAqAfFALxQFQQAewEqAJgM5Qoba5QD8UqEAbhABOUAFwUA3ACgpAMzSYcqKFiYB5AEYArCJgAyWANYRCRABT8AhgBtkELuIIBKcVdvRVsBG0W5TUAHoAKigAMig4bV1gKCCAqABvKSgUqCEIYGQhZTc7KABCUnJUZGtrMPDQSDhZKFzoIvIAIkidTCbpAF8ZBl1rS3SodFwmGMtxMzMnMhJ+OCwGaYAfRKhZODhxUaEsVABzKE7pKugAQTIvJAUOfBOaqEsiCUD4hLWNreAd-cOpLFqzKpNG1gAZjGZLE4nMlUpZngFXu9NlBtrsDt0INYmBAYSk4S8oFMZnV5gwen0BtBhqhRlANBMiaRZqTlokjlITlAAEIXeBXdhKPB3WoaJ4EhLdf6EoFRfRGCBmDRQ3F0+HxACi1EgmHEEuep0wyBsDOmTJJC1ZeqkmOxKo0aqgmu1wAmpuZC31huNvAEwmkQA

💻 Code

type NotFunction<T> = T extends Function ? never : T;

function isObjectLike<T>(value?: T): value is NotFunction<T> /* & object */ { // Simplify the code, assume the `value` must be an object.
    return value !== null && typeof value === "object";
}

declare const a: (() => void) | { foo: string };
type A = NotFunction<typeof a>; // { foo: string }
if (isObjectLike(a))
    a; // { foo: string }
else
    a; // () => void

declare const b: (() => void) | {};
type B = NotFunction<typeof b>; // {}
if (isObjectLike(b))
    b; // Expect: {}; Actual: (() => void) | {};
else
    b; // Expect: () => void; Actual: never;

🙁 Actual behavior

I'm trying to implement type guards for lodash's isObjectLike function, and the current implementation works fine for most cases.
To make the code more concise, the step of checking if it is an object is commented here, leaving only the check of whether it is not a function.

If the type of the variable being checked (b in the sample code) is a union type that contains a function and an empty object (or any supertype of the function, like { name: string }), the result of type narrowing will incorrectly include the function as well.

In the sample code, the type of b is (() => void) | {}, and the type of NotFunction<typeof b> is {}, which is correct. However, the type of the function returned value is NotFunction<T> is (() => void) | {}, which is not as expected.

🙂 Expected behavior

The function isObjectLike should narrow the type of b to {}, which consistent with the type of NotFunction<typeof b>.

Additional information about the issue

In order to check if a variable is an object, I have to painfully write value !== null && typeof value === "object" everywhere. The isObjectLike function in lodash encapsulates this operation, but lacks type guards. There is also an isObject function which returns value is object, unfortunately it treats functions as objects.

I'm not sure if the current behavior is expected, and if so, it seems that the isObjectLike function does not have a perfect type guard solution, and may have to wait until the negated type is available.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Working as IntendedThe behavior described is the intended behavior; this is not a bug

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions