3

Is it possible to have a conditional type that can test for objects that can be empty?

For example:

   function test<T>(a: T): T extends {} ? string : never {
        return null
      }
   let o1: {}
   let o2: { fox? }
   let o3: { fox }
   test(o1)
   test(o2)
   test(o3) // this one should be 'never'

Since the conditional type test also for inheritance, all 3 cases yield 'string' but I want to yield 'never' if at least one property of the type is required (non empty object type like o3)

UPDATE

When I wrote this question, I tried to nail down the cause of a problem I was having. I though I should solve my doubt not my problem and also simplify my question. However the answers deviate from my problem.

Basically I was trying to build a function where the first argument is an object and the second one is optional if the first can be fully partial (initialized with {})

 function test<T extends {}>(a: T, ...x: T extends {} ? [never?] : [any])
 function test(a, b) {
   return null
 }

let o1: {}
let o2: { fox? }
let o3: { fox }

test(o1) // ok
test(o2) // ok
test(o3) // should fail and require second argument
SystematicFrank
  • 16,555
  • 7
  • 56
  • 102
  • It doesn't seem like it. But if you know the type of `o1` (`typeof o1`) you also know if the object is empty, so why make a separate function for it when you already know it. If you don't know the type, TypeScript can not figure it out. – tscpp Jun 16 '20 at 08:07

2 Answers2

5

You can do it in a relatively simple way with the keyof operator.

Remeber that never is essentially the equivalent of the empty set

type Foo = keyof {} // never
type Bar = keyof { a: 1, b: 2 } // "a" | "b"

Applied to your use case, this becomes

declare function f<T>(t: T): keyof T extends never ? string : never

const a = f({}) //string
const b = f({ a: 1 }) //never

edit

I'm not sure I fully understand the use case you've now included in the question, but if the idea is to treat an object with only optional like like an empty object, you can do it with some more type machinery.

Let's borrow the definition of OptionalPropertyOf from this question.

Given an object of type T where T extends object, we can define the following

type OptionalPropertyOf<T extends object> = Exclude<{
  [K in keyof T]: T extends Record<K, T[K]>
    ? never
    : K
}[keyof T], undefined>

type ExcludeOptionalPropertyOf<T extends object> = Omit<T, OptionalPropertyOf<T>>

type Foo = { a: number, b: string }
type OptFoo = OptionalPropertyOf<Foo> // never
type NonOptFoo = ExcludeOptionalPropertyOf<Foo> // { a: number, b: string }

type Bar = { a: number, b?: string }
type OptBar = OptionalPropertyOf<Bar> // "b"
type NonOptBar = ExcludeOptionalPropertyOf<Bar> // { a: number }

type Baz = { a?: number, b?: string }
type OptBaz = OptionalPropertyOf<Baz> // "a" | "b"
type NonOptBaz = ExcludeOptionalPropertyOf<Baz> // {}

Then, let's slightly change the definition of f to

declare function f<T extends object>(t: T): keyof ExcludeOptionalPropertyOf<T> extends never ? string : never

And now you get what you were looking for

declare const a: Foo
declare const b: Bar
declare const c: Baz
declare const d: {}

const fa = f(a) // never
const fb = f(b) // never
const fc = f(c) // string
const fd = f(d) // string

Playground link

bugs
  • 14,631
  • 5
  • 48
  • 52
  • nice! Idid not know that trick, but in my case I need to group {} and {a?} together while {a} should be a different type. I've just updated my question. – SystematicFrank Jun 16 '20 at 09:16
0

This doesn't use a conditional type, and it doesn't use quite the same code as your question; so I appreciate it might be completely useless. But I thought I'd put something in to see if it works as a starting point to working towards a useful answer.

You can use overloads to achieve the same thing:

function test(a: {}): string;
function test(a: Record<string | number | symbol, any>): never;
function test(a: {} | Record<string | number | symbol, any>): string | never {
    if (Object.keys(a).length === 0) {
        return 'test';
    } else {
        throw new Error();
    }
}

const a = test({});       // type string
const b = test({ a: 1 }); // type never

Playground link

OliverRadini
  • 6,238
  • 1
  • 21
  • 46