为啥 Typescript 不以正确的方式支持函数重载?

Posted

技术标签:

【中文标题】为啥 Typescript 不以正确的方式支持函数重载?【英文标题】:Why Typescript doesn't support function overloading in a right way?为什么 Typescript 不以正确的方式支持函数重载? 【发布时间】:2019-04-20 10:06:59 【问题描述】:

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

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 不能这样工作。有什么想法吗?

【问题讨论】:

"但是没有诸如“为什么它会以这种方式工作?”之类的问题。” AFAICS,这可能是因为“为什么”问题往往很快陷入猜测和意见,除非(有时甚至在之后)有人从语言架构师那里找到一个离散的报价。 (即使排除这一点,我也不知道 SO 是否真的针对“为什么”问题而设计,但我可能错了。) @underscore_d,我没有找到这个奇怪问题的特殊资源。不过,我终于在下面找到了答案。 【参考方案1】:

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

当然,您可能会违反 TypeScript 的设计目标之一(特别是 non-goal #5 关于添加运行时类型信息),但这不会发生。很明显,当您检查number 时,您可以输出typeof xxx === 'number',但是当您检查用户定义的interface 时会输出什么?解决这个问题的一种方法是要求开发人员为每个函数重载提供一个user-defined type guard,它确定参数是否是正确的类型。但现在是让开发人员为每个函数重载指定事物对,这比当前的 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

它有效。耶?

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

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

【讨论】:

绝妙的答案!他们应该将这个问题和这个答案添加到常见问题解答github.com/Microsoft/TypeScript/wiki/FAQ 现在我很清楚了 我不明白你关于运行时类型检查的观点。编译器可以在编译时轻松将每个重载转换为唯一的函数,并根据签名替换代码中的用法。类似于将foo(signature_1)foo(signature_2) 转换为foo_1foo_2 主要区别在于,不像OP建议的那样在运行时进行类型检查,您只需在编译时根据签名替换用法,因此实际上没有使用中心定义/在运行时重载。实际上,我采用了类似的方法,使用 fooType 函数结构,因为这比函数内的类型保护更简洁、高效且简单。 我看到了不同;是的,那将是另一种方法。但这仍然会违反 TypeScript 的 Design Non-Goal #5 关于根据类型系统的结果发出不同代码的规定。所以你要弄清楚在运行时使用哪个重载,这可能会涉及到这样的答案。 我真的不认为重载的实现与传统的 polyfill 有什么不同。是的,需要考虑一下在运行时要使用哪个重载,但是在编译和运行时函数之间仍然存在逻辑 1:1 映射。代码已编译,几乎不需要访问。【参考方案2】:

Typescript 的创建是为了让程序员的生活更轻松,它添加了一些所谓的“语法糖”,它赋予了 OOD 的优势。

这实际上不是 TypeScript 的设计目标之一。您可以阅读目标here。对函数重载的单独实现的支持将属于“依赖[ing]运行时类型信息”的非目标。

【讨论】:

感谢您的回答!顺便说一句,有很多关于在 TypeScript 存储库中添加函数重载的建议,例如 [link] (github.com/Microsoft/TypeScript/issues/3442)

以上是关于为啥 Typescript 不以正确的方式支持函数重载?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 TypeScript 4 转译器没有看到这个不正确的类型传递给函数?

为啥不以编程方式快速更改 uibutton 标题?

为啥 CheckBox 检查不以编程方式与 Kotlin 一起工作?

为啥 JDialog 构造函数不以指定的所有者组件为中心?

为啥浏览器不以保存 cookie 的方式保存 JWT 令牌?

为啥codeigniter2不以更安全的方式存储csrf_hash,比如session?