Notes on generics and enums in TS

This commit is contained in:
thomasabishop 2022-07-06 09:08:37 +01:00
parent a18f113ff3
commit 56ccb2ce1d
3 changed files with 121 additions and 78 deletions

View file

@ -0,0 +1,120 @@
---
tags:
- Programming_Languages
- typescript
---
# Enums
In essence an `enum` in TypeScript is an incremented store of immutable variables. The only way I can understand them is just to think of them as a bag of constants.
Below is an example of an `enum`:
```ts
enum Continents {
NorthAmerica,
SouthAmerica,
Africa,
Asia,
Europe,
Antartica,
Australia,
}
// usage
var region = Continents.Africa;
```
Here is an example of a string-based enum from the Studio codebase:
```tsx
export enum HelpLinksEnum {
composerView = 'composerView',
conditionalHelp = 'conditionalHelp',
configView = 'configView',
dashboard = 'dashboard',
dataView = 'dataView',
functionHelp = 'functionHelp',
previewMode = 'previewMode',
}
```
### Main properties
- **Auto-incrementation (numeric enums only):**
- Enums are auto-incrementing.
- If we wanted to change the automatic sequence we could do something like the below and TS would automatically update the index to reflect our change
```ts
enum NewProjectMenuItems {
Row = 5
Table // would automatically become `6` in the index
}
```
- **Enums are types**
- Enums are interpreted as types by TS. Consequently we can an enum as a type annotation. This can be particularly useful when typing function parameters. For example:
```ts
enum UserResponse {
No = 0,
Yes = 1,
}
function respond(recipient: string, message: UserResponse): void {
// ...
}
```
- **Enums accept computed values**
- As is the case with objects generally, we are not limited to primitive data types when setting values on enums, we can also assign functions.
- **Reverse mapping**
- Reverse mapping is the ability to access a key via its value in addition to the more standard practice of accessing a value via its key.
- Demonstration:
```ts
enum PrintMedia {
Newspaper = 1,
Newsletter,
Magazine,
Book,
}
PrintMedia.Magazine; // returns 3
PrintMedia['Magazine']; // returns 3
PrintMedia[3]; // returns Magazine
```
- **Useful in switch statements**
- Enums come in handy when you want to reduce the verbosity and repetition of a large switch statement.
- The following is an example from the codebase:
```tsx
public projectMenuItemClickHandler(e: IUserProjectMenuItemClick): void {
switch (e.menuItem) {
case UserProjectMenuItems.Rename:
e.item.rename = true;
break;
case UserProjectMenuItems.Export:
this.exportProject(e.item);
break;
case UserProjectMenuItems.Clone:
this.cloneProject(e.item);
break;
}
}
```
### Main benefits
More generally, by using enums we ensure that a given data structure is preserved unaltered and intact throughout the codebase which is very useful when multiple developers are working with the same classes and components. This creates a high-level canonical form of a given data structure that may be used in multiple contexts. This reduces the likelihood of different developers 'reinventing the wheel' and duplicating common data structures, helping to promote modularity and minimise technical debt.
### Constraints and best practices
There are some important constraints that should be borne in mind when using enums:
- **Initialise string enums!**
- String enums must be initialised with a string or a reference to another enum. They cannot be tacitly defined as is the case with numeric enums
- **Don't mix data types!**
- It is technically possible to construct an enum that contains both numbers and strings but this should be avoided as it can lead to unintended side-effects in the incrementation process. It also kind of undermines the purpose of enums. In this scenario, a custom type would probably be more appropriate.
- **Strings only as keys**
- In contrast to a `Map` or object an enum cannot have a numeric key. It must always be a string.

View file

@ -2,6 +2,7 @@
tags:
- Programming_Languages
- typescript
- functions
---
# Functions

View file

@ -61,71 +61,8 @@ Note that even though the function in question does not express any preference f
> In the generic function we have used `T` as our placeholder for a generic type as this is the convention. However there is no compunction to do so. We could have used any letter or string, providing that the string is not a reserved term.
## Second example
This example extends the first and demonstrates how generics can help to reduce repetitive code whilst also providing type safety.
Imagine we have a single object that can be classified against multiple data sets. For instance a book may be translated into different languages, be available in a variety of formats, and be published in multiple countries. Let's create custom types reflecting this:
```ts
type Translations = {
[english: string]: string;
german: string;
french: string;
};
type Publishers = {
[usa: string]: string;
uk: string;
germany: string;
};
type Formats = {
[hardback: string]: boolean;
paperback: boolean;
audio: boolean;
};
```
Now let's say we have a book _Dune_ that is a database entry corresponding to the `Translations` type
```ts
const dune: Translations = {
english: 'https://www.amazon.com/dune',
german: 'https://www.amazon.de/dune',
french: 'https://www.amazon.fr/dune',
};
```
Our users want to be able to quickly check different properties of a given book in the database. To allow them to check for specific translations we might write a utility function:
```ts
function isTranslationAvailable(
database: Translations,
translation: string | number,
): translation is keyof Translations {
return translation in database;
}
```
We would expect `isTranslationAvailable(dune, 'german')` to return `true` and `isTranslationAvailable(dune, 'hebrew')` to return `false`.
Next we want to see if _Dune_ is available as audio book, we could adapt `isTranslationAvailable` :
```tsx
function isFormatAvailable(database: Formats, format: string | number): format is keyof Formats {
return format in database;
}
```
This is clearly sub-optimal: we require a different lookup function for every property type yet the logic is identical. This is unnecessarily verbose and insufficiently abstracted. It is also not very adaptable in that for each new property set we need to create a different function. We can imagine that if the list of possible properties were to grow over time, additional code would need to be written and existing references updated.
The solution then, is to genericise the utility function so tht
## Another example
---
This example demonstrates how we can use generics to reduce repetition when writing functions and is also a more realistic use case.
Let's say we have two types or interfaces:
@ -144,16 +81,6 @@ type SubtitleFormatUrls = {
};
```
<aside>
💡 `URL` is a built-in Object in JavaScript and can be accessed via the URL constructor.
</aside>
```tsx
let m = 'https://developer.mozilla.org';
let a = new URL('/', m);
```
An example of an object matching these type definitions:
```tsx
@ -171,11 +98,6 @@ function isFormatAvailable(obj: VideoFormatUrls, format: string): format is keyo
}
```
<aside>
💡 Note that in the above function we have used a **type predicate** in the return signature → **add notes about this**.
</aside>
Now imagine that we need to do the same thing with subtitles, but given that `isFormatAvailable()` is typed to the `VideoFormatUrls` type we would get an error if we used this function for subtitles. But we also don't want to write a near identical function typed to `SubtitleFormatUrls` to subtitles just to ensure adequate type safety.
Alternatively we could use a union type, for example: