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.

keyboard pitch scientific pitch notation

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).