Search code examples
typescripttypestypescript-never

Is there any way of describing "an object type that cannot be empty" in typescript?


I have a function, and it can operate on any type T. The only constraint is "if T is an object, it can't be an object which can potentially be empty".

I tried this:

declare function func<T>(o: T extends Record<string, never> ? never : T): void;

func("string")         // passes, as expected
func({})               // fails, as expected
func<{x?: number}>({}) // passes, noooo

Also this:

type EmptyObj = Omit<{x: number}, 'x'>

declare function func2<T>(o: T extends EmptyObj ? never : T): void;

func2("HI") // fails
func2({}).  // fails
func2<{x?: number}>({x: 1}) // fails, as expected
func2<{x: number}>({x: 1}) // fails

func2 is probably running into the the fact {} is the top type, so everything extends it. Is typescript capable of describing what I need it to?


Solution

  • Here's one approach:

    declare function func<T extends ({} extends T ? never : unknown)>(o: T): void;
    

    This is a recursive constraint on the type parameter T (such self-referential constraints are known as F-bounded quantification and allow more expressive constraints where they are allowed) which amounts to "allow T if and only if the empty object type {} is not assignable to T". If you can assign an empty object to T, then T should be rejected; if you cannot, then T should be accepted.

    This is implemented by constraining T to the conditional type {} extends T ? never : unknown. If {} extends T is true, then {} is assignable to T, and so the conditional type resolves to never, and so the constraint is T extends never which will not be met, and you'll get an error. On the other hand, if {} extends T is false, then {} is not assignable to T, and so the conditional type resolves to unknown, and so the constraint is T extends unknown, which will essentially always be met, and you won't get an error.

    Let's test it:

    func("string")         // passes
    func({})               // fails
    func<{ x?: number }>({}) // fails
    

    Looks good. Since {} extends string is false, you can call func("string"). But since {} extends {} is true and since {} extends { x?: number} is true, then you cannot call func({}) or func<{x?: number}>({}).

    Playground link to code