从TypeScript中的通用,已区分的并集缩小返回类型 [英] Narrowing a return type from a generic, discriminated union in TypeScript

查看:88
本文介绍了从TypeScript中的通用,已区分的并集缩小返回类型的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个类方法,该方法接受单个参数作为字符串,并返回具有匹配的type属性的对象.此方法用于缩小可区分的联合类型的范围,并确保返回的对象将始终是具有提供的type区分值的特定的缩小类型.

I have a class method which accepts a single argument as a string and returns an object which has the matching type property. This method is used to narrow a discriminated union type down, and guarantees that the returned object will always be of the particular narrowed type which has the provided type discriminate value.

我正在尝试为此方法提供类型签名,该签名将正确地从通用参数缩小类型,但是在没有用户明确提供应缩小类型的情况下,我没有尝试从已区分的并集中缩小类型到.可以,但是很烦人,而且感觉很多余.

I'm trying to provide a type signature for this method that will correctly narrow the type down from a generic param, but nothing I try narrows it down from the discriminated union without the user explicitly providing the type it should be narrowed down to. That works, but is annoying and feels quite redundant.

希望这种最低限度的再现可以清楚地表明:

Hopefully this minimum reproduction makes it clear:

interface Action {
  type: string;
}

interface ExampleAction extends Action {
  type: 'Example';
  example: true;
}

interface AnotherAction extends Action {
  type: 'Another';
  another: true;
}

type MyActions = ExampleAction | AnotherAction;

declare class Example<T extends Action> {
  // THIS IS THE METHOD IN QUESTION
  doSomething<R extends T>(key: R['type']): R;
}

const items = new Example<MyActions>();

// result is guaranteed to be an ExampleAction
// but it is not inferred as such
const result1 = items.doSomething('Example');

// ts: Property 'example' does not exist on type 'AnotherAction'
console.log(result1.example);

/**
 * If the dev provides the type more explicitly it narrows it
 * but I'm hoping it can be inferred instead
 */

// this works, but is not ideal
const result2 = items.doSomething<ExampleAction>('Example');
// this also works, but is not ideal
const result3: ExampleAction = items.doSomething('Example');

我还尝试变得聪明,尝试动态建立映射类型",这是TS中的一项相当新的功能.

I also tried getting clever, attempting to build up a "mapped type" dynamically--which is a fairly new feature in TS.

declare class Example2<T extends Action> {
  doSomething<R extends T['type'], TypeMap extends { [K in T['type']]: T }>(key: R): TypeMap[R];
}

这具有相同的结果:它不会缩小类型,因为在类型映射{ [K in T['type']]: T }中,每个计算属性T的值不是对于K in迭代的每个属性,但只是相同的MyActions联合.如果我要求用户提供我可以使用的预定义的映射类型,那将起作用,但这不是一个选择,因为在实践中这将是非常差的开发人员体验. (工会很大)

This suffers from the same outcome: it doesn't narrow the type because in the type map { [K in T['type']]: T } the value for each computed property, T, is not for each property of the K in iteration but is instead just the same MyActions union. If I require the user provide a predefined mapped type I can use, that would work but this is not an option as in practice it would be a very poor developer experience. (the unions are huge)

这个用例可能看起来很奇怪.我试图将问题简化为一种更易使用的形式,但是我的用例实际上是关于Observables的.如果您熟悉它们,我会尝试更准确地键入 ofType由redux-observable提供的运算符.它基本上是 type属性上.

This use case might seem weird. I tried to distill my issue into a more consumable form, but my use case is actually regarding Observables. If you're familiar with them, I'm trying to more accurately type the ofType operator provided by redux-observable. It is basically a shorthand for a filter() on the type property.

这实际上与Observable#filterArray#filter缩小类型的方式非常相似,但是TS似乎可以弄清楚这一点,因为谓词回调具有value is S返回值.目前尚不清楚我如何在这里适应类似的情况.

This is actually super similar to how Observable#filter and Array#filter also narrow the types, but TS seems to figure that out because the predicate callbacks have the value is S return value. It's not clear how I could adapt something similar here.

推荐答案

就像编程中许多好的解决方案一样,您可以通过添加一个间接层来实现.

Like many good solutions in programming, you achieve this by adding a layer of indirection.

具体来说,我们可以在操作标记(即"Example""Another")及其各自的有效负载之间添加表格.

Specifically, what we can do here is add a table between action tags (i.e. "Example" and "Another") and their respective payloads.

type ActionPayloadTable = {
    "Example": { example: true },
    "Another": { another: true },
}

然后我们可以做的是创建一个助手类型,该助手类型使用映射到每个操作标签的特定属性来标记每个有效负载:

then what we can do is create a helper type that tags each payload with a specific property that maps to each action tag:

type TagWithKey<TagName extends string, T> = {
    [K in keyof T]: { [_ in TagName]: K } & T[K]
};

我们将用来在动作类型和完整动作对象之间创建一个表:

Which we'll use to create a table between the action types and the full action objects themselves:

type ActionTable = TagWithKey<"type", ActionPayloadTable>;

这是一种更简单(尽管不太清晰)的书​​写方式:

This was an easier (albeit way less clear) way of writing:

type ActionTable = {
    "Example": { type: "Example" } & { example: true },
    "Another": { type: "Another" } & { another: true },
}

现在,我们可以为每个out动作创建方便的名称:

Now we can create convenient names for each of out actions:

type ExampleAction = ActionTable["Example"];
type AnotherAction = ActionTable["Another"];

我们可以通过编写一个联合来创建

And we can either create a union by writing

type MyActions = ExampleAction | AnotherAction;

或者我们每次写新动作时都可以避免更新联合

or we can spare ourselves from updating the union each time we add a new action by writing

type Unionize<T> = T[keyof T];

type MyActions = Unionize<ActionTable>;

最后,我们可以继续进行您所上的课.不用在动作上进行参数化,而是在动作表上进行参数化.

Finally we can move on to the class you had. Instead of parameterizing on the actions, we'll parameterize on an action table instead.

declare class Example<Table> {
  doSomething<ActionName extends keyof Table>(key: ActionName): Table[ActionName];
}

这可能是最有意义的部分-Example基本上只是将表的输入映射到其输出.

That's probably the part that will make the most sense - Example basically just maps the inputs of your table to its outputs.

总的来说,这是代码.

/**
 * Adds a property of a certain name and maps it to each property's key.
 * For example,
 *
 *   ```
 *   type ActionPayloadTable = {
 *     "Hello": { foo: true },
 *     "World": { bar: true },
 *   }
 *  
 *   type Foo = TagWithKey<"greeting", ActionPayloadTable>; 
 *   ```
 *
 * is more or less equivalent to
 *
 *   ```
 *   type Foo = {
 *     "Hello": { greeting: "Hello", foo: true },
 *     "World": { greeting: "World", bar: true },
 *   }
 *   ```
 */
type TagWithKey<TagName extends string, T> = {
    [K in keyof T]: { [_ in TagName]: K } & T[K]
};

type Unionize<T> = T[keyof T];

type ActionPayloadTable = {
    "Example": { example: true },
    "Another": { another: true },
}

type ActionTable = TagWithKey<"type", ActionPayloadTable>;

type ExampleAction = ActionTable["Example"];
type AnotherAction = ActionTable["Another"];

type MyActions = Unionize<ActionTable>

declare class Example<Table> {
  doSomething<ActionName extends keyof Table>(key: ActionName): Table[ActionName];
}

const items = new Example<ActionTable>();

const result1 = items.doSomething("Example");

console.log(result1.example);

这篇关于从TypeScript中的通用,已区分的并集缩小返回类型的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