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