How to Write a Key Generator using TypeScript
Tags: typescript, webdev, tutorial, learning
Published: 2023-09-19 18:45:00

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)](https://www.youtube.com/watch?v=pb4ggtC1Mh8).

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](#file-structure)
* [Step 1: Defining the Types](#step-1)
* [Step 2: Creating the Key Generator Class](#step-2)
* [Step 3: Defining and implementing an auxiliary method](#step-3)
* [Step 4: Implementing our Main Method](#step-4)
* [Final Step: Generating Keys](#final-step)


## The File Structure <a name="file-structure"></a>

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 <a name="step-1"></a>

### 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:

```js
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`:

```js
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:


```js
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 <a name="step-2"></a>

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?

```js
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.

```js
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:

```js
export type Props = {
  characterType?: characterType;
  groups?: number;
  groupSeparator?: separatorType;
  numberOfKeys?: number;
  groupFormat?: string;
};
```

Finally, let's implement those two and create our class.

```js
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 <a name="step-3"></a>

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.

```js
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.

```js
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:

```js
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 <a name="step-4"></a>

The good stuff is here.
As before, I will split the code into separate blocks and briefly explain what I did.

```js
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.

```js
const groupFormatArr = this._groupFormat
  ? Array.from(this._groupFormat!)
  : [];

let keys: string[] = [];
```

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:

```js
  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 <a name="final-step"></a> 

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:

```js
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:

```js
const code = new KeyGenerator(4, props).generate();
```

After running this code, I got this:

```js
[
  '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](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.