Kevin Chavez
Kevin Chavez

Reputation: 1031

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?

Upvotes: 3

Views: 980

Answers (1)

jcalz
jcalz

Reputation: 330266

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

Upvotes: 3

Related Questions