首页 > 解决方案 > 在 Typescript 中,如何使用所述类型的文字类型属性从联合中选择一个类型?

问题描述

我有一个减速器在反应。动作可以是 8 种类型之一,但为简单起见,我们假设只有 2 种类型

type Add = {
  type: 'add';
  id: string;
  value: string;
}

type Remove = {
  type: 'remove';
  id: string;
}

type Action = Add | Remove;

我没有使用 switch case,而是使用了一个处理程序对象,其中每个处理程序都是一个处理特定操作的函数

const handlers = {
  add: (state, action) => state,
  remove: (state, action) => state,
  default: (state, action) => state,
}

const reducer = (state, action) => {
  const handler = handlers[action.type] || handlers.default;
  return handler(state, action);
}

现在我想handlers适当地键入对象。所以处理函数应该接受与其在handlers对象中的键对应的类型的动作。

type Handlers = {
  [key in Action["type"]]: (state: State, action: Action) => State
//                                                ↑this here should be the action which has type
//                                                matching to it's key. So when the key is
//                                                'add', it should be of type Add, and so on.
}

我能想到的只是明确说明键和匹配的操作类型。有没有办法根据键的值从联合中“选择”类型?

标签: typescript

解决方案


仅提取类型很简单。

type Add = {
    type: 'add';
    id: string;
    value: string;
}
  
type Remove = {
    type: 'remove';
    id: string;
}
  
type Action = Add | Remove;

type addType = Extract<Action, {type: 'add'}>;

您可以为联合的每个元素创建一个映射类型来自动执行此操作。

type OfUnion<T extends {type: string}> = {
    [P in T['type']]: Extract<T, {type: P}>
}

这是一个映射类型。它就像一个类型函数,它接受一个类型,转换它,然后返回那个新类型。OfUnion期待形状像 的东西也是如此T extends {type: string}。这意味着它将接受{type: 'add'}或像type Action = Add | Remove. 重要的是该联合类型的每个元素都有一个type: string属性。

当您有一个联合类型并且每个选项都有一个共同的属性时,您可以安全地访问该属性(在本例中为type)。这得到分发,所以Action['type']给了我们'add' | 'remove'。映射类型正在创建一个具有键的对象,[P in T['type']]也就是'add' | 'remove'

属性的类型add应该是那个动作的类型,Add。你可以通过Extract<>我打的那个电话来解决这个问题。这实质上将联合过滤为仅包含与特定类型匹配的元素。在这种情况下,过滤到仅匹配的内容{type: 'add'}。与此匹配的唯一选项是{type: 'add', id: string, value: string}对象,因此这就是它的设置。

现在你有一个看起来像这样的对象:

{
    add: {
        type: 'add';
        id: string;
        value: string;
    },
    remove: {
        type: 'remove';
        id: string;
    }
}

那太棒了!但是我们需要将其转换为处理程序。所以处理程序仍然会有addremove键,但不是对象类型,它们将是接受对象类型并返回任何内容的函数。

type Handler<T> = {
    [P in keyof T]: (variant: T[P]) => any
}

这里的一个例子P将是'add'并且T[P]将是那种Add类型。所以现在处理程序应该接收一个Add对象,并用它做一些事情。这正是我们想要的。

现在您可以编写一个带有自动完成功能的类型安全的匹配函数:

function match<
    T extends {type: string},
    H extends Handler<OfUnion<T>>,
> (
    obj: T,
    handler: H,
): ReturnType<H[keyof H]> {
    return handler[obj.type as keyof H]?.(obj as any);
}

请注意,您需要在此处使用扩展来推断 H 的特定类型。您需要完全指定的类型来获取每个分支的返回类型。

编辑游乐场链接

match()函数需要接受一个对象,该对象将具有类似的联合类型Action,然后将其预期的处理程序对象限制为某个合同(我们刚刚创建的处理程序类型)。最好将其作为一个函数而不是针对每种情况执行,因为这样您就不需要继续重写类型定义!该功能将带来它们。最好在不使用状态对象的情况下不可知地执行此操作,因为您仍然可以在处理程序分支内使用状态对象。查找“闭包”以获取更多信息,但您也可以相信我认为该state对象将在范围内。但是现在,我们也可以在不相关match的地方使用,比如在 .jsx 中state

const reducer = (state: State, action: Action) => match(action, {
  add: ({id, value}) => state,
  remove: ({id}) => state,
})

我希望这会有所帮助!相信我,使用这些类型比编写它们更容易。您需要高级打字稿知识来编写它们,但任何初学者都可以使用它们。


您可能对我的库感兴趣variant,它确实可以做到这一点。事实上,这些示例只是我的库代码的简化版本。我的match函数将自动限制您传入的处理程序对象,返回类型将是所有处理程序分支的联合。

欢迎你偷这个,或者自己试试这个库。它将在同一方向为您提供更多内容。


推荐阅读