Skip to content

Latest commit

 

History

History
285 lines (221 loc) · 12.8 KB

template-dsl.md

File metadata and controls

285 lines (221 loc) · 12.8 KB

Item 54: Use Template Literal Types to Model DSLs and Relationships Between Strings

Things to Remember

  • Use template literal types to model structured subsets of string types and domain-specific languages (DSLs).
  • Combine template literal types with mapped and conditional types to capture nuanced relationships between types.
  • Take care to avoid crossing the line into inaccurate types. Strive for uses of template literal types that improve developer experience without requiring knowledge of fancy language features.

////## Code Samples

type MedalColor = 'gold' | 'silver' | 'bronze';

💻 playground


type PseudoString = `pseudo${string}`;
const science: PseudoString = 'pseudoscience';  // ok
const alias: PseudoString = 'pseudonym';  // ok
const physics: PseudoString = 'physics';
//    ~~~~~~~ Type '"physics"' is not assignable to type '`pseudo${string}`'.

💻 playground


interface Checkbox {
  id: string;
  checked: boolean;
  [key: `data-${string}`]: unknown;
}

const check1: Checkbox = {
  id: 'subscribe',
  checked: true,
  value: 'yes',
// ~~~~ Object literal may only specify known properties,
//        and 'value' does not exist in type 'Checkbox'.
  'data-listIds': 'all-the-lists',  // ok
};
const check2: Checkbox = {
  id: 'subscribe',
  checked: true,
  listIds: 'all-the-lists',
// ~~~~~~ Object literal may only specify known properties,
//          and 'listIds' does not exist in type 'Checkbox'
};

💻 playground


interface Checkbox {
  id: string;
  checked: boolean;
  [key: string]: unknown;
}

const check1: Checkbox = {
  id: 'subscribe',
  checked: true,
  value: 'yes',  // permitted
  'data-listIds': 'all-the-lists',
};
const check2: Checkbox = {
  id: 'subscribe',
  checked: true,
  listIds: 'all-the-lists'  // also permitted, matches index type
};

💻 playground


const img = document.querySelector('img');
//    ^? const img: HTMLImageElement | null

💻 playground


const img = document.querySelector('img#spectacular-sunset');
//    ^? const img: Element | null
img?.src
//   ~~~ Property 'src' does not exist on type 'Element'.

💻 playground


interface HTMLElementTagNameMap {
  "a": HTMLAnchorElement;
  "abbr": HTMLElement;
  "address": HTMLElement;
  "area": HTMLAreaElement;
  // ... many more ...
  "video": HTMLVideoElement;
  "wbr": HTMLElement;
}

💻 playground


interface ParentNode extends Node {
  // ...
  querySelector<E extends Element = Element>(selectors: string): E | null;
  // ...
}

💻 playground


type HTMLTag = keyof HTMLElementTagNameMap;
declare global {
  interface ParentNode {
    querySelector<
      TagName extends HTMLTag
    >(
      selector: `${TagName}#${string}`
    ): HTMLElementTagNameMap[TagName] | null;
  }
}

💻 playground


const img = document.querySelector('img#spectacular-sunset');
//    ^? const img: HTMLImageElement | null
img?.src  // ok

💻 playground


const img = document.querySelector('div#container img');
//    ^? const img: HTMLDivElement | null

💻 playground


type CSSSpecialChars = ' ' | '>' | '+' | '~' | '||' | ',';
type HTMLTag = keyof HTMLElementTagNameMap;

declare global {
  interface ParentNode {
    // escape hatch
    querySelector(
      selector: `${HTMLTag}#${string}${CSSSpecialChars}${string}`
    ): Element | null;

    // same as before
    querySelector<
      TagName extends HTMLTag
    >(
      selector: `${TagName}#${string}`
    ): HTMLElementTagNameMap[TagName] | null;
  }
}

💻 playground


const img = document.querySelector('img#spectacular-sunset');
//    ^? const img: HTMLImageElement | null
const img2 = document.querySelector('div#container img');
//    ^? const img2: Element | null

💻 playground


// e.g. foo_bar -> fooBar
function camelCase(term: string) {
  return term.replace(/_([a-z])/g, m => m[1].toUpperCase());
}

// (return type to be filled in shortly)
function objectToCamel<T extends object>(obj: T) {
  const out: any = {};
  for (const [k, v] of Object.entries(obj)) {
    out[camelCase(k)] = v;
  }
  return out;
}

const snake = {foo_bar: 12};
//    ^? const snake: { foo_bar: number; }
const camel = objectToCamel(snake);
// camel's value at runtime is {fooBar: 12};
// we'd like the type to be {fooBar: number}
const val = camel.fooBar;  // we'd like this to have a number type
const val2 = camel.foo_bar;  // we'd like this to be an error

💻 playground


type ToCamelOnce<S extends string> =
    S extends `${infer Head}_${infer Tail}`
    ? `${Head}${Capitalize<Tail>}`
    : S;

type T = ToCamelOnce<'foo_bar'>;  // type is "fooBar"

💻 playground


type ToCamel<S extends string> =
    S extends `${infer Head}_${infer Tail}`
    ? `${Head}${Capitalize<ToCamel<Tail>>}`
    : S;
type T0 = ToCamel<'foo'>;  // type is "foo"
type T1 = ToCamel<'foo_bar'>;  // type is "fooBar"
type T2 = ToCamel<'foo_bar_baz'>;  // type is "fooBarBaz"

💻 playground


type ObjectToCamel<T extends object> = {
  [K in keyof T as ToCamel<K & string>]: T[K]
};

function objectToCamel<T extends object>(obj: T): ObjectToCamel<T> {
  // ... as before ...
}

💻 playground


const snake = {foo_bar: 12};
//    ^? const snake: { foo_bar: number; }
const camel = objectToCamel(snake);
//    ^? const camel: ObjectToCamel<{ foo_bar: number; }>
//                    (equivalent to { fooBar: number; })
const val = camel.fooBar;
//    ^? const val: number
const val2 = camel.foo_bar;
//                 ~~~~~~~ Property 'foo_bar' does not exist on type
//                         '{ fooBar: number; }'. Did you mean 'fooBar'?

💻 playground