# TypeScript Control Flow Analysis: A Practical Guide for Everyday Code

TypeScript’s type system is powerful, but what truly makes it feel *intelligent* is **Control Flow Analysis (CFA)**. CFA allows TypeScript to follow your program’s execution paths—through `if` statements, expressions, and function calls—and automatically **narrow** types as conditions are evaluated.

This capability significantly reduces the need for manual type annotations and helps catch subtle bugs early. This guide walks through the most important CFA patterns commonly used in everyday TypeScript code: conditional narrowing, expressions, discriminated unions, type guards, assertion functions, and assignment-based narrowing.

---

## What Control Flow Analysis Does

Control Flow Analysis typically begins with a **union type** and progressively narrows it based on program logic.

For example, a value might initially be typed as `string | number`. After a `typeof` check, TypeScript can safely treat it as either `string` or `number` within the appropriate branch.

CFA works through standard JavaScript control structures, including:

* `if` and `switch` statements
    
* Logical expressions such as `&&` and `||`
    
* Runtime checks like `typeof`, `instanceof`, and `'property' in object`
    
* Custom helper functions designed to narrow types
    

The result is idiomatic JavaScript code that benefits from increasingly precise type information as execution flows forward.

---

## Narrowing Types with If Statements

### Using `typeof` for Primitive Types

Consider a value that may be either a string or a number:

```ts
const input = getUserInput(); // string | number

if (typeof input === "string") {
  input.toUpperCase(); // input: string
} else {
  input.toFixed(2); // input: number
}
```

The `typeof` check informs TypeScript which type applies in each branch, enabling correct IntelliSense and compile-time safety.

---

### Using `instanceof` for Class-Based Objects

For values created from classes:

```ts
const input = getUserInput(); // number | number[]

if (input instanceof Array) {
  console.log(input.length); // input: number[]
} else {
  console.log(input.toFixed(2)); // input: number
}
```

TypeScript narrows the type based on the `instanceof` result.

---

### Using `'property' in object` for Structural Checks

You can distinguish between object shapes by checking for the existence of a property:

```typescript
const input = getUserInput(); // string | { error: string }

if ("error" in input) {
  console.error(input.error); // input: { error: string }
} else {
  console.log(input); // input: string
}
```

---

### Using Custom Type Guard Functions

You can define helper functions that explicitly instruct TypeScript how to narrow types:

```typescript
function isArrayOfNumbers(value: unknown): value is number[] {
  return Array.isArray(value);
}

const input = getUserInput(); // unknown

if (isArrayOfNumbers(input)) {
  // input: number[]
} else {
  // input: unknown
}
```

The return type `value is number[]` is what makes this a custom type guard.

---

## Narrowing Within Expressions

Control Flow Analysis also works within expressions, not just explicit `if` blocks.

```typescript
const input = getUserInput(); // string | number

const inputLength =
  (typeof input === "string" && input.length) || 0;
```

Within the left side of the `&&` operator, `input` is narrowed to `string`. After the expression resolves, the type returns to its original union. This allows concise, expressive, and type-safe code.

---

## Discriminated Unions

A **discriminated union** is a union where all members share a common field—typically called a *discriminant*—such as `status` or `type`.

```typescript
type Responses =
  | { status: 200; data: any }
  | { status: 301; to: string }
  | { status: 400; error: Error };
```

Because all variants include `status`, TypeScript can reliably narrow based on its value:

```typescript
const response = getResponse(); // Responses

switch (response.status) {
  case 200:
    return response.data;
  case 301:
    return redirect(response.to);
  case 400:
    throw response.error;
}
```

Each `case` receives a fully narrowed type without the need for type assertions.

---

## Type Guards: Custom Narrowing Logic

Type guards are functions whose return type explicitly describes how a value should be narrowed when the function returns `true`.

```typescript
type Response = SuccessResponse | APIErrorResponse;

function isErrorResponse(obj: Response): obj is APIErrorResponse {
  return obj instanceof APIErrorResponse;
}

const response = getResponse(); // Response

if (isErrorResponse(response)) {
  // response: APIErrorResponse
} else {
  // response: SuccessResponse
}
```

The defining pattern is:

```ts
function fn(value: T): value is NarrowedT
```

Whenever `fn(value)` evaluates to `true`, TypeScript narrows `value` to `NarrowedT` within that scope.

---

## Assertion Functions: Narrowing or Throwing

Assertion functions take type narrowing a step further. Instead of returning a boolean, they either **throw an error** or allow execution to continue. Their return type uses the `asserts` keyword.

```typescript
function assertSuccessResponse(obj: any): asserts obj is SuccessResponse {
  if (!(obj instanceof SuccessResponse)) {
    throw new Error("Not a success!");
  }
}

const res = getResponse(); // SuccessResponse | ErrorResponse
assertSuccessResponse(res);
// res is now SuccessResponse
```

If the function does not throw, TypeScript assumes the assertion holds and narrows the type accordingly.

---

## Assignment and `as const`: Preserving Literal Types

By default, object literals are widened. For example, `"Zagreus"` becomes `string`. Using `as const` preserves literal values.

```ts
const data1 = {
  name: "Zagreus",
};
// { name: string }

const data2 = {
  name: "Zagreus",
} as const;
// { readonly name: "Zagreus" }
```

This is particularly useful when:

* Building discriminated unions
    
* Working with string literal unions
    
* Relying on exact values for control flow
    

CFA also tracks related variables:

```ts
const res = getResponse();
const isSuccessResponse = res instanceof SuccessResponse;

if (isSuccessResponse) {
  // res is treated as SuccessResponse here
}
```

And it updates types when variables are reassigned:

```ts
let data: string | number;

data = "Hello"; // data: string
data = 3;       // data: number
```

---

## Why Control Flow Analysis Matters

Control Flow Analysis is what makes TypeScript feel natural and productive for JavaScript developers:

* You write standard JavaScript using familiar control structures
    
* TypeScript refines types automatically as logic unfolds
    
* You avoid unnecessary `any` usage and reduce runtime errors
    
* Editor tooling becomes significantly smarter
    

To leverage CFA effectively:

* Use `typeof`, `instanceof`, and `'property' in object`
    
* Design discriminated unions with a shared field
    
* Write custom type guards and assertion functions
    
* Apply `as const` when literal precision matters
    

Mastering these patterns allows TypeScript to work *with* your code rather than against it—providing safety, clarity, and confidence without sacrificing readability.
