TypeScript 中有两个功能可让您定义返回类型取决于参数类型的函数:overloaded 函数和generic 函数。他们都有警告。
重载函数
重载函数有多个调用签名,当您调用该函数时,编译器会根据输入依次检查每个调用签名来选择哪个调用签名。这是toggleDateString 的重载调用签名类型:
declare const toggleDateString: {
(value: Date): string;
(value: string): Date;
}
toggleDateString(new Date()).toUpperCase(); // string
toggleDateString("hello").getFullYear() // Date
重载函数注意事项:
-
通过函数语句实现重载函数时,先声明每个调用签名,然后提供实现:
function toggleDateString(value: Date): string;
function toggleDateString(value: string): Date;
function toggleDateString(value: Date | string) {
if (typeof value === 'string') {
return someDate;
} else {
return someString;
}
}
编译器会进行一些检查以确保实现与调用签名的组合并非完全不兼容,但它允许一些不安全的实现。它只是并没有真正尝试验证实现是否正确。见microsoft/TypeScript#13235。因此,您需要在重载的函数实现语句中小心您正在做正确的事情。例如,下面的编译没有错误:
function toggleDateString(value: Date): string;
function toggleDateString(value: string): Date;
function toggleDateString(value: Date | string) {
if (typeof value !== 'string') { // <-- oops
return someDate;
} else {
return someString;
}
}
-
当您尝试通过 表达式(如 function 表达式或箭头函数)实现重载函数时,您会遇到相反的问题:编译器仍然没有真正尝试验证实现是正确的,但现在它不再允许不安全的实现,而是警告安全的实现。见microsoft/TypeScript#38622。例如:
const toggleDateString: { // error!
// ~~~~~~~~~~~~~~~~ <--
// Type '(value: Date | String) => string | Date' is not assignable to type
// '{ (value: Date): string; (value: string): Date; }'.
(value: Date): string;
(value: string): Date;
} = (value: Date | String) => {
if (typeof value === 'string') {
return someDate;
} else {
return someString;
}
}
因此,为了在那里继续,您需要通过 type assertion 之类的方式抑制错误:
const toggleDateStringAssert = ((value: Date | String) => {
if (typeof value === 'string') {
return someDate;
} else {
return someString;
}
}) as {
(value: Date): string;
(value: string): Date;
}; // no error now
这会让你重新开始允许不安全的事情(如果你再次将 === 更改为 !==,你会看到)。
-
调用重载函数时,编译器一次只允许调用其中一个调用签名。它不会“组合”调用签名;见microsoft/TypeScript#41407。如果编译器无法确定调用哪个调用签名,则会出现错误:
toggleDateString(new Date()); // okay
toggleDateString("someString"); // okay
toggleDateString(Math.random() < 0.5 ? new Date() : "someString"); // error!
// ------------> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// No overload matches this call.
function oops<T extends Date | string>(t: T) {
toggleDateString(t); // error!
// No overload matches this call.
}
-
在类型系统中操作或检查重载函数的类型时,编译器通常会假装只存在第一个或最后一个调用签名。请参阅microsoft/TypeScript#26591 了解更多信息。这有时会产生奇怪的效果:
type OutputOfToggleDateString = ReturnType<typeof toggleDateString>
// type OutputOfToggleDateString = Date // <-- just Date?!
通用函数
对于泛型函数,您只有一个调用签名,但有一个泛型类型参数,该参数将在调用函数时根据输入推断。您需要根据泛型类型参数来表达所需的返回类型。在一般情况下,这可能需要conditional types。这是一个实现:
const toggleDateString = <T extends Date | string>(value: T): T extends Date ? string : Date => {
if (typeof value === 'string') {
return someDate as any;
} else {
return someString as any;
}
}
您可以在更多情况下调用这些函数,因为只有一个调用签名:
toggleDateString(new Date()); // okay, string
toggleDateString("someString"); // okay, Date
toggleDateString(Math.random() < 0.5 ? new Date() : "someString"); // okay, string | Date
function okay<T extends Date | string>(t: T) {
toggleDateString(t); // okay, T extends Date ? string : Date
}
而且编译器在类型系统中处理这些类型的方面要好一些:
type OutputOfToggleDateString = ReturnType<typeof toggleDateString>
// type OutputOfToggleDateString = string | Date
带有条件返回类型警告的泛型函数:
-
您可能已经注意到,上面的函数在实现中使用了类型断言as any。和重载一样,编译器确实无法验证函数实现是否满足调用签名:
const togDateStringUgh = <T extends Date | string>(value: T): T extends Date ? string : Date => {
if (typeof value === 'string') {
return someDate; // error! 'Date' is not assignable to 'T extends Date ? string : Date'
} else {
return someString; // error! 'string' is not assignable to 'T extends Date ? string : Date'
}
}
请参阅microsoft/TypeScript#33912 了解更多信息。现在你需要某种类型断言来防止错误。
-
泛型条件类型比重载看起来有点奇怪,也更难解释。
我个人倾向于避免重载并更喜欢泛型函数,但这是一个见仁见智的问题。无论哪种方式都可以。
Playground link to code