Are you looking to create a custom key generator in TypeScript for your next project? Whether it's for generating secure API keys or unique identifiers, this type of function can be very useful.
In this step-by-step tutorial, I'll walk you through the process of how I built a TypeScript key generator from scratch.
The inspiration for this code came from this video (in Portuguese).
A disclaimer, though: as very well pointed in the comments, this is not a script for generating security keys (like API keys, etc). I got the idea for this from a YT in which they used an app for generating code for in-person sweepstakes. You can adapt it using Crypto
module from Node.js or another JS runtime so it can be secure.
Also, I know there's a lot of libraries out there for generating keys and security hashes, but I deliberately didn't search them before implementing my idea so it didn't become biased by other's ideas. In short, this small project served more as a challenge for myself than anything else, like making a robust key and secure generator, which was not the goal.
Be aware that this is my first post here on dev.to, and actually my first time trying to explain a code to others, which is harder than writing in the coding journal I keep to my future-self.
Also, it has been less than an year I started studying Javascript and Typescript, so there might be some ugly code.
Table of Contents
- The File Structure
- Step 1: Defining the Types
- Step 2: Creating the Key Generator Class
- Step 3: Defining and implementing an auxiliary method
- Step 4: Implementing our Main Method
- Final Step: Generating Keys
The File Structure
After I wrote this tutorial, I realized it could be confusing not knowing how the project was structured beforehand. So here are the files and folders of our project:
├── index.ts
└── utils
├── charactersByType.ts
└── types
├── characterType.ts
└── separatorType.ts
Step 1: Defining the Types
Character Types
Before we dive into the key generation logic, let's start by defining the types of characters we want to include in our keys.
I did this by creating a file called characterType.ts
inside the folder utils/types. Here's the code for this file:
export const charType = {
Letters: "Letters",
Numbers: "Numbers",
LettersAndNumbers: "LettersAndNumbers",
HexChar: "HexChar",
} as const;
export type characterType = keyof typeof charType;
In this code snippet, we defined a set of character types such as "Letters," "Numbers," "LettersAndNumbers," and "HexChar" using TypeScript's const assertion. We also create a type characterType which is a union of all the keys of the charType object. These character types will be crucial for generating diverse and customizable keys in later steps.
Character Sets
Now we'll create a file named charactersByType.ts
to define different character sets based on the characterType
selected. Here's the code for charactersByType.ts
:
export const charactersByType = {
Letters: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
LettersAndNumbers: "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
Numbers: "0123456789",
HexChar: "0123456789ABCDEF",
};
In this code snippet, we've defined four character sets:
Letters: Contains uppercase English letters.
LettersAndNumbers: Combines uppercase letters and numbers.
Numbers: Contains only numerical digits.
HexChar: Includes hexadecimal characters (0-9 and A-F).
These character sets will determine the available characters for generating keys.
Separators
We'll also define the separators that can be used to separate groups of characters within a key. Create a file named separatorType.ts
with the following code:
export type separatorType = "-" | "_" | "." | " ";
Here, we've defined a type separatorType
that represents four possible separators: hyphen (-), underscore (_), period (.), and space ( ). Feel free to include any characters you might want.
With our character sets and separators defined, we're ready to move on to the next step.
Step 2: Creating the Key Generator Class
Now that we have defined our character types, it's time to create the heart of our key generator: the CodeGenerator class. This class will allow us to generate keys based on various parameters such as the type of characters, the number of groups, separators, and more.
Let's create a index.ts
file in our root folder and import our types, shall we?
import { charactersByType } from "./utils/charactersByType";
import { characterType, charType } from "./utils/types/characterType";
import { separatorType } from "./utils/types/separatorType";
Now let's write our interface, which will be implemented by our class.
export interface ICodeGenerator {
numberOfCharacters?: number;
generate: () => string[];
}
Since we will receive optional parameters as well, let's create a type called Props
and pass the actual properties as optional parameters:
export type Props = {
characterType?: characterType;
groups?: number;
groupSeparator?: separatorType;
numberOfKeys?: number;
groupFormat?: string;
};
Finally, let's implement those two and create our class.
export default class CodeGenerator implements ICodeGenerator {
private _numberOfCharacters: number;
private _characterType?: characterType;
private _groupSeparator?: separatorType;
private _groups?: number;
private _groupFormat?: string;
private _numberOfKeys?: number;
constructor(numberOfCharacters: number, props: Props = {} as Props) {
this._numberOfCharacters = numberOfCharacters;
this._characterType = props?.characterType ?? charType.LettersAndNumbers;
this._groups = props?.groups ?? 1;
this._groupSeparator = props?.groupSeparator ?? "-";
this._numberOfKeys = props?.numberOfKeys ?? 1;
this._groupFormat = props?.groupFormat;
}
In our constructor
, I assigned a default value for props
, it being an empty object of type Props
. That way, Typescript won't complain about it being the wrong type.
Also, we had to assign default values to our props in case they are not passed by the user. I've done that using the nullish coalescing operator to ensure that whenever the value is null or undefined, it will be replaced by our default value.
Step 3: Defining and implementing an auxiliary method
As you can see in the ICodeGenerator
interface, we'll have a public method called generate
to generate the actual keys.
However, to make the code inside the method cleaner, we'll have one private method to assist us validating our group format (which can only be used in certain circumstances and need to contain only two types of characters).
So, let's first write the code for our abstraction, and afterwards the code of the generate
method, which will return an Array
of strings
.
validateGroupFormat method
The role of this private method is to validate the input of the groupFormat
property that can be informed by the user.
There are 2 rules we need to have in mind for this:
- The user can only use the
groupFormat
property when thecharType
isLettersAndNumbers
. - The
groupFormat
can only contain the letters "L" (for Letters) and "N" (for Numbers).
Also, we need to check if the groupFormat
is empty (the user didn't set the property), and if true, skip the entire validation.
Let's check how this method was implemented. I will split the code into 3 parts for better understanding.
private static validateGroupFormat(
groupFormat: string,
characters: string,
): void {
if (!groupFormat) {
console.log("groupFormat is not defined, skipping validation.");
return;
}
Here we've set the method, which will receive the groupFormat
and the characters
.
Also, our first rule has been set, checking if the groupFormat
is null or undefined. This will output on console a message and skip the validation by returning stopping the method.
You can suppress this console.log method if you wish. I decided to keep it for debugging purposes.
Let's continue writing our code. Now we will write the first validation rule.
if (characters != charactersByType.Letters) {
throw new Error("The grouptFormat can only be used with 'LettersAndNumbers' charaterType!");
}
This will check if the characters
is something other than LettersAndNumbers
. In other words, it checks if characters
is not Letters
, Numbers
or HexChar
, and throw an error if it is.
Now let's see our second and final rule:
const regexStatement = /[N|L]/g;
const matchLetters = groupFormat.match(regexStatement);
if (matchLetters!.length != groupFormat.length) {
throw new Error("The group format can only contain letters 'L' and numbers 'N'");
}
To check if the groupFormat
contains any letters or characters other than "L" and "N", I created a simple RegEx variable that will be used to verify this rule using the match()
method that exists in every String
.
This match
method returns an Array
containing all the characters that match the rule set using RegEx. We stored that array in matchLetters
.
Afterwards, we compare the length of matchLetters
with the length of the groupFormat
. If their length are the same, it means all letters are either "L" or "N". If the groupFormat
contains some character other than those, matchLetters
's length will be smaller than groupFormat
's length, since the Array will not contain those characters. In this case, we throw an error which will stop our app.
We are done with this auxiliary method. Now let's dive into the core of our little program: implementing the method for generating the actual keys.
Step 4: Defining and implementing our main method
The good stuff is here.
As before, I will split the code into separate blocks and briefly explain what I did.
public generate(): string[] {
let characters = charactersByType[this._characterType!];
const numberOfCharacters =
this._groupFormat?.length || this._numberOfCharacters;
CodeGenerator.validateGroupFormat(this._groupFormat!, characters);
First I declared and assigned the characters
variable by selecting the characters from inside the charactersByType
object. Note that we used the "non-null assertion operator" to ensure Typescript won't yell at us for passing a parameter that could potentially be undefined
(We know it won't because we have set a default parameter to it).
Then, a new rule emerges (and you can totally implement this in a different way): if groupFormat
is passed by the user, we will replace the property numberOfCharacters
, which is mandatory, with the length of the groupFormat
.
Here's why: Let's say a developer implement this and assign numberOfCharacters
a value of "5", and then pass the groupFormat
a value of "LLNLNLL" (7 characters).
Without this rule, the generate
method would result a key with 5 characters. However, to ensure the groupFormat
is actually respected and generate
generates a key with 7 characters, I decided to assign to numberOfCharacters
the length of that group, otherwise our loop won't have the necessary number of iterations to return all the characters needed.
On the other hand, if groupFormat
has less characters than passed in numberOfCharacters
, the problem would have been even worse. Hence the existence of this rule.
But considering this is a design preference, you can totally omit this rule and instead give priority to numberOfCharacters
as informed in the constructor of the class. However, to do so, you'll need to adapt the code to either not accept a groupFormat
with fewer characters, or define a rule on how the remaining characters would be generated.
But, let's get back on track. In the next line of code we'll call the validateGroupFormat
method to ensure it has been passed according to our set of rules.
Next, we will create an Array containing the characters in the groupFormat
, which will be used later, and another Array that will receive our actual generated keys.
const groupFormatArr = this._groupFormat
? Array.from(this._groupFormat!)
: [];
let keys: string[] = [];
Now we will create 3 nested for
loops, in this order:
- The first to iterate on how many keys the user wants to be generated.
- The second will iterate on how many groups that key should have.
- Finally, the last will iterate on how many characters the key will have.
At the end of the second for
loop, after our third loop finishes generating our group of keys, we will check if this is the last (or the only) group. If it's not, it will place our groupSeparator
at the end of the characters.
Inside the last for
loop, we will need to check if groupFormat
was informed, so we can generate the key according to it, and otherwise just generate a key with a random format.
If groupFormat
was informed, we will need to check, in each iteration, if the character generated needs to be a Letter
or a Number
, and reassign characters
accordingly.
Next, to generate the character, we will randomly select a number between 0 and the length of characters
(using Math.random
and Math.floor
), and use that number as an index for selecting a character inside characters
, using the charAt
method of Strings.
Finally, we will finalize the code by returning the keys.
Let's see how this was done:
for (let count = 0; count < this._numberOfKeys!; count++) {
let result: string = "";
for (let groups = 1; groups <= this._groups!; groups++) {
for (let i = 0; i < numberOfCharacters; i++) {
if (this._groupFormat) {
const char = groupFormatArr[i];
characters =
char === "L"
? (characters = charactersByType[charType.Letters])
: (characters = charactersByType[charType.Numbers]);
}
result += characters.charAt(
Math.floor(Math.random() * characters.length),
);
}
if (this._groups! - groups != 0) {
result += this._groupSeparator;
}
}
keys.push(result);
}
return keys;
}
This is the end of our code.
Final Step: Generating Keys
Now let's execute it inside another file:
I created a new file called test.js
that will import our class, set the props, create a new instance of the class and, finally, generate our keys.
Let's see how it works:
const KeyGenerator = require("./dist/index.js");
const props = {
characterType: "LettersAndNumbers",
groups: 3,
groupSeparator: "-",
numberOfKeys: 5,
groupFormat: "LLLNN",
};
const codeGenerator = new KeyGenerator(4, props);
const code = codeGenerator.generate();
console.log(code);
Here I wanted to create 5 keys containing 3 groups each. Each group will have the following format: "LLLNN", where "L" stands for "Letter" and "N" for "Number", as we saw earlier.
I did not inform the groupSeparator
, so the default is going to be used.
Notice I instantiated the class with the number 4, meaning I informed I wanted a key containing 4 characters, but I also informed a groupFormat
of 5 characters.
Finally, I logged created another variable to receive the result of the generate
method.
Notice you can also instantiate and generate the keys in a single line, like this:
const code = new KeyGenerator(4, props).generate();
After running this code, I got this:
[
'STQ28-LYK19-AFK35',
'JWK56-DLO89-QUO83',
'NBC89-FAM05-LFO68',
'XBS08-ZAX06-TOT61',
'RUW52-KHC73-BVC16'
]
Observe how we received the right amount of keys with 3 groups each, but each group has 5 characters instead of 4. This is due to our rule that the length of groupFormat
will have priority over the numberOfCharacters
.
Well, that's it for today.
The full and updated code can be seen here: https://github.com/joaotextor/easy-key-generator
Let me know your thoughts about my first post here on dev.to in the comment section. Was I clear enough? Are there any adjustments you might do in my code?
I hope to get back here soon with more content for y'all.
Top comments (2)
Math.random
is not cryptographically secure, so it's not a good idea to use it for generating secure keys.You can approximate a secure version of
Math.random
(random number in the range[0, 1)
usingcrypto.getRandomValues
like this:However, by far the simplest way of generating secure, random strings for use as API keys is by just using
crypto.randomUUID
:crypto.getRandomValues
andcrypto.randomUUID
are available in all modern browsers, Deno, Bun, and NodeJS.Very good explanation. I appreciate your warning on this.
I've used Crypto before to generate IDs, but didn't used because security was not one of the goals of this script.
Also, I forget to put a disclaimer about this, so thank you for sharing this concern.