DEV Community

João Textor
João Textor

Posted on • Updated on

How to Write a Key Generator using TypeScript

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

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
Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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 = "-" | "_" | "." | " ";
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

Now let's write our interface, which will be implemented by our class.

export interface ICodeGenerator {
  numberOfCharacters?: number;
  generate: () => string[];
}
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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;
  }
Enter fullscreen mode Exit fullscreen mode

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 the charType is LettersAndNumbers.
  • 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;
  }
Enter fullscreen mode Exit fullscreen mode

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!");
}
Enter fullscreen mode Exit fullscreen mode

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'");
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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[] = [];
Enter fullscreen mode Exit fullscreen mode

Now we will create 3 nested for loops, in this order:

  1. The first to iterate on how many keys the user wants to be generated.
  2. The second will iterate on how many groups that key should have.
  3. 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;
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

After running this code, I got this:

[
  'STQ28-LYK19-AFK35',
  'JWK56-DLO89-QUO83',
  'NBC89-FAM05-LFO68',
  'XBS08-ZAX06-TOT61',
  'RUW52-KHC73-BVC16'
]
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
lionelrowe profile image
lionel-rowe

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) using crypto.getRandomValues like this:

const U32_MAX = Uint32Array.from([-1])[0]
const secureRandom = () => crypto.getRandomValues(new Uint32Array(1))[0] / (U32_MAX + 1)
Enter fullscreen mode Exit fullscreen mode

However, by far the simplest way of generating secure, random strings for use as API keys is by just using crypto.randomUUID:

Array.from({ length: 5 }, crypto.randomUUID.bind(crypto))
// sample output:
// [
//  "f72f6d1f-56b8-4e66-a069-2ce428ddedef",
//  "06a3a5f6-5baf-4a70-a286-61eaf17d52af",
//  "799fded3-0de4-463f-b7eb-d57c3b3c844b",
//  "bee1401a-2881-494f-980a-aff29abb0d9c",
//  "b0a3f915-48a7-490f-b904-6722173e1ccc"
// ]
Enter fullscreen mode Exit fullscreen mode

crypto.getRandomValues and crypto.randomUUID are available in all modern browsers, Deno, Bun, and NodeJS.

Collapse
 
joaotextor profile image
João Textor

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.