TS

TypeScript Fundamentals

17 lessons

Progress0%
1. Introduction to TypeScript
1What is TypeScript?2Setting Up TypeScript
2. Basic Types
1Primitive Types2Interfaces and Type Aliases
3. Functions and Generics
Typed FunctionsGenerics
4. Classes and OOP
ClassesInheritance and Interfaces
5. Advanced Types
Union and Intersection TypesUtility Types
6. Modules and Decorators
ES Modules
7. Decorators
Class & Method DecoratorsProperty & Parameter Decorators
8. Declaration Files
Writing .d.ts Files@types & DefinitelyTyped
9. Advanced Patterns
Conditional Types & inferTemplate Literal Types & satisfies
All Tutorials
TypeScriptAdvanced Patterns
Lesson 17 of 17 min
Chapter 9 · Lesson 2

Template Literal Types & satisfies

TypeScript 4.1 introduced template literal types, bringing the expressiveness of JavaScript template strings into the type system. TypeScript 4.9 followed with the satisfies operator, which validates a value against a type while preserving the most precise inferred type.

Template Literal Types

Template literal types use the same backtick syntax as JavaScript template strings, but at the type level:

typescript
type Greeting = `Hello, ${"Alice" | "Bob"}!`;
// "Hello, Alice!" | "Hello, Bob!"

When used with unions, they cross-product all combinations:

typescript
type Axis = "x" | "y" | "z";
type CSSTransform = `translate${Capitalize<Axis>}`;
// "translateX" | "translateY" | "translateZ"

Intrinsic String Manipulation Types

TypeScript provides four built-in types for transforming string literal types:

  • Uppercase<S> — "hello" → "HELLO"
  • Lowercase<S> — "HELLO" → "hello"
  • Capitalize<S> — "hello" → "Hello"
  • Uncapitalize<S> — "Hello" → "hello"

Mapped Types with Template Literals

Combining mapped types and template literals produces powerful object transformations:

typescript
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

The satisfies Operator

Before satisfies, you had two choices: annotate a variable (loses literal type) or leave it unannotated (no type checking). satisfies gives you both:

typescript
type ColorMap = Record<string, string>;

// Annotation — loses the literal key types
const colors1: ColorMap = { red: "#f00", green: "#0f0" };
colors1.red.toUpperCase(); // TS doesn't know it's a string — wait, it does here

// satisfies — validates shape AND preserves literal types
const colors2 = {
  red: "#f00",
  green: [0, 255, 0],
} satisfies Record<string, string | number[]>;

colors2.red.toUpperCase();   // string methods available — red is string
colors2.green.map(v => v);   // array methods available — green is number[]

const Assertions

as const freezes a value's type to its narrowest literal form and marks everything readonly:

typescript
const routes = ["home", "about", "contact"] as const;
// readonly ["home", "about", "contact"]  — not string[]

as const satisfies

Combining both operators validates the shape while preserving literal types and readonly status — the best of all worlds:

typescript
const config = {
  env: "production",
  port: 3000,
} as const satisfies { env: "development" | "production"; port: number };

Template literal types and satisfies are two of TypeScript's most ergonomic recent additions, dramatically reducing the need for type casts while keeping inference sharp.

Code Examples

CSS Property Names with Template Literal Typestypescript
type CSSUnit = "px" | "rem" | "em" | "%" | "vw" | "vh";
type CSSValue = `${number}${CSSUnit}`;

type Side = "top" | "right" | "bottom" | "left";
type CSSMargin = `margin-${Side}`;
// "margin-top" | "margin-right" | "margin-bottom" | "margin-left"

type CSSPadding = `padding-${Side}`;

type FlexDirection = "row" | "column";
type FlexVariant = `flex-${FlexDirection}` | `inline-${FlexDirection}`;
// "flex-row" | "flex-column" | "inline-row" | "inline-column"

