从字符串操作推断字符串文字类型

Infer string literal type from string manipulation

我有一个函数,它接受一个字符串并创建一个对象,将四个 CRUD 操作映射到包含参数的“操作”字符串:

function createCrudActions(name: string) {
  const record = name.toUpperCase();

  return {
    create: `CREATE_${record}`,
    read: `READ_${record}`,
    update: `UPDATE_${record}`,
    delete: `DELETE_${record}`,
  };
}

我不想让 returned 对象中的每个 属性 的类型为 string,而是想看看是否可以将它们设为字符串文字类型。我尝试使用模板文字类型来实现这一点:

type CrudActions = "create" | "read" | "update" | "delete";

type Actions<T extends string> = {
  [K in CrudActions]: `${Uppercase<K>}_${Uppercase<T>}`;
}

function createCrudActions<T extends string>(name: T) {
  const record = name.toUpperCase();

  return {
    create: `CREATE_${record}`,
    read: `READ_${record}`,
    update: `UPDATE_${record}`,
    delete: `DELETE_${record}`,
  };
}

const postActions: Actions<"post"> = createCrudActions("post");

但是对于这段代码,TypeScript 编译器不会将函数的 return 值视为可分配给 Actions<T>——对象的属性仍然是 string。错误是:

Type '{ create: string; read: string; update: string; delete: string; }' is not assignable to type 'Actions<"post">'. Types of property 'create' are incompatible. Type 'string' is not assignable to type '"CREATE_POST"'.

我尝试对每个 属性 值以及 returned 对象本身使用 const 断言 (as const),但是 属性 类型仍然是字符串.有什么方法可以不只转换 returned 对象 (as Actions<T>) 来做到这一点?如果是这样,那将有点违背目的,所以我希望有某种方法可以让编译器理解。但我认为它可能无法确定运行时 toUpperCase 调用对应于 Actions<T>.

定义中的 Uppercase 转换

TS Playground

编辑:另一种接近但不完全是我想要的方法:

type CrudActions = "create" | "read" | "update" | "delete";

type ActionCreator = (s: string) => { [K in CrudActions]: `${Uppercase<K>}_${Uppercase<typeof s>}` };

const createCrudActions: ActionCreator = <T extends string>(name: T) => {
  const record = name.toUpperCase();

  return {
    create: `CREATE_${record}`,
    read: `READ_${record}`,
    update: `UPDATE_${record}`,
    delete: `DELETE_${record}`,
  };
}

const postActions = createCrudActions("post");

但在这种情况下,`createCrudActions("post") 的 return 类型是:

{
  create: `CREATE_${record}`;
  read: `READ_${record}`;
  update: `UPDATE_${record}`;
  delete: `DELETE_${record}`;
}

而我希望它是:

{
  create: `CREATE_POST`;
  read: `READ_POST`;
  update: `UPDATE_POST`;
  delete: `DELETE_POST`;
}

TS Playground

我哄骗它在所有属性 上使用 as const 铸造 toUpperCase() return 值 as Uppercase<T>;不过,在这一点上,它并不比 as Actions<T> 好多少。从技术上讲,这验证了转换是正确的,但它保护的代码不太可能更改,并且使用它的代码同样受到类型错误的保护。

function createCrudActions<T extends string>(name: T) {
  const record = name.toUpperCase() as Uppercase<T>;

  return {
    create: `CREATE_${record}` as const,
    read: `READ_${record}` as const,
    update: `UPDATE_${record}` as const,
    delete: `DELETE_${record}` as const,
  };
}

Playground Link

Do you know why the overload is required in this case, as opposed to just specifying the return type of the function?

考虑这个例子:

type CrudActions = "create" | "read" | "update" | "delete";

type Actions<T extends string> = {
  [K in CrudActions]: `${Uppercase<K>}_${Uppercase<T>}`;
}

function createCrudActions<T extends string>(name: T):Actions<T> {
  /**
   * toUpperCase returns string instead of Uppercase<T>,
   * hence `CREATE_${record}` is now `CREATE_${string}` whereas you
   * want it to be `CREATE_${Uppercase<T>}`
   */
  const record = name.toUpperCase();

  return {
    create: `CREATE_${record}`,
    read: `READ_${record}`,
    update: `UPDATE_${record}`,
    delete: `DELETE_${record}`,
  } as const; // error
}

const postActions = createCrudActions("post").create;

很清楚为什么我们这里有错误。因为 toUpperCase returns string 而我们要对 Uppercase<T>.

进行操作

但为什么重载在这种情况下有效?函数重载是双变的,这意味着如果 overdload 可分配给函数类型签名,它就会编译,反之亦然。当然,我们放宽了类型严格性但获得了灵活性。

看这个例子,没有重载:


function createCrudActions<T extends string>(name: T) {

  const record = name.toUpperCase();

  const result = {
    create: `CREATE_${record}`,
    read: `READ_${record}`,
    update: `UPDATE_${record}`,
    delete: `DELETE_${record}`,
  } as const;

  return result
}

const result = createCrudActions("post");

type Check1<T extends string> = typeof result extends Actions<T> ? true : false

type Check2<T extends string> = Actions<T> extends typeof result ? true : false

type Result = [Check1<'post'>, Check2<'post'>]

Result[false, true]。由于 Result 至少有一个 true,函数重载应该可以工作。

重载版本:

type CrudActions = "create" | "read" | "update" | "delete";

type Actions<T extends string> = {
  [K in CrudActions]: `${Uppercase<K>}_${Uppercase<T>}`;
}

function createCrudActions<T extends string>(name: T): Actions<T>
function createCrudActions<T extends string>(name: T) {
  const record = name.toUpperCase();

  return {
    create: `CREATE_${record}`,
    read: `READ_${record}`,
    update: `UPDATE_${record}`,
    delete: `DELETE_${record}`,
  } as const;
}

const result = createCrudActions("post");

尝试将额外的 underscore 添加到 Actions 实用程序类型:

type Actions<T extends string> = {
  [K in CrudActions]: `_${Uppercase<K>}_${Uppercase<T>}`;
}

现在,重载不能分配给函数类型签名,因为所有类型都不能相互分配。

但是,您可以将 upercasing 移动到一个单独的函数。通过这种方式,您将只创建一小段不安全代码,而您的主要功能将是 safe。当我说 safe 时,我的意思是:as much as TS allows it to be safe.

type CrudActions = "create" | "read" | "update" | "delete";

const uppercase = <T extends string>(str: T) => str.toUpperCase() as Uppercase<T>;

function createCrudActions<T extends string>(name: T) {
  const record = uppercase(name)

  return {
    create: `CREATE_${record}`,
    read: `READ_${record}`,
    update: `UPDATE_${record}`,
    delete: `DELETE_${record}`,
  } as const;
}

const result = createCrudActions("post").create

现在您甚至不需要 Actions 类型,因为 TS 能够自行推断出所有类型