如何在两个函数参数之一上要求对象属性

时间:2019-07-02 18:11:58

标签: typescript

我有一个返回方法的工厂。工厂可以设置默认的id,在这种情况下,不再需要将其传递给方法。

这是一个简化的测试用例(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())
}

id.toString()只是为了触发TypeScript抱怨id在这一点上可能是未定义的。这个问题的上下文是octokit/auth-app.js#5,它更复杂。

2 个答案:

答案 0 :(得分:4)

此问题有两个方面:method()的调用方和method()的实现。在TypeScript中,通常要使类型检查器对调用方表现良好,而不是使其在实现内部起作用。

从调用者的角度,我们可以确保必须使用包含已定义的method属性的两个参数中的至少一个来调用id。一种实现方法是使用overloads,由于这是我能想到的最不疯狂的解决方案,因此,我将向大家展示(其他解决方案包括使用tuple types as a rest parameter的并集):< / p>

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
}

您可以在此处调用第一个或第二个调用签名,并且实现签名对调用者隐藏:

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

在实现过程中,事情并不那么令人愉快。没有简单的方法可以使编译器确信类型[number, number | undefined] | [number | undefined, number]的值在第一个或第二个元素处都有已定义的值。也就是说,编译器没有将这样的并集视为discriminated union,因此将第一个元素与undefined进行检查对编译器视为第二个并集没有影响。您也许可以为此实现某种user-defined type guard,但这是过大的。

相反,我们只接受我们比编译器更聪明,并使用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
}

还要注意,我将您对state.id的支票更改为使用三元,因为0是虚假的,而0 || undefinedundefined。我假设您的意思是id始终是一个数字,这由三元校验(避免了虚假性)保证。

那可能不是您希望的,但这是我能做的最好的。希望能有所帮助;祝你好运!

Link to code

答案 1 :(得分:1)

除了通过jcalz的出色答案之外,还有另一种(playground)供您考虑。

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