type ValueType = string | number
interface I<X extends ValueType>{
f: (value: X) => void
}
const run = <X extends ValueType>(i: I<X>) => {
i.f(2)
}
使用i.f(2)
时出错
TS2345: Argument of type 'number' is not assignable to parameter of type 'X'.
'number' is assignable to the constraint of type 'X', but 'X' could be instantiated with a different subtype of constraint 'ValueType'.
我知道这个错误可以用as
解决。
但我希望在使用as
之前检测函数参数的类型,比如这个参数是数字还是字符串?
下面是要解释的伪代码。
const run = <X extends ValueType>(i: I<X>) => {
if (typeof X === 'string'){
const _f = i.f as (value: string) => void
_f("jack")
}else if (typeof X === 'number'){
const _f = i.f as (value: number) => void
_f(1)
}
}
这不是TypeScript的限制,而是JavaScript的限制。实际上,您想要检查的keyof
是i.f
的参数。JavaScript本身不是一种类型化的语言,所以它实际上并不存在。
在这种情况下,特别是在一般情况下,尝试引入泛型实际上没有任何好处。你可以有:
interface I {
f: (value: string | number) => void
}
const run = (i: I) => {
i.f(2)
}
TypeScript不会让你在运行时用普通JavaScript做任何不可能的事情。
如果你的目标是简单地确保传入的是一个参数和一个具有该类型的.f
的i
,我可以建议一个更简单的解决方案吗:
type ValueType = string | number
interface I<X> {
f: (value: X) => void
}
const run = <X extends ValueType>(i: I<X>, value: X) => {
i.f(value)
}
这更符合TypeScript的目的。这样可以防止开发人员传递错误的参数。它将在上出错
run({ f: (x: number) => x + 1 }, 'hi');
但接受:
run({ f: (x: number) => x + 1 }, 8);
泛型大多不重要,因为对于涉及string
或number
的函数类型的特定联合,您所要做的甚至是不可能的。因此,让我们重构以更清楚地显示问题:
type TakesNumber = { (value: number): void; }
const n1: TakesNumber = x => console.log(x.toFixed(2));
const n2: TakesNumber = x => {}
type TakesString = { (value: string): void; }
const s1: TakesString = x => console.log(x.toUpperCase());
const s2: TakesString = x => {}
function foo(f: TakesNumber | TakesString) {
if (takesNumber(f)) { f(2); } else { f("x"); }
}
foo(n1); // "2.00"
foo(s1); // "X"
foo(n2);
foo(s2);
// how do we implement this function?
declare function takesNumber(f: TakesNumber | TakesString): f is TakesNumber;
问题:如何区分接受number
自变量(TakesNumber
)的函数和接受string
自变量(TakesString
)的函数?等价地,给定一个类型为TakesNumber | TakesString
的值,我们如何编写一个类型保护函数(takesNumber
)来区分它们,从而知道要传递哪种参数类型?
答案:我们不能;这是不可能的。
当代码编译为JavaScript时,TypeScript类型系统将被擦除。这包括类似于(x: number) => {}
或(x: string) => {}
中的参数类型注释。上述代码将以JavaScript形式发出,如:
const n1: TakesNumber = x => console.log(x.toFixed(2));
const n2: TakesNumber = x => {}
const s1: TakesString = x => console.log(x.toUpperCase());
const s2: TakesString = x => {}
function foo(f) {
if (takesNumber(f)) { f(2); } else { f("x"); }
}
foo(n1);
foo(s1);
foo(n2);
foo(s2);
希望您能看到,在运行时,对于函数n1
、n2
、s1
和s2
,您不能做任何合理的事情来告诉您它们期望的是number
还是string
。如果你以某种方式解析了函数体,你可能会把n1
和s1
区分开来,但n2
和s2
编译成了相同的函数x => {}
,所以这样的信息根本不存在。
所以你不应该尝试这样做,因为这是不可能的。
当你想做一些不可能的事情时,很可能是XY问题,你有一个完全合理的底层用例,但你认为你需要做一些不可行的事情来满足它。
如果您的用例是在运行时具有函数,并且您需要能够对它们进行检查,以确定它们采用的是string
还是number
,那么一种可行的方法是您自己将这些信息添加到函数中。
例如,可以将一个名为takesNumber
的属性添加到TakesNumber
,将一个称为takesString
的属性添加至TakesString
:
type TakesNumber = {
(value: number): void;
takesNumber: any
}
type TakesString = {
(value: string): void;
takesString: any;
}
然后当你想区分的时候,你只需要检查那些键是否是in
的功能:
function foo(f: TakesNumber | TakesString) {
if ("takesNumber" in f) {
f(2);
} else {
f("x");
}
}
当然,这意味着你需要实际添加这样一个属性;如果你忘记了编译器会警告你:
foo((x: number) => console.log(x.toFixed())); // error!
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Argument of type '(x: number) => void' is not assignable
// to parameter of type 'TakesNumber | TakesString'.
但如果你记得添加属性,那么它将按需工作:
function n(x: number) { console.log(x.toFixed()) };
n.takesNumber = true;
foo(n); // this works
function s(x: string) { console.log(x.toUpperCase()) };
s.takesString = true;
foo(s); // this works
foo(Object.assign(
(x: number) => console.log(x.toFixed()),
{ takesNumber: true }
)); // also works
这比你想要的更复杂,但它有可能的好处。当然,也许有更好的方法来满足您的底层用例,但无论该方法是什么,它都必须与TypeScript的类型擦除一起工作,并且不能依赖TypeScript本身来提供运行时类型信息。
游乐场链接到代码