// Getters pattern using mapped type + template literal
interface Config {
  width: number;
  height: number;
  label: string;
}

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type ConfigGetters = Getters<Config>;
// { getWidth: () => number; getHeight: () => number; getLabel: () => string }

// Runtime example
function createGetters<T extends object>(obj: T): Getters<T> {
  return Object.fromEntries(
    Object.entries(obj).map(([k, v]) => [
      `get${k[0].toUpperCase()}${k.slice(1)}`,
      () => v,
    ])
  ) as Getters<T>;
}

const cfg: Config = { width: 800, height: 600, label: "canvas" };
const getters = createGetters(cfg);
console.log(getters.getWidth());
console.log(getters.getHeight());
console.log(getters.getLabel());

Template literal types cross-product string unions to produce all valid CSS property name variants. The Getters mapped type combines keyof with Capitalize and a template literal to rename every property to its getter form.

satisfies vs Type Annotationtypescript
type Theme = {
  colors: Record<string, string | [number, number, number]>;
  spacing: Record<string, number>;
};

// --- With type annotation ---
// TypeScript widens each value to string | [number, number, number]
const annotated: Theme = {
  colors: { primary: "#3b82f6", accent: [59, 130, 246] },
  spacing: { sm: 4, md: 8, lg: 16 },
};
// annotated.colors.primary is string | [number,number,number]
// We must narrow before using string methods

// --- With satisfies ---
// TypeScript validates the shape but keeps the PRECISE inferred type
const theme = {
  colors: { primary: "#3b82f6", accent: [59, 130, 246] as [number, number, number] },
  spacing: { sm: 4, md: 8, lg: 16 },
} satisfies Theme;

// theme.colors.primary is string  ← no narrowing needed
// theme.colors.accent  is [number, number, number]
console.log(theme.colors.primary.toUpperCase());
console.log(theme.colors.accent.map((c) => c * 2));
console.log(theme.spacing.md);

// satisfies also catches shape violations at the point of definition
// Uncommenting the line below would be a compile error:
// const bad = { colors: { x: 123 } } satisfies Theme;
//                               ^^^ number is not string | [n,n,n]

With a type annotation, TypeScript widens each value to the union declared in Theme. With satisfies, TypeScript checks conformance but keeps the narrower inferred type, so string methods are available on primary and tuple methods on accent without any type assertions.

as const for Readonly Tuple Inferencetypescript
// Without as const: TypeScript infers string[], not a tuple
const routesWide = ["home", "about", "contact"];
// type: string[]  — order and exact values unknown

// With as const: TypeScript infers a readonly tuple of string literals
const routes = ["home", "about", "contact"] as const;
// type: readonly ["home", "about", "contact"]

type Route = (typeof routes)[number]; // "home" | "about" | "contact"

function navigate(route: Route): void {
  console.log(`Navigating to /${route}`);
}

navigate("home");   // OK
navigate("about");  // OK
// navigate("shop"); // Error: Argument of type '"shop"' is not assignable

// as const satisfies — validates shape AND preserves literal types
type AppConfig = {
  env: "development" | "production" | "test";
  port: number;
  debug: boolean;
};

const config = {
  env: "production",
  port: 3000,
  debug: false,
} as const satisfies AppConfig;

type Env = typeof config.env; // "production"  (literal, not the whole union)
console.log(config.env);
console.log(config.port);

// config.env = "development"; // Error: cannot assign to readonly property

as const narrows array types to readonly tuples of string literals. Using (typeof routes)[number] extracts a precise union of the literal values. Combining as const with satisfies validates shape conformance while keeping every property readonly and literal-typed.

Quick Quiz

1. What type does `type T = \`${'a' | 'b'}-${'x' | 'y'}\`` produce?

2. What is the key difference between annotating a variable with a type vs using satisfies?

3. What does as const do to an array literal like [1, 2, 3]?

4. Which built-in TypeScript utility type transforms a string literal type so its first character is uppercase?

Was this lesson helpful?

PreviousFinish