Infer string literal type from string manipulation

Solution 1:

I cajoled this to work with as const on all the properties and casting the toUpperCase() return value as Uppercase<T>; at that point, though, it's not that much better than as Actions<T>. Technically this validates the transformation as correct, but the code this protects is unlikely to change and the code that consumes it is equally well-protected from type errors.

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

Solution 2:

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

Consider this example:

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;

It is clear why we have an error here. Because toUpperCase returns string whereas we want to operate on Uppercase<T>.

But why overloading works in this case? Function overloading acts bivariantly, it means that it compiles if overdload is assignable to function type signature or vice versa. Of course, we loose type strictness but gain flexibility.

See this exmaple, without overloading:


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 is [false, true]. Since Result has at least one true, function overloading should work.

Version with overloading:

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");

Try to add extra underscore to Actions utility type:

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

Now, overloading is not assignable to function type signature because non of the types are not assignable to each other.

However, you can move upercasing to a separate function. In this way you will create only a little piece of unsafe code whereas your main function will be safe. When I say safe I mean : 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

Now you don't even need Actions type because TS is able to infer all types on its own