模板文字导致映射对象中丢失类型信息

问题描述 投票:0回答:0

我正在编写的工具遇到了一个看似棘手的问题。该工具允许根据初始状态对象的形状定义所有动作类型、动作、选择器和缩减器函数。在我深入研究细节之前,让我先描述一下预期的用途。

它以一个枚举开始,它定义了状态内部属性的所有键,例如:

enum MyEnum {
  TEST = 'test1',
  TEST2 = 'test2',
}

使用枚举,然后我们定义一个初始状态。

const exampleState = {
  [MyEnum.TEST]: new Entry<string>(),
  [MyEnum.TEST2]: new Entry<number>(),
};

请注意,除了 Entry 类上使用的通用类型参数之外,使用类型注释来声明此状态对象是不必要的,并且会破坏该工具的目的,因为初始状态是状态形状和类型的真实来源。

跳过此工具完整版中包含的其他初始化步骤,我们可以独立使用属于它的各个功能而不会产生不良影响。

创建动作类型:

const actionTypes = createActionTypes<typeof MyEnum>(MyEnum);

创建动作:

const actions = createActions<typeof MyEnum, typeof exampleState>(actionTypes, MyEnum);

如果我开始编写一些代码,我会得到语法建议,显示该对象有两个函数:“test1”和“test2”。

如果我使用正确和不正确的参数运行函数,我会得到预期的效果。第一个条目是一个字符串,所以参数也应该是一个字符串。分别使用数字和字符串调用 test1 和 test2 函数时,Typescript 会正确抛出错误。

但是,使用按键作为函数名很无聊。将 test1 函数称为

updateTest1
会更好,因为这就是动作的作用。

我有第二个创建者函数可以调用,它就是这样做的。

const aliasedActions = createAliasedActions(actions, MyEnum);

就像之前的操作一样,当我开始访问

aliasedActions
的属性时,我得到了正确的语法建议,只有这一次,我看到了“
updateTest1
”和“
updateTest2

接下来是我遇到的问题。当我用字符串或数字调用任何函数时,TypeScript 不会对它们中的任何一个抛出错误!

aliasedActions.updateTest1(1); // okay
aliasedActions.updateTest1('1'); // okay
aliasedActions.updateTest2(1); // okay
aliasedActions.updateTest2('1'); // okay

检查类型后,参数似乎是所有可能类型的联合 (

string | number | undefined
),而不是所需的特定类型,给定初始状态的模式(
updateTest1
应该采用
string | undefined
updateTest2
应该采取
number | undefined
)。

发生了什么事?

Action
的类型定义采用泛型类型参数作为初始状态的键,并使用它来引用参数“payload”(
S[K]['value']
) 的类型。

class Entry<T = any> {
  value?: T;
  constructor(
    value?: T,
  ) {
    this.value = value;
  }
}

type MyState<
  E extends { [key: string]: string },
> = {
  [key in E[keyof E]]: Entry
};

type Action<
  E extends { [key: string]: string },
  S extends MyState<E>,
  K extends E[keyof E],
> = (
  payload: S[K]['value'],
) => ({
  type: ActionTypes<E>[K],
  key: K,
  payload: S[K]['value'],
});

type Actions<
  E extends { [key: string]: string },
  S extends MyState<E>,
> = {
  [K in E[keyof E]]: Action<E, S, K>
};

上面的代码看起来是正确的,

actions
的测试证实了这一点。然而,
aliasedActions
有一些不同类型的定义,这是我不知道我应该做什么不同的地方。

type ActionTypeName<
  E extends { [key: string]: string },
  K extends E[keyof E],
> = `UPDATE_${Uppercase<K>}`;

类型 AliasedActions< E extends { [key: string]: string }, S extends MyState, > = { [K in E[keyof E] as ActionName]: Action; };

由于在映射对象类型中,我断言枚举中的一个值

K
作为模板文字,看来我不能再使用
K
作为
Action
的第三个泛型类型参数。因此,
Action<E, S, E[keyof E]>
而不是
Action<E, S, K>
。然而,副作用是
Action<E, S, E[keyof E]>
使参数“payload”成为所有潜在类型的联合,而不是特定类型。

我需要帮助找出解决方法。它应该很简单,但我没有尝试过。有什么方法可以让我将

K
作为原始类型传递,但仍然保留密钥
ActionName<E, K>

测试示例代码:

class Entry<T = any> {

  value?: T;

  constructor(
    value?: T,
  ) {
    this.value = value;
  }

}

type MyState<
  E extends { [key: string]: string },
> = {
  [key in E[keyof E]]: Entry
};

type Action<
  E extends { [key: string]: string },
  S extends MyState<E>,
  K extends E[keyof E],
> = (
  payload: S[K]['value'],
) => ({
  type: ActionTypes<E>[K],
  key: K,
  payload: S[K]['value'],
});

type Actions<
  E extends { [key: string]: string },
  S extends MyState<E>,
> = {
  [K in E[keyof E]]: Action<E, S, K>
};

type ActionTypes<
  E extends { [key: string]: string },
> = {
  [K in E[keyof E]]: ActionTypeName<E, K>
};

type ActionTypeName<
  E extends { [key: string]: string },
  K extends E[keyof E],
> = `UPDATE_${Uppercase<K>}`;

type ActionName<
  E extends { [key: string]: string },
  K extends E[keyof E],
> = `update${Capitalize<K>}`;

type AliasedActions<
  E extends { [key: string]: string },
  S extends MyState<E>,
> = {
  [K in E[keyof E] as ActionName<E, K>]: Action<E, S, E[keyof E]>;
};

const createActionTypeName = <
  E extends { [key: string]: string },
  K extends E[keyof E] = E[keyof E],
>(key: K): ActionTypeName<E, K> => {
  const postFix = key.toUpperCase() as Uppercase<K>;
  return `UPDATE_${postFix}`;
};

const createActionTypes = <
  E extends { [key: string]: string },
>(
  keys: E,
): ActionTypes<E> => {
  const result: Partial<ActionTypes<E>> = {};
  for (const k in keys) {
    const key: E[keyof E] = keys[k];
    result[key] = createActionTypeName<E>(key);
  }
  return result as unknown as ActionTypes<E>;
};

const createAction = <
  E extends { [key: string]: string },
  S extends MyState<E>,
  K extends E[keyof E] = E[keyof E],
>
(
  type: ActionTypes<E>[K],
  key: K,
): Action<E, S, K> => (
  payload: S[K]['value'],
): ReturnType<Action<E, S, K>> => ({
  type,
  key,
  payload,
});

const createActions = <
  E extends { [key: string]: string },
  S extends MyState<E>,
>(
  actionTypes: ActionTypes<E>,
  keys: E,
): Actions<E, S> => {
  const result: Partial<Actions<E, S>> = {};
  for (const k in keys) {
    const key: E[keyof E] = keys[k];
    const actionType = actionTypes[key];
    result[key] = createAction<E, S>(actionType, key);
  }
  return result as unknown as Actions<E, S>;
};

const createActionName = <
  E extends { [key: string]: string },
  K extends E[keyof E] = E[keyof E],
>(
  key: K,
): ActionName<E, K> => {
  const [first, ...letters] = key;
  const postFix = [first.toUpperCase(), ...letters].join('') as Capitalize<K>;
  return `update${postFix}`;
};

const createAliasedActions = <
  E extends { [key: string]: string },
  S extends MyState<E>,
>(
  actions: Actions<E, S>,
  keys: E,
): AliasedActions<E, S> => {
  const result: Partial<AliasedActions<E, S>> = {};
  for (const k in keys) {
    const key = keys[k];
    const name = createActionName<E>(key);
    result[name] = actions[key];
  }
  return result as unknown as AliasedActions<E, S>;
};

enum MyEnum {
  TEST = 'test1',
  TEST2 = 'test2',
}

const exampleState = {
  [MyEnum.TEST]: new Entry<string>(),
  [MyEnum.TEST2]: new Entry<number>(),
};

const actionTypes = createActionTypes<typeof MyEnum>(MyEnum);

const actions = createActions<typeof MyEnum, typeof exampleState>(actionTypes, MyEnum);

// This is the desired effect
const test = actions.test1('test');
const test2 = actions.test1(1); // TS2345: Argument of type '1' is not assignable to parameter of type 'string | undefined'.

// This is also the desired effect
const test3 = actions.test2('test'); // TS2345: Argument of type 'test' is not assignable to parameter of type 'number | undefined'.
const test4 = actions.test2(1); // okay

// After changing the object so that the functions are named based on the key, type information is lost.
const aliasedActions = createAliasedActions(actions, MyEnum);

// This is not the intended effect!
aliasedActions.updateTest1(1); // okay
aliasedActions.updateTest1('1'); // okay
aliasedActions.updateTest2(1); // okay
aliasedActions.updateTest2('1'); // okay

/*
    TS2345: Argument of type '{}' is not assignable to parameter of type 'string | number | undefined'.
      Type '{}' is not assignable to type 'number'.
 */
// Typescript correctly throws an error on this, showing that it's not considering the param "any"
aliasedActions.updateTest2({});
typescript typescript-generics typescript2.0
© www.soinside.com 2019 - 2024. All rights reserved.