Ok. So. The honeymoon phase is over. I can say TypeScript is steadily becoming a part of my daily stack. While working on converting music-fns from Flow to TypeScript I bumped into a feature I didn’t know existed. But first, a little bit of context.
music-fns is a utility library that provides a set of functions to work with music notation. You can generate chords, melodies, calculate intervals, frequencies, etc. I like to pitch it (pun unintended) as “lodash for music.”
Internally, I have a noteToObject function that parses a note (written in scientific pitch notation) to an object. That object contains the note, accidental, and octave information that I use for various operations.
Some examples of valid scientific pitch notation:
- A
- Ab
- A#4
- B3
- F♯2
- G♭
In my codebase, I would type this as a string.
type ScientificNote = string;
But, by using template literal types, we can narrow it down — a lot. This feature introduced in TypeScript 4.1 enables us to use template literals when constructing types.
A valid scientific note contains:
- a note (A – G): C – D – E – F – G – A – B, where C is a “Do”.
and optionally
- an accidental: symbol or letter equivalent (for convenience)
- an octave: a number
Let’s construct our new type.
type Flat = 'b'| '♭'
type Sharp = '#'| '♯'
type Accidental = Flat | Sharp
type Octave = number
type Note = 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G'
type ScientificNote = `${Note}${Accidental | ''}${Octave | ''}`
const note1: ScientificNote = 'Ab3' // VALID
const note2: ScientificNote = 'G' // VALID
const note3: ScientificNote = '333' // NOT VALID
const note4: ScientificNote = 4 // NOT VALID
const note5: ScientificNote = false // NOT VALID
const note6: ScientificNote = 'H#' // NOT VALID
The type above covers a lot but fails to catch ‘impossible notes’.
Have a look at the black keys of a piano. The white keys are natural notes, the black keys are accidentals. There, you don’t have a sharp between B and C & between E and F (although this might trigger some discussions. I might revisit this decision later). If you know the rules (notes + locate the ‘middle C’) it should be easy enough to translate notation to keys on a piano.
Since every sharp (‘#’| ‘♯’) can be translated to a flat (‘b’| ‘♭’). We have 4 ‘impossible notes’. By using Exclude and template literal types we can cover this with TypeScript.
type Flat = 'b'| '♭'
type Sharp = '#'| '♯'
type Accidental = Flat | Sharp
type Octave = number
type Note = 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G'
type ImpossibleNotes = `${'E' | 'B'}${Sharp}` | `${'F' | 'C'}${Flat}`;
export type ScientificNoteWithAccidental = Exclude<
`${Note}${Accidental}`,
ImpossibleNotes
>;
export type ScientificNote = `${ScientificNoteWithAccidental | Note}${
| Octave
| ''}`;
const note1: ScientificNote = 'Ab3' // VALID
const note2: ScientificNote = 'G' // VALID
const note3: ScientificNote = '333' // NOT VALID
const note4: ScientificNote = 4 // NOT VALID
const note5: ScientificNote = false // NOT VALID
const note6: ScientificNote = 'H#' // NOT VALID
const note7: ScientificNote = 'B#' // NOT VALID
const note8: ScientificNote = 'B♯3' // NOT VALID
const note9: ScientificNote = 'F♭3' // NOT VALID
Have fun experimenting with this new and powerful feature!
PS: There is also an issue open for RegExp type definitions that is worth checking out (just imagine the possibilities).
Member discussion