Model data and type relationships

Does TypeScript Have Dependent Types? Model Literal-Driven Constraints

Use discriminated unions and conditional types for literal-driven constraints, and learn what changes when an input widens to boolean.

Editorial illustration for Does TypeScript Have Dependent Types? Model Literal-Driven Constraints

TypeScript does not directly support dependent types in the general sense: a type computed from an arbitrary value. It can, however, express useful correlations when the relevant distinctions are available as literal types. If one property determines another, use a discriminated union. If a function’s result varies with an input type, consider a generic conditional type. The important question is whether the distinction is still visible to the type checker at the call site.

A value-dependent property, with only two cases

A generic normally relates types: given some type T, another type can refer to T. A dependent type goes further by letting a type depend on a value. TypeScript’s literal types make a small part of that idea practical. The values "count" and "label" can also identify distinct types, so a union can list the permitted combinations.

type Entry =
  | { kind: "count"; value: number }
  | { kind: "label"; value: string };

const total: Entry = { kind: "count", value: 12 };
const heading: Entry = { kind: "label", value: "Open orders" };

// Intentionally invalid: a "count" entry requires a number.
const mismatch: Entry = { kind: "count", value: "twelve" };

function readEntry(entry: Entry): number | string {
  if (entry.kind === "count") {
    const count: number = entry.value;
    return count;
  }

  const label: string = entry.value;
  return label;
}

The two valid assignments match a listed variant. The mismatch assignment does not. Inside the kind check, TypeScript can narrow entry to the "count" variant, where value is a number. This is a finite set of permitted shapes, not a rule that calculates a property’s type from any value someone might supply. For another example of the pattern, see the discriminated-union discussion.

A return type selected by a literal argument

Suppose a caller can request either a basic report or one with details. The following is a type-checking example, not an executable implementation. declare describes the function’s signature but supplies no function body.

type BasicReport = {
  mode: "basic";
  title: string;
};

type DetailedReport = {
  mode: "detailed";
  title: string;
  sections: string[];
};

type ReportFor<T extends boolean> =
  T extends true ? DetailedReport : BasicReport;

declare function getReport<T extends boolean>(
  withDetails: T
): ReportFor<T>;

const detailed = getReport(true);   // DetailedReport
const basic = getReport(false);     // BasicReport

declare const includeDetails: boolean;
const uncertain = getReport(includeDetails);
// DetailedReport | BasicReport

For true, the inferred type argument is the literal type true, so ReportFor<true> selects DetailedReport. The same reasoning gives BasicReport for false. A general boolean does not tell the checker which case the call will take. Here it produces both possible result types because the conditional type distributes over the true | false union. The TypeScript conditional-types documentation describes both branch selection by assignability and distribution over union members.

The conditional type is not evaluating withDetails at runtime. It tests whether the type on the left of extends is assignable to the type on the right. In T extends true, that means testing a type against true, not running an if statement. Nor does the declared signature establish that a real getReport implementation returns the matching object. A body must actually produce the promised result, and callers need a real implementation before these calls can run. A type assertion used to silence an implementation error would not verify that behavior.

Where the approximation stops

Imagine appending two vectors and asking the result type to contain their exact combined length, calculated from arbitrary runtime lengths. That is the sort of value-level dependency used to illustrate dependent types. TypeScript can represent particular known shapes and some calculations over types, but a discriminated union or boolean conditional does not establish a general type-level proof of that runtime length calculation. The distinction matters: a known tuple shape and an array whose length is discovered only while running the program do not offer the same information to a type-level mapping.

Use literal correlations when the cases are finite and meaningful to callers. Use a conditional type when a known input type should select an output type. If the input has widened, expect the answer to widen too. And if the relationship depends on what actually arrives at runtime, check that value in running code rather than treating the signature as validation.

Find a note

Search by topic, title, or keyword.