可以接受两个对象或合并对象的重载函数

Overloaded Function that can take two objects or a consolidated object

我有一个接口,我们称它为Worker。它看起来像这样。

interface Worker {
  doWork(key: string): number;
};

A Worker 接受一个键值并用它做一些事情。现在我想要一个间接调用 doWork 的函数,比如

function doWork(key: string, worker: Worker): number {
  // Pretend there's more complicated stuff happening around it :)
  return worker.doWork(key);
}

在大多数情况下这很好。但是,有些 Worker 实例仅对特定 key 值有意义。在那些情况下,我希望我的函数能够推断出 key 值,即

function doWork(worker: Worker & { key: string }): number {
  // Pretend there's more complicated stuff happening around it :)
  return worker.doWork(worker.key);
}

所以我的想法是使用 Typescript 重载签名将这两个函数合并为一个函数,这样您就可以用一个或两个参数调用这个函数。

function doWork(key: string, worker: Worker): number;
function doWork(worker: Worker & { key: string }): number;
function doWork(arg0: string | (Worker & { key: string }), arg1?: Worker): number {
  if (typeof arg0 === 'string') {
    const key: string = arg0;
    const worker: Worker = arg1!; // non-null assertion
    return worker.doWork(key);
  } else {
    const key: string = arg0.key;
    const worker: Worker = arg0;
    return worker.doWork(key);
  }
}

这行得通,除了我在第 6 行有一个混乱的非空(嗯,非未定义)断言。因为函数 doWork 只能用 或者 (1) 一个 string 和一个 Worker,或者 (2) 一个有钥匙的 Worker,在我看来,一旦我检查 typeof arg0 === 'string',Typescript 应该能够推断出我们处于第一种情况,因此 arg1 而不是 未定义。但是如果我们删除 !,

function doWork(key: string, worker: Worker): number;
function doWork(worker: Worker & { key: string }): number;
function doWork(arg0: string | (Worker & { key: string }), arg1?: Worker): number {
  if (typeof arg0 === 'string') {
    const key: string = arg0;
    const worker: Worker = arg1; // Oops!
    return worker.doWork(key);
  } else {
    const key: string = arg0.key;
    const worker: Worker = arg0;
    return worker.doWork(key);
  }
}

我们收到一个错误。

$ tsc --strict overload.ts 
overload.ts:11:11 - error TS2322: Type 'Worker | undefined' is not assignable to type 'Worker'.
  Type 'undefined' is not assignable to type 'Worker'.

11     const worker: Worker = arg1;
             ~~~~~~


Found 1 error.

是否有类型安全的方法来编写此函数,不涉及绕过 Typescript(未经检查的向下转换,any,等)?我可以向 Typescript 证明我的函数实现能够像它应该的那样处理这两个重载吗?

Overloaded functions are split into a set of call signatures that callers see, and a single implementation with a signature that the implementation sees. These are compared loosely with each other, but that's about it. The compiler does not use the call signatures to narrow types inside the implementation; they're fairly separate. There have been a number of feature requests around improving this: see microsoft/TypeScript#14515 and microsoft/TypeScript#22609,等等。到目前为止,它的语言还没有支持。

并且根据实现签名,arg0string | (Worker & { key: string }) 类型,而 arg1Worker | undefined 类型。因此,每个参数都是独立且不相关的 union type,检查 arg0arg1.

没有任何影响

与其尝试为此使用“真正的”重载,您可以通过使用一个采用 rest tuples:

并集的函数签名来获得类似的行为
function doWork(
  ...args: [key: string, worker: Worker] | [worker: Worker & { key: string }]
): number {
    if (args.length === 2) {
        const key: string = args[0]; // okay
        const worker: Worker = args[1]; // okay
        return worker.doWork(key);
    } else {
        const key: string = args[0].key; // okay
        const worker: Worker = args[0]; // okay
        return worker.doWork(key);
    }
}

这样的替换是可能的,因为您的两个原始调用签名具有相同的 return 类型 (number)。从调用者的角度来看,这些看起来仍然非常像重载:

// 1/2 doWork(key: string, worker: Worker): number
doWork("xyz", { doWork(k) { return k.length } }); // okay

// 2/2 doWork(worker: Worker & { key: string; }): number
doWork({ key: "xyz", doWork(k) { return k.length } }); // okay

但是编译器现在可以通过 control flow analysis.

验证实现体是否安全

请注意,如果我们保留了您原来的 typeof 检查,编译器仍然会抱怨 worker 可能 undefined:

if (typeof args[0] === "string") {
    const key: string = args[0];
    const worker: Worker = args[1]; // error! still Worker | undefined
    return worker.doWork(key);
}

即使检查在逻辑上应该区分 args[string, Worker] 类型还是 [{key: string} & Worker}] 类型,编译器也看不到这一点。该语言不支持 discriminating a union by a non-literal type 类型,例如 string.

但是元组length属性是数字字面量类型的;所以 length 充当适当的 判别器 ,检查 args.length === 2 允许编译器区分进行了哪种形式的调用,并且一切正常,没有编译器错误。

Playground link to code