基于参数缩小函数的返回值



考虑以下代码

type Foo = {
kind: "foo";
foo: number;
};
type Bar = {
kind: "bar";
bar: number;
};
type FooBar = Foo | Bar;
function foobar(arg: FooBar, kind: "foo" | "bar"): FooBar | undefined {
if (arg.kind === kind) {
return arg;
}
return undefined;
}
declare const a: FooBar;
if (a.kind === "foo") {
a.foo;  // no error, type is correctly narrowed
}
foobar(a, "foo").foo; // error: Property 'foo' does not exist on type 'Bar'

是否可以为foobar函数编写类型签名,以便根据kind参数缩小返回值?

我们可以通过使用泛型参数来提取类型,以对我们希望在输出类型中用作"变量"的类型进行建模。

在这种情况下,我使用ExtractKind<T>来提取不同种类的T.的并集

然后我们可以使用RefineKind<T, K>-其中T是具有一个种类的类型,K是要精化为的种类。

我已经用一个未定义的检查包装了输出结果——如果你愿意的话,我可以提供一个关于如何详尽地映射所有类型以消除这种检查的需要的例子,但这似乎超出了最初问题的范围。

如果你需要进一步解释这些概念,请告诉我!

完整示例:


type Foo = {
kind: "foo";
foo: number;
};
type Bar = {
kind: "bar";
bar: number;
};
type FooBar = Foo | Bar;
type ExtractKind<T> = T extends { kind: infer K } ? K : never;
type RefineKind<T, K> = T extends { kind: K } ? T : never;
function foobar<K extends ExtractKind<FooBar>>(arg: FooBar, kind: K) {
if (arg.kind === kind) {
return arg as RefineKind<FooBar, K>;
}
return undefined;
}
const foo = foobar(a, "foo");
if (foo !== undefined) {
foo.foo; // foo -> number
}

打字游戏场链接这里

我认为您在这里真正想做的是实现一个用户定义的类型保护。本质上,您编写了一个返回类型为arg is SomeType的布尔函数,这告诉typescript,如果函数是true,那么arg必须是Sometype类型。如果是false,那么这意味着arg不能是Sometype类型。

以下是该函数在您的情况下的样子:

function isKind<T extends Kinds>(arg: FooBar, kind: T): arg is Extract<FooBar, {kind: T}> {
return arg.kind === kind;
}

当你在if语句中使用这个函数时,你会得到关于类型的有意义的信息:

function testUnknown( either: FooBar ) {
if ( isKind(either, "bar") ) {
// we now know that either is Bar
const barVal = either.bar; // ok
const fooVal = either.foo; // error
}
else {
// we now know that either is Foo
const barVal = either.bar; // error
const fooVal = either.foo; // ok
}
}

我个人更喜欢把isKind写成一个双箭头函数,这样你就可以更容易地在array.map或其他回调中调用isKind("foo"),但这取决于代码风格和个人偏好。

const isKind = <T extends Kinds>(kind: T) => 
(arg: FooBar): arg is Extract<FooBar, {kind: T}> => {
return arg.kind === kind;
}

您原来的foobar功能可以在内部使用isKind类型的防护装置。这与@mbdavis的答案类似,但您不需要使用"as"断言arg的类型,因为typescript已经知道该类型已被细化。

function foobar<T extends Kinds>(arg: FooBar, kind: T): Extract<FooBar, {kind: T}> | undefined {
return isKind(arg, kind) ? arg : undefined;
}

但我怀疑您实际上甚至不需要这个foobar函数,可以直接在代码中调用isKind

打字游戏场链接

最新更新