根据参数缩小函数的返回值

时间:2020-10-08 12:01:36

标签: typescript

考虑以下代码

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参数缩小返回值?

2 个答案:

答案 0 :(得分:3)

我们可以通过使用通用参数对要在输出类型中用作“变量”的类型进行建模来提取类型。

在这种情况下,我使用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
}

打字机游乐场链接here

答案 1 :(得分:1)

我认为您实际上要在这里实现user-defined type guard。本质上,您编写了一个返回类型为arg is SomeType的布尔函数,它告诉打字稿该函数是否为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的答案,但是您不需要使用arg来断言"as"的类型,因为打字稿已经知道类型已经精炼了。

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

但是我怀疑您甚至根本不需要此foobar函数,而可以直接在您的代码中调用isKind

Typescript Playground Link