首页 > 解决方案 > 为什么 Typescript 不以正确的方式支持函数重载?

问题描述

关于 Typescript 中函数重载的工作方式有很多问题(例如,TypeScript 函数重载)。但不存在诸如“为什么它会以这种方式工作?”之类的问题。现在函数重载看起来像这样:

function foo(param1: number): void; 
function foo(param1: number, param2: string): void;

function foo(...args: any[]): void {
  if (args.length === 1 && typeof args[0] === 'number') {
    // implementation 1
  } else if (args.length === 2 && typeof args[0] === 'number' && typeof args[1] === 'string') {
    // implementation 2
  } else {
    // error: unknown signature
  }
}

我的意思是,Typescript 的创建是为了让程序员的生活更轻松,它添加了一些所谓的“语法糖”,它提供了 OOD 的优势。那么为什么 Typescript 不能代替程序员来做这些烦人的事情呢?例如,它可能看起来像:

function foo(param1: number): void { 
  // implementation 1 
}; 
function foo(param1: number, param2: string): void {
  // implementation 2 
};
foo(someNumber); // result 1
foo(someNumber, someString); // result 2
foo(someNumber, someNumber); // ts compiler error

此 Typescript 代码将被转换为以下 Javascript 代码:

function foo_1(param1) { 
  // implementation 1 
};
function foo_2(param1, param2) { 
  // implementation 2 
}; 
function foo(args) {
  if (args.length === 1 && typeof args[0] === 'number') {
    foo_1(args);
  } else if (args.length === 2 && typeof args[0] === 'number' && typeof args[1] === 'string') {
    foo_2(args);
  } else {
    throw new Error('Invalid signature');
  }
};

而且我没有找到任何原因,为什么 Typescript 不能这样工作。有任何想法吗?

标签: typescriptoopoverloading

解决方案


如果你愿意,思考如何在 TypeScript 中实现“真正的”函数重载是一个有趣的练习。让编译器获取一堆单独的函数并从中生成一个函数很容易。但是在运行时,这个单一函数必须根据参数的数量和类型知道要调用几个底层函数中的哪一个。参数的数量肯定可以在运行时确定,但是参数的类型是完全擦除的,所以没有办法实现它,你就卡住了。

当然,您可能会违反 TypeScript 的设计目标之一(特别是关于添加运行时类型信息的非目标 #5),但这不会发生。看起来很明显,当您检查时number,您可以输出typeof xxx === 'number',但是当检查用户定义的时您会输出什么interface?解决这个问题的一种方法是要求开发人员为每个函数重载提供一个用户定义的类型保护,以确定参数是否是正确的类型。但现在它是在让开发人员为每个函数重载指定事物对的领域,这比当前的 TypeScript 重载概念更复杂。

为了好玩,让我们看看作为一个期望函数和类型保护来构建重载函数的库,您自己可以有多接近这一点。像这样的东西怎么样(假设 TS 3.1 或更高版本):

interface FunctionAndGuard<A extends any[]=any[], R=any, A2 extends any[]= A> {
  function: (...args: A) => R,
  argumentsGuard: (args: any[]) => args is A2
};
type AsAcceptableFunctionsAndGuards<F extends FunctionAndGuard[]> = { [K in keyof F]:
  F[K] extends FunctionAndGuard<infer A, infer R, infer A2> ?
  FunctionAndGuard<A2, R, A> : never
}
type UnionToIntersection<U> =
  (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never

type Lookup<T, K> = K extends keyof T ? T[K] : never;
type FunctionAndGuardsToOverload<F extends FunctionAndGuard[]> =
  Lookup<UnionToIntersection<F[number]>, 'function'>;

function makeOverloads<F extends FunctionAndGuard[]>(
  ...functionsAndGuards: F & AsAcceptableFunctionsAndGuards<F>
): FunctionAndGuardsToOverload<F> {
  return ((...args: any[]) =>
    functionsAndGuards.find(fg => fg.argumentsGuard(args))!.function(...args)) as any;
}

makeOverloads()函数接受可变数量的FunctionAndGuard参数,并返回一个重载函数。试试看:

function foo_1(param1: number): void {
  // implementation 1 
};
function foo_2(param1: number, param2: string): void {
  // implementation 2 
};

const foo = makeOverloads({
  function: foo_1,
  argumentsGuard: (args: any[]): args is [number] =>
    args.length === 1 && typeof args[0] === 'number'
}, {
    function: foo_2,
    argumentsGuard: (args: any[]): args is [number, string] =>
      args.length === 2 && typeof args[0] === 'number' && typeof args[1] === 'string'
  }
);

foo(1); // okay
foo(1, "two"); // okay
foo(1, 2); // error

有用。耶?

回顾一下:在运行时没有某种方式来确定参数的类型是不可能的,这在一般情况下需要开发人员指定的类型保护。因此,您可以通过要求开发人员为每个重载提供类型保护来进行重载,或者通过执行他们现在所做的,通过具有单个实现和多个调用签名来进行重载。后者更简单。

希望能提供一些见解。祝你好运!


推荐阅读