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,
};
}