在TypeScript泛型中使用接口的属性?

时间:2018-03-12 10:03:33

标签: typescript generics redux

我试图创建一个帮助程序通用actionCreator函数,以避免每个操作的actionCreator类型函数:

interface Response {
    readonly value: 'A' | 'B';
}

enum ActionTypes {
    created = 'ns/created',
    deleted = 'ns/deleted',
}

interface Created {
    readonly type: ActionTypes.created;
    readonly payload: Response;
}

interface Deleted {
    readonly type: ActionTypes.deleted;
}

type Action = Created | Deleted;

// actionCreator = (type, payload) => ({type, payload});
// actionCreator(ActionTypes.created, response);
// actionCreator(ActionTypes.deleted);

是否可以告诉TS actionCreator不应该只返回一个字符串和一个可选的有效负载对象,而是要知道type Action来检查类型和有效负载对? 像function actionCreator<T>(type: T.type, payload: T.payload)这样无效的TS。

1 个答案:

答案 0 :(得分:1)

以下所有假设您不仅仅有两种类型的操作。如果你真的只有两个,那么使用函数重载并完成它:

function actionCreator(type: ActionTypes.created, payload: Response): Created;
function actionCreator(type: ActionTypes.deleted): Deleted;
function actionCreator(type: any, payload?: any) {
  return (typeof payload === 'undefined') ? { type } : { type, payload }
}

有关更一般的内容,请继续阅读:

一旦conditional types功能登陆TypeScript 2.8(预计将于2018年3月某个时候发布,您现在可以使用typescript@next进行尝试),您将能够指定一个行为为的功能(I想你想要的,而不是触及你现有的代码,如下:

// If K is a key of T, return T[K].  Otherwise, return R
type MaybePropType<T, K extends string, R=never> = K extends keyof T ? T[K] : R

// Given K, one of the "type" properties of the Action union,
// produce the constituent of Action union whose type is K.  So:
// ActionFromType<ActionTypes.created> is Created, and
// ActionFromType<ActionTypes.deleted> is Deleted.
type ActionFromType<K extends Action['type'], AA = Action>
  = AA extends infer A
  ? MaybePropType<AA, 'type'> extends K ? AA : never
  : never

// The one-argument actionCreator() call accepts an element of ActionTypes
// corresponding to an Action with no payload, and produces an Action of that type.
function actionCreator<
  T extends ActionTypes & ('payload' extends keyof ActionFromType<T> ? never : {})
  >(type: T): ActionFromType<T>;

// The two-argument actionCreator() call accepts an element from ActionTypes
// corresponding to an Action with a payload, and a value of the payload type,
// and produces an Action of that type.
function actionCreator<
  T extends ActionTypes & ('payload' extends keyof ActionFromType<T> ? {} : never)
  >(type: T, payload: MaybePropType<ActionFromType<T>, 'payload'>): ActionFromType<T>;

// The implementation of actionCreator() creates a one-or-two-property
// value from its arguments.  Note that this implementation won't be right
// if you ever add new Action types that require more properties
function actionCreator(type: any, payload?: any) {
  return (typeof payload === 'undefined') ? { type } : { type, payload }
}

以下是它的工作原理:

declare const resp: Response;

// no errors
const created = actionCreator(ActionTypes.created, resp); // Created
const deleted = actionCreator(ActionTypes.deleted); // Deleted

// errors
const badCreated = actionCreator(ActionTypes.created); // missing payload
const badDeleted = actionCreator(ActionTypes.deleted, "payload?!"); // unexpected payload
const badDeleted2 = actionCreator('ns/deleted'); // not typed as an enum

这是很多类型杂耍,一些错误信息将是神秘的,它取决于一个非常新的功能。

如果你不介意改变你的代码,你可以得到一些适用于TypeScript 2.7的东西,actionCreator()更容易推理...但是它之前的样板可能有点令人生畏:< / p>

// create a mapping from action type to payload type
type ActionPayloads = {
  'ns/created': Response
}
// create a list of actions with no payloads
type ActionsWithoutPayloads = 'ns/deleted';

// this is a helper type whose keys are the ActionTypes
// and whose values represent the part of the Action without the type
// so, just {} for a payloadless type, and {readonly payload: P} for
// a payloaded type
type ActionMap = { [K in ActionsWithoutPayloads]: {} } &
  { [K in keyof ActionPayloads]: { readonly payload: ActionPayloads[K] } }

// this is a helper function which makes sure we define ActionTypes
// correctly
const keysOfActionMap = <T extends { [k: string]: keyof ActionMap }>(x: T) => x;

// now we have ActionTypes.  It is a plain object, not an enum, 
// but it behaves similarly
const ActionTypes = keysOfActionMap({
  created: 'ns/created',
  deleted: 'ns/deleted'
});

// some helper type functions
type IntersectionToObject<T> = { [K in keyof T]: T[K] }
type ValueOf<T> = T[keyof T];

// extract Action from previous stuff.  
// Action by itself is a union of actions from before, while
// Action<K> is the action corresponding to K from ActionTypes.
type Action<K extends keyof ActionMap = keyof ActionMap> =
  ValueOf<{
    [P in K]: IntersectionToObject<{ readonly type: P } & ActionMap[P]>
  }>

// if you need names for Created and Deleted:    
type Created = Action<typeof ActionTypes.created>;
type Deleted = Action<typeof ActionTypes.deleted>;

最后这是函数,它的输入更简单:

// actions without payloads only accept one parameter
function actionCreator<K extends ActionsWithoutPayloads>(
  type: K): Action<K>;

// actions with payloads require two parameters
function actionCreator<K extends keyof ActionPayloads>(
  type: K, payload: ActionPayloads[K]): Action<K>;

// same impl as before
function actionCreator(type: any, payload?: any) {
  return (typeof payload === 'undefined') ? { type } : { type, payload }
}

让我们确保它有效:

declare const resp: Response;

// no errors
const created = actionCreator(ActionTypes.created, resp); // Created
const deleted = actionCreator(ActionTypes.deleted); // Deleted
const okayDeleted = actionCreator('ns/deleted'); // okay this time

// errors
const badCreated = actionCreator(ActionTypes.created); // missing payload
const badDeleted = actionCreator(ActionTypes.deleted, "payload?!"); // unexpected payload 

呼!希望有所帮助;祝你好运。