typescript - 泛型函数返回相同的 T 或不同的 R,不能将高阶函数 '(i: T) => i' 作为默认参数值
我想了解如何使用泛型类型达到我想要的效果,所以这是我的例子:
function foo(value: 42 = 42 ,bar:(val: any) => any = I => I){
return bar(value)
}
我在示例中使用某种类型 T 的 value,在我的例子中类型是 number,所以我使用 42。我希望将一个函数作为参数,该函数将把值更改为不同的类型 R,或者返回相同类型 T,在这种情况下,默认参数值将是恒等函数 'I => I'。
我把我的函数改写成了箭头函数,像这样:
const foo = <R1>(
value: number = 42,
bar: <R0>(val: number) => R0 = I => I
) => {
return bar<R1>(value);
};
const foo0 = (
value: number = 42,
bar: <R0>(val: number) => R0 = I => I
) => {
return bar(value);
};
const foo1 = <R1>(
value: number = 42,
bar: (val: number) => R1 = I => I
) => {
return bar(value);
};
错误:类型 'number' 不能赋值给类型 'RX'。'RX' 可能使用任意类型实例化,该类型可能与 'number' 无关。
我使用 R1 和 R0 是因为我不确定在这个例子中 R1 和 R0 的区别,但是如果我删除 I => I 默认值,错误信息就消失了,但我不明白为什么,也不明白该如何解决它……
在下面的例子中 fooB(42,i=>i) 和 foo0B(42,i=>i) 出错了,但 foo1B(42,i=>i) 没有出错
我如何设置默认值 I => I 而不必同时指定 R | number
const fooB = <R1>(value: number = 42, bar: <R0>(val: number) => R0) => {
return bar<R1>(value);
};
const foo0B = (value: number = 42, bar: <R0>(val: number) => R0) => {
return bar(value);
};
const foo1B = <R1>(value: number = 42, bar: (val: number) => R1) => {
return bar(value);
};
fooB(42,i=>i)
foo0B(42,i=>i)
foo1B(42,i=>i)
答案#1
你从示例中得到的错误信息是由于在调用函数时可以显式给出类型参数,例如:
fooB<number>(42, (i) => i);
考虑 fooB:
const fooB = <R1>(value: number = 42, bar: <R0>(val: number) => R0) => {
return bar<R1>(value);
};
这里发生的是,内部 bar 也可以用除了 R1 之外的任何其他类型调用。如果是这种情况,就无法保证 number(value 的类型)是与 R0 兼容的类型。
问题出现在例如我们这样做的时候:
fooB<21>(42, (i) => i);
关于这个调用,我们可以说:R1 被实例化为 21。
这里 42 extends number 是正确的,因为考虑到 value 的类型,但 number extends 21 是 FALSE。换句话说:42 和 21 是类型 number 的不同子类型。因此返回的 i 既不能赋值给 R1,也不能赋值给 R0。类似的问题也发生在 foo0B 和 R0 上。
已经有一些很好的解释说明了这个错误消息为什么会出现。
解决方法
如果我理解正确,你想要一个函数,它:
- 接受类型为
T的参数 - 可选择地接受一个函数
fn,该函数接受类型为T的参数并返回类型为U的值 - 使用第一个参数调用
fn,然后返回结果 (U)(如果提供了fn),否则返回T(即恒等)。
编写这个的最直接方式如下:
function foo<T, U>(value: T, fn: (val: T) => T | U = i => i): T | U {
return fn(value);
}
但是在这里,foo 和 fn 确实只能返回联合类型 T | U,因为只有一个签名需要兼顾这两种情况
- 没有
fn - 使用
fn
如果你想根据是否传递了回调函数来分支类型(我假设这是基于默认的 I => I),你可以改用函数重载。本质上,这就像在说“我为 foo 有以下两种签名”:
function foo<T>(value: T): T;
function foo<T, U>(value: T, fn: (val: T) => U): U;
function foo<T>(value: T, fn = (i: T) => i) {
return fn(value);
}
第一个函数签名是用于未传入回调函数的情况:它接受一个 T,并只会返回一个 T。从这个意义上说,foo 就像是恒等函数。
第二个签名接受回调 fn。如果在调用 foo 时没有显式给出类型参数,U 将从 fn 的类型中推断出来。否则,fn 的类型将根据给定的 U 类型进行验证。
然后是函数实现。它可以处理两种签名,并通过使用恒等函数(默认值)来匹配第一个签名,或者使用给定函数来匹配第二个签名。这个实现签名只明确指定对所有重载而言都是常量的类型,因此只指定了T。重载将通过按调用逐个确定返回类型来完成其余部分。
这是一个完整的 TS 游乐场,使用以下示例:
// Without callback
const a: number = foo(42); // Fine
const b: string = foo("I'm a string"); // Fine
const c: string = foo(42); // ERROR: type '42' is not assignable to type 'string'
// With callback
const d: number = foo('42', parseInt); // Ok
const e: string = foo(42, (x: number) => x.toString()); // Ok
const f: string = foo(42, (x: number) => x + 1); // ERROR: type 'number' is not assignable to type 'string'
// With explicit types
const g: number = foo<string, number>('42', parseInt); // Ok
const h: number = foo<string, number>('42', (x) => x); // ERROR: type 'string' is not assignable to type 'number'
