Gregor
Gregor

Reputation: 2375

How to require a an object property on one of two function arguments

I have a factory which returns a method. The factory can set a default id, in which case it is no longer required to be passed to the method.

Here is a simplified test case (Playground)

type FactoryOptions = {
  id?: number
}

type MethodOptions = {
  id?: number
}

function factory (options: FactoryOptions) {
  return method.bind(null, options)
}

function method (state: FactoryOptions, options: MethodOptions) {
  const id = state.id || options.id
  console.log(id.toString())
}

The id.toString() is just to trigger TypeScript to complain that id might be undefined at this point. The context of this question is octokit/auth-app.js#5 which is more complicated.

Upvotes: 1

Views: 82

Answers (2)

Shaun Luttin
Shaun Luttin

Reputation: 141522

As an alternative to the excellent answer via jcalz, here is another (playground) for your consideration.

type FactoryOptions = {
  id?: number
}

type MethodOptions = {
  id?: number
}

function factory (options: FactoryOptions) {
  return method.bind(null, options)
}

const hasId = (input: any): input is {id: number} => 
  typeof input.id !== 'undefined'; 

function method (state: Omit<FactoryOptions, 'id'>, options: Required<MethodOptions>): void;
function method (state: Required<FactoryOptions>, options: Omit<MethodOptions, 'id'>): void;
function method (state: FactoryOptions, options: MethodOptions): void {
  if(hasId(state)) {
    console.log(state.id.toString());
  } else if (hasId(options)) {
    console.log(options.id.toString());
  }
}

method({}, {}); // error
method({ id: 10 }, {}); // ok
method({ }, { id: 10 }); // ok

Upvotes: 1

jcalz
jcalz

Reputation: 328312

There are two sides to this problem... the caller's side of method(), and the implementation of method(). In TypeScript it's generally easier to make the type checker behave nicely for callers than it is to make it work inside implementations.

From the caller's side, we can make sure that method must be called with at least one of the two arguments containing a defined id property. One way to do this is with overloads, and since that's the least crazy-looking solution I can come up with, that's what I'll show (the other solutions involve using unions of tuple types as a rest parameter):

type WithId = {
  id: number;
};

function method(state: FactoryOptions & WithId, options: MethodOptions): void;
function method(state: FactoryOptions, options: MethodOptions & WithId): void;
function method(state: FactoryOptions, options: MethodOptions) {
   // impl
}

Here you can either call the first or second call signature, and the implementation signature is hidden from the caller:

method({}, {}) // error
method({id: 1}, {}) // okay
method({}, {id: 1}) // okay

Inside the implementation, things are less pleasant. There's no easy way to convince the compiler that a value of type [number, number | undefined] | [number | undefined, number] has a defined value at either the first or second element. That is, the compiler doesn't see such a union as a discriminated union, so checking the first element against undefined has no effect on what the compiler sees as the second union. You might be able to implement some kind of user-defined type guard for this, but that's overkill.

Instead, let's just accept that we are smarter than the compiler and use a type assertion:

function method(state: FactoryOptions & WithId, options: MethodOptions): void;
function method(state: FactoryOptions, options: MethodOptions & WithId): void;
function method(state: FactoryOptions, options: MethodOptions) {
  const id = (typeof state.id !== "undefined"
    ? state.id
    : options.id) as number; // assert as number
  console.log(id.toString()); // okay now
}

Also note that I changed your check of state.id to use a ternary because 0 is falsy and 0 || undefined is undefined. I assume you mean for id to always be a number, which is guaranteed by the ternary check (which sidesteps the falsiness).

That might not be what you hoped for, but it's the best I can do. Hope that helps; good luck!

Link to code

Upvotes: 4

Related Questions