0

I have defined an enum in Prisma (so not a TypeScript enum), and I'm wondering if I can sync a TypeScript String enum to the generate Type created by the Prisma Client. Here are the relevant details.

export const GroupInvitationStatus: {
  Pending: 'Pending',
  Accepted: 'Accepted',
  Declined: 'Declined'
};

export type GroupInvitationStatus = (typeof GroupInvitationStatus)[keyof typeof GroupInvitationStatus]

When I Import GroupInvitationStatus from the Prisma Client, I see that it is:

(alias) type GroupInvitationStatus = "Pending" | "Accepted" | "Declined"

My goal is to make sure that in a defined TypeScript string enum that I create, that every possible value from the Prisma enum (as referenced through the imported type above) is specified in the TypeScript enum.

Is this possible - I've read the TypeScript documentation on enums and have searched for a solution, but so far haven't found one.

Is it overkill? Should I just consider using the type directly and skipping the enum?

Edit 1 to add: I found this answer, which seems more or less like it does what I need it to.

Is there a way to dynamically generate enums on TypeScript based on Object Keys?

However, I can't seem to get TypeScript to "know" when changes are made to the Prisma Client, which exists in the node modules.

So this solution, while I think it's better, doesn't help my case any. I'm beginning to think I can just use the generated const instead of the type as an enum - it seems functionally identical.

New edit: Here is a relevant Code Sandbox, where I define a "prisma-client.ts" which is a spoofed representation of exports from a node module in my project, and "target-file.enum.ts" where I use the exports from the Prisma Client.

https://codesandbox.io/s/sweet-curran-n7ruyt?file=/src/target-file.enum.ts

Michael Jay
  • 503
  • 3
  • 15
  • Just to be clear, this *isn't* an enum - that's something different in TS. What you have here is a union type. – Ben Wainwright Jul 29 '22 at 12:19
  • I know - I'm trying to dynamically generate an enum from the type. Although as I mention in my edit, I think that the const is going to actually be sufficient for me. – Michael Jay Jul 29 '22 at 12:30
  • 1
    What is "the imported type from the prisma client"? Are you importing an actual union type from an npm package? If so, please include the actual type import statement so that the types in question can be examined. – jsejcksn Jul 29 '22 at 12:46
  • Fair ask, @jsejcksn. I included a Code Sandbox link in the original question now. – Michael Jay Jul 29 '22 at 12:55
  • Thanks for the clarification. I plan to compose an answer for you in just a bit. – jsejcksn Jul 29 '22 at 13:33

2 Answers2

0

Reproduced in Typescript playground

It should work as described here.

which is similar like putting this in a .d.ts file:

declare const GroupInvitationStatus: {
  Pending: 'Pending',
  Accepted: 'Accepted',
  Declined: 'Declined'
};

this is working like a enum. I would not recommend to write a duplicate Enum for your own.

schloemi
  • 41
  • 1
  • 5
  • 1
    GroupInvitationStatus is defined as a const and as a type. This is generated by Prisma, and doesn't seem to break anything. In a TS file that imports GroupInvitationStatus, it even seems to know which is used for which based on how the import is used. – Michael Jay Jul 29 '22 at 12:29
  • I dont know why it works now but i found a way that Typescript accepts your version - thanks! – schloemi Jul 29 '22 at 12:38
  • Is it because they're just exports and not variable declarations, and since they're different types (const vs type), there's no problem? This is a curious one! – Michael Jay Jul 29 '22 at 12:41
  • In my above comment, I say GroupInvitation status is DEFINED, but they're actually just EXPORT statements - so I guess that's why it's okay? – Michael Jay Jul 29 '22 at 12:42
0

Below I'll detail a technique for creating what I call "synthetic string enums": runtime objects which have readonly keys and values equal to each other, derived from a tuple of string literals. The types involved in the creation steps of each of these can be constrained during the process, informed by an existing string union (your import). It's definitely verbose (but that's just the nature of TypeScript), but allows for flexibility and the type-correctness assurance that you're after.

What makes this whole thing possible is a higher-order function which uses a generic type constraint to produce an identity function whose input parameter is constrained by the original type.

I've commented the code heavily, so that you can have documentation in your codebase if you decide to use it. If anything is unclear (or perhaps I made a typo, etc.) feel free to ask for clarification in a comment.

I encourage you to view the code in the TypeScript Playground because of its IntelliSense and editing features.

// import type {GroupInvitationStatus} from 'some_module_specifier';

// Example of what's being imported in the commented statement above:
type GroupInvitationStatus = 'Accepted' | 'Declined' | 'Pending';

/**
 * Returns a new identity function which accepts and returns a value,
 * but will create a compiler error if the value does not extend
 * the constraining type provided to this function
 */
function createConstrainedIdentityFn <Constraint = never>(): <T extends Constraint>(value: T) => T {
  return value => value;
}

/** A very close approximation of the type of runtime object that is TS's string enum */
type StringEnumObj<StrUnion extends string> = { readonly [S in StrUnion]: S };

/** Takes a tuple of strings and creates an object with a key-value pair for each string (that's equal to it) */
function objectFromTuple <T extends readonly string[]>(
  strings: T,
): StringEnumObj<T[number]> {
  return Object.fromEntries(strings.map(s => [s, s])) as StringEnumObj<T[number]>; // Has to be asserted
}

// This is the only part that you'll need to "manually" update. It's also valuable
// to define it separately so that you can use the string literals to iterate keys
// in a type-safe way in other parts of your code
const groupInvitationStatusTuple = createConstrainedIdentityFn<readonly GroupInvitationStatus[]>()(
  //                                         The first invocation returns the identity function ^ ^
  //                              The second invocation is where you provide the input argument >>^
  ['Accepted', 'Declined', 'Pending'] as const,
  //                                  ^^^^^^^^
  // Be sure to use "as const" (a const assertion) so that this is inferred as
  // a tuple of string literals instead of just an array of strings
);

// If you try to use it with a value that's not in the imported union, you'll get a compiler error.
// This prevents excess member types:
createConstrainedIdentityFn<readonly GroupInvitationStatus[]>()(['Accepted', 'Declined', 'Pending', 'nope'] as const); /*
                                                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Argument of type 'readonly ["Accepted", "Declined", "Pending", "nope"]' is not assignable to parameter of type 'readonly GroupInvitationStatus[]'.
  Type '"Accepted" | "Declined" | "Pending" | "nope"' is not assignable to type 'GroupInvitationStatus'.
    Type '"nope"' is not assignable to type 'GroupInvitationStatus'.(2345) */

// Note that this doesn't guard against missing union constituents,
// but that'll be addressed in the next part, so don't worry.
createConstrainedIdentityFn<readonly GroupInvitationStatus[]>()(['Accepted', 'Declined'] as const); // Ok, but it'd be nice if this were an error

// Create the synthetic string enum runtime object. Shadowing the string union import name is absolutely fine
// because type names and value names can co-exist in TypeScript (since all types are erased at runtime).
// This is ideal in your case because it actually helps reproduce additional string enum-like behavior
// that I'll cover at the end.
const GroupInvitationStatus = createConstrainedIdentityFn<StringEnumObj<GroupInvitationStatus>>()(
  //                                                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  // This is the type that you ultimately want, and it's used to constrain what can go into the identity function
  objectFromTuple(groupInvitationStatusTuple),
);

// If you try to create it with a tuple that has missing union constituents, you'll get a compiler error:
createConstrainedIdentityFn<StringEnumObj<GroupInvitationStatus>>()(
  objectFromTuple(['Accepted', 'Declined'] as const), /*
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Argument of type 'StringEnumObj<"Accepted" | "Declined">' is not assignable to parameter of type 'StringEnumObj<GroupInvitationStatus>'.
Property 'Pending' is missing in type 'StringEnumObj<"Accepted" | "Declined">' but required in type 'StringEnumObj<GroupInvitationStatus>'.(2345) */
);

// Note that it doesn't guard against excess values,
// but that was already covered during the tuple creation step above.
createConstrainedIdentityFn<StringEnumObj<GroupInvitationStatus>>()(
  objectFromTuple(['Accepted', 'Declined', 'Pending', 'nope'] as const), // Ok, but it'd be nice if this were an error
);


// Let's examine the shape of the created synthetic string enum runtime object:
GroupInvitationStatus.Accepted; // "Accepted"
GroupInvitationStatus.Declined; // "Declined"
GroupInvitationStatus.Pending; // "Pending"

// And in the console:
console.log(GroupInvitationStatus); // { Accepted: "Accepted", Declined: "Declined", Pending: "Pending" }
// Looks good!


// Lastly, some usage examples (data and types, just like a string enum from TS):

function printStatus (status: GroupInvitationStatus): void {
  console.log(status);
}

printStatus(GroupInvitationStatus.Accepted); // OK, prints "Accepted"
printStatus(GroupInvitationStatus.Pending); // OK, prints "Pending"

// Using valid string literals is also ok with this technique:
printStatus('Declined'); // OK, prints "Declined"

// Using an invalid type produces an expected and welcome error:
printStatus('Debating'); /*
            ~~~~~~~~~~
Argument of type '"Debating"' is not assignable to parameter of type 'GroupInvitationStatus'.(2345) */


// Iterating keys:

// You can't use the "keys"/"entries" etc. methods
// on Object to iterate keys in a type-safe way:
for (const key of Object.keys(GroupInvitationStatus)) {
  key; // string
}

// But you CAN use the tuple above:
for (const key of groupInvitationStatusTuple) {
  key; // "Accepted" | "Declined" | "Pending"
}

jsejcksn
  • 27,667
  • 4
  • 38
  • 62