TypeScript
TypeScript narrowing and satisfies solve different problems
Use runtime guards to narrow uncertain values and satisfies to check configurations without confusing the two.
TypeScript narrowing and the satisfies operator help at different points in a program. Narrowing lets the compiler follow checks in your code and refine the type it knows about. The satisfies operator checks whether an expression fits a type while preserving the expression’s inferred type. Neither removes the need to check data arriving from outside your application.
Narrow a value before using it
Consider a small display helper that accepts either a label or a number. Calling a string method on the union directly would be unsafe because a number does not provide that method. A typeof check gives the compiler and the reader the information needed for each branch.
function display(value: string | number): string {
if (typeof value === "number") {
return value.toFixed(2)
}
return value.trim()
}
display(12) // "12.00"
display(" ready ") // "ready"Inside the first branch, value is a number. After that branch returns, the remaining path handles a string. This is useful when different inputs need different operations. The runtime condition does real work, and TypeScript uses that condition when analysing the program.
Keep the checks close to the operations they justify. A long function that changes a variable several times can be harder to reason about than two short branches. The purpose is to make valid paths clear, not to collect clever ways of avoiding a compiler error.
Check a configuration with satisfies
Now consider an object written in your own source code. You want its output setting to be either json or text, while retaining a useful inferred type for the object. This is where satisfies fits.
type Output = "json" | "text"
const options = {
output: "json",
indent: 2
} satisfies { output: Output, indent: number }
const selected: "json" = options.outputThe compiler checks the configuration against the shape on the right of satisfies. It also knows that this particular output property contains "json". If you change that property to "csv", the configuration fails the check. The final assignment demonstrates why preserving the narrower inferred value can be useful.
An annotation has a related purpose, but it can deliberately give a variable a broader type. Choose the form that describes your intention. A reusable variable that will change may need a wider type, while a fixed configuration can benefit from retaining what the compiler inferred.
Do not mistake a compiler check for input validation
The satisfies operator does not create a runtime validator. It cannot inspect the contents of a response that your application has not received yet. A type assertion also cannot prove that a remote service returned the shape you expected. Treat outside input as uncertain until your program checks it.
function readOutput(value: unknown): Output | null {
if (value === "json" || value === "text") {
return value
}
return null
}
readOutput("json") // "json"
readOutput("csv") // null
readOutput(2) // nullThis guard deliberately accepts just two strings. It returns null for everything else, giving the caller a clear rejection path. The unknown input type makes the uncertainty visible instead of allowing arbitrary operations through any. For a nested request body, write checks for the relevant fields or use a validation library that matches your project’s requirements.
Avoid truthiness checks when zero is valid
A condition such as if (value) checks truthiness. It does not simply ask whether a value exists. Zero and an empty string are falsy, so a truthiness branch can accidentally discard meaningful input. If a form allows a quantity of zero, compare against the specific missing value rather than relying on a broad truthiness test.
The same care applies to an optional field. Decide whether an empty string means a valid empty label, an unanswered question or an error. That is an application decision. A type checker helps enforce the decision once it is expressed, but it cannot choose the right business rule for you.
Test both the type errors and the runtime paths
The examples here were checked with TypeScript 5.9.3. For the configuration example, compile the valid object, then deliberately replace "json" with "csv" and confirm that compilation fails. For the display helper, exercise both branches. For the input guard, include accepted strings, an unexpected string and a non-string value. These checks cover separate questions and should stay separate.
The TypeScript narrowing handbook explains how control flow refines types. The TypeScript 4.9 release notes introduce satisfies. Use a current compiler in your project and check its diagnostics before copying an example into a build. A clean compilation is useful evidence about types, while runtime tests show what happens to actual values.