10

I have a union of two types, one of which is an empty obj.

type U = {} | { a: number } // | { b: string } | { c: boolean } ....

I would like to exclude the empty object from the union however Exclude is no help

type A = Exclude<U, {}>
// A = never

I tried using as const but it's the same result

const empty = {} as const
type Empty = typeof empty
type U = Empty | { a: number }
type A = Exclude<U, Empty>
//type A = never

The extra Irony is that excluding the other properties is straightforward

  type B = Exclude<U, { a: number }>
  // type B = {}

TS Playground

So is it possible to exclude an empty interface from other interfaces in a union?

lonewarrior556
  • 3,917
  • 2
  • 26
  • 55

3 Answers3

9

Answering my own question..

If you use AtLeastOne from @lukasgeiter answer here: Exclude empty object from Partial type

you can do the following:

type AtLeastOne<T, U = {[K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U];
    
type ExcludeEmpty<T> = T extends AtLeastOne<T> ? T : never; 
    
type U = {} | { a: number } | { b: string }
    
type Foo = ExcludeEmpty<U> // { a: number } | { b: string }

TSplayground

lonewarrior556
  • 3,917
  • 2
  • 26
  • 55
3

From the docs for conditional typing here, you can actually assign types based on some condition.

T extends U ? X : Y

So for the above question, what you could do is make use of keyof keyword that is used to extract keys from objects. When there is no any keys found the type is never so, we can check if the keyof object extends never or not i.e.

 keyof K extends never

So combing conditional typing below;

const empty = {} as const
type Empty = typeof empty

type NoEmpty<K> = keyof K extends never ? never : K;

type C = NoEmpty<Empty>;

type U = NoEmpty<Empty> | NoEmpty<{ a: number }>

You can finally see that type of the U to be non empty object i.e excluding empty object. Check this playground

Dipesh Dulal
  • 518
  • 4
  • 8
  • What If you don't control decaring type U how would you wrap all the parts of the union – lonewarrior556 Apr 24 '20 at 15:24
  • This doesn't work for all cases. For example `type D = NoEmpty<{} | boolean>` gets type `never` instead of `boolean`. – Sam Jun 12 '23 at 15:31
1

Here's a simpler way:

type WithoutEmpty<T> = T extends T ? {} extends T ? never : T : never

You need to start the expression with T to make it a "naked type parameter" otherwise it won't be distributive over unions. See this explanation.

TS playground

Sam
  • 14,642
  • 6
  • 27
  • 39