基于鉴别器从类型联合中提取类型

Extracting type from union of types based on a discriminator

在下面的示例中,如何为 withoutSwitchReducer 中的 action 参数提供正确的类型?

enum ActionTypesEnum {
    FOO = 'FOO',
    BAR = 'BAR',
}

type ActionTypes = {
    type: ActionTypesEnum.FOO,
    payload: { username: string }
} | {
    type: ActionTypesEnum.BAR,
    payload: { password: string },
};

// "withSwitchReducer" works fine as TS can infer the descriminator from action.type    

function withSwitchReducer(action: ActionTypes) {
    switch (action.type) {
        case 'FOO':
            return action.payload.username;
        case 'BAR':
            return action.payload.password;
        default:
            return null;
    }
}

// The code below gives as error as "action.payload.username" is not available on "action.payload.password" and vice versa

const withoutSwitchReducer = {
    [ActionTypesEnum.FOO]: (action: ActionTypes) => {
        return action.payload.username;
    },
    [ActionTypesEnum.BAR]: (action: ActionTypes) => {
        return action.payload.password;
    }
};

此处使用 Intellisense 的相同代码:TS Playground Link

有两种方法可以做到这一点。

您可以声明一次类型:

const withoutSwitchReducer: { [k in ActionTypesEnum]: (action: Extract<ActionTypes, { type: k }>) => string } = {
    [ActionTypesEnum.FOO]: (action) => {
        return action.payload.username;
    },
    [ActionTypesEnum.BAR]: (action) => {
        return action.payload.password;
    },
};

或者您可以单独描述它们:

const withoutSwitchReducer2 = {
    [ActionTypesEnum.FOO]: (action: Extract<ActionTypes, { type: ActionTypesEnum.FOO }>) => {
        return action.payload.username;
    },
    [ActionTypesEnum.BAR]: (action: Extract<ActionTypes, { type: ActionTypesEnum.BAR }>) => {
        return action.payload.password;
    },
};

声明一次类型的好处显然是您不必一遍又一遍地做同样的事情,但是单独描述它们可以让您从推断这些函数的 return 类型中获益。

更新:正如 Titian Cernicova-Dragomir 在评论中提到的,您可以将其声明为可在其他地方重用的类型:

type ReducerMap<T extends { type: string }> = { [P in T['type']]: (action: Extract<T, { type: P }>) => any }

函数的return类型是any。 我试图找到一种方法来推断每个定义的实际 return 值,但这不太可能。

并且由于它用作缩减器映射,您可能不会关心 return 值,因为它已被框架使用。

ActionTypes 是复合类型。实例化变量时,应通过as.

显式指明其具体类型

解决方案:

function withSwitchReducer(action: ActionTypes) {
    switch (action.type) {
        case 'FOO':
            return (action.payload as {
                type: ActionTypesEnum.FOO,
                payload: { username: string }
            }).username;
        case 'BAR':
            return (action.payload as {
              type: ActionTypesEnum.BAR,
              payload: { password: string }
            }).password;
        default:
            return null;
    }
}

const withoutSwitchReducer = {
    [ActionTypesEnum.FOO]: (action: ActionTypes) => {
        return (action.payload as {
            type: ActionTypesEnum.FOO,
            payload: { username: string }
        }).username;
    },
    [ActionTypesEnum.BAR]: (action: ActionTypes) => {
        return (action.payload as {
          type: ActionTypesEnum.BAR,
          payload: { password: string }
        }).password;
    }
};

更好的解决方案:

interface Foo {
    type: 'FOO'
    payload: { username: string }
}

interface Bar {
    type: 'BAR'
    payload: { password: string }
}

type ActionTypes = Foo | Bar

function withSwitchReducer(action: ActionTypes) {
    switch (action.type) {
    case 'FOO':
        return (action.payload as Foo).username;
    case 'BAR':
        return (action.payload as Bar).password;
    default:
        return null;
    }
}

const withoutSwitchReducer = {
    'FOO': (action: ActionTypes) => {
        return (action.payload as Foo).username;
    },
    'BAR': (action: ActionTypes) => {
        return (action.payload as Bar).password;
    }
};

字符串字面量可以作为类型,如var a: 'Apple'。并且可以组合起来,比如var b: 'Apple' | 'Orange'