I've got a function that takes a string and creates an object mapping the four CRUD actions to "action" strings containing the argument:
function createCrudActions(name: string) {
const record = name.toUpperCase();
return {
create: `CREATE_${record}`,
read: `READ_${record}`,
update: `UPDATE_${record}`,
delete: `DELETE_${record}`,
};
}
Rather than have the type of each property in the returned object be string
, I wanted to see if I could make them string literal types. I tried using template literal types to achieve this:
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");
But with this code, the TypeScript compiler doesn't see the function's return value as assignable to Actions<T>
—the object's properties are still string
. The error is:
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"'.
I tried using const assertions ( as const
) on each property value, as well as on the returned object itself, but the property types remain strings. Is there any way to do this without just casting the returned object ( as Actions<T>
)? If so, that would kind of defeat the purpose, so I'm hoping there's some way to make the compiler understand. But I think it might not be able to determine that the runtime toUpperCase
call corresponds to the Uppercase
transformation in the definition of Actions<T>
.
Edit: Another approach that is close but not quite what I want:
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");
But in this case the return type for `createCrudActions("post") is:
{
create: `CREATE_${record}`;
read: `READ_${record}`;
update: `UPDATE_${record}`;
delete: `DELETE_${record}`;
}
Whereas I'd like it to be:
{
create: `CREATE_POST`;
read: `READ_POST`;
update: `UPDATE_POST`;
delete: `DELETE_POST`;
}
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,
};
}
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
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.