缩小TypeScript中通用的,有区别的并集的返回类型

jay*_*lps 17 discriminated-union typescript typescript-typings

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

我正在尝试为这种方法提供一个类型签名,它将正确地缩小从一般参数的类型,但我没有尝试从被区分的联合中缩小它,而没有用户明确提供它应该缩小到的类型.这有效,但很烦人,感觉很多余.

希望这种最小的再现清楚地表明:

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');
Run Code Online (Sandbox Code Playgroud)

我也试着变得聪明,试图动态地建立一个"映射类型" - 这是TS中一个相当新的特性.

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

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


这个用例可能看起来很奇怪.我试图将我的问题提炼成更易消费的形式,但我的用例实际上是关于Observables.如果您熟悉它们,我会尝试更准确地键入ofTyperedux-observable提供运算符.它基本上是一个速记filter()type财产.

这实际上非常类似于如何Observable#filter以及Array#filter缩小类型,但TS似乎认为这是因为谓词回调具有value is S返回值.我不清楚如何在这里适应类似的东西.

Dan*_*ser 23

与编程中的许多优秀解决方案一样,您可以通过添加一个间接层来实现此目的.

具体来说,我们在这里可以做的是在动作标签(即"Example""Another")和它们各自的有效载荷之间添加一个表.

type ActionPayloadTable = {
    "Example": { example: true },
    "Another": { another: true },
}
Run Code Online (Sandbox Code Playgroud)

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

type TagWithKey<TagName extends string, T> = {
    [K in keyof T]: { [_ in TagName]: K } & T[K]
};
Run Code Online (Sandbox Code Playgroud)

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

type ActionTable = TagWithKey<"type", ActionPayloadTable>;
Run Code Online (Sandbox Code Playgroud)

这是一种更容易(虽然不那么清晰)的写作方式:

type ActionTable = {
    "Example": { type: "Example" } & { example: true },
    "Another": { type: "Another" } & { another: true },
}
Run Code Online (Sandbox Code Playgroud)

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

type ExampleAction = ActionTable["Example"];
type AnotherAction = ActionTable["Another"];
Run Code Online (Sandbox Code Playgroud)

我们可以通过写作创建一个联盟

type MyActions = ExampleAction | AnotherAction;
Run Code Online (Sandbox Code Playgroud)

或者,每当我们通过写作添加新动作时,我们都可以免于更新工会

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

type MyActions = Unionize<ActionTable>;
Run Code Online (Sandbox Code Playgroud)

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

declare class Example<Table> {
  doSomething<ActionName extends keyof Table>(key: ActionName): Table[ActionName];
}
Run Code Online (Sandbox Code Playgroud)

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

总之,这是代码.

/**
 * 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);
Run Code Online (Sandbox Code Playgroud)

  • 这也很聪明 (4认同)
  • 对于后人来说,我目前确实不想要的是TS,这里有一个现有的票据请求:https://github.com/Microsoft/TypeScript/issues/17915此解决方案被接受,因为它提供了最灵活的妥协,虽然我不确定我是否可以(或想要)在实践中使用它. (3认同)

bin*_*les 13

从 TypeScript 2.8 开始,您可以通过条件类型来实现这一点。

// Narrows a Union type base on N
// e.g. NarrowAction<MyActions, 'Example'> would produce ExampleAction
type NarrowAction<T, N> = T extends { type: N } ? T : never;

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> {
    doSomething<K extends T['type']>(key: K): NarrowAction<T, K>
}

const items = new Example<MyActions>();

// Inferred ExampleAction works
const result1 = items.doSomething('Example');
Run Code Online (Sandbox Code Playgroud)

注意:感谢@jcalz 从这个答案/sf/answers/3508817231/中提出 NarrowAction 类型的想法