有很多关于 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 不能这样工作。有什么想法吗?
如果愿意的话,思考如何在 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
它有效。 是吗?
回顾一下:在运行时如果没有某种方式来确定参数的类型是不可能的,这在一般情况下需要开发人员指定的类型保护。 因此,您可以通过要求开发人员为每个重载提供类型保护来进行重载,或者按照他们现在所做的那样,通过使用单个实现和多个调用签名来实现重载。 后者更简单。
Typescript 的创建是为了通过添加一些所谓的“语法糖”来使程序员的生活更轻松,这提供了 OOD 的优势。