Using NonNullable: Excluding null and undefined
Introduction
Working with nullable values is one of the most common sources of bugs in TypeScript applications. Whether you're dealing with optional API responses, configuration values, or legacy JavaScript data, you often need to express that a value cannot be null or undefined. TypeScript ships a small but powerful utility type, NonNullable
In this tutorial you'll learn:
- What NonNullable
does and how it differs from other null-safety tools in TypeScript - Practical patterns for using NonNullable
with generics, mapped types, and function return types - How NonNullable
interacts with runtime checks and validation libraries - Common pitfalls, performance considerations, and debugging strategies
We'll walk through step-by-step examples that start simple and grow into real-world use cases, integrating NonNullable
For a broader view of the available type transforms in TypeScript, consider reviewing our introduction to utility types, which covers Partial, Pick, Record and other helpers in detail: Introduction to Utility Types: Transforming Existing Types.
Background & Context
NonNullable
This is different from the non-null assertion operator (!) and type assertions, both of which can silence the compiler without changing the actual type. See our deep dive on the non-null assertion operator for when that operator might be appropriate: Non-null Assertion Operator (!) Explained. If you're using runtime assertions or converting shapes, check our guidance on type assertions and their risks: Type Assertions (as keyword or <>) and Their Risks.
Key Takeaways
- NonNullable
removes null and undefined from a type — useful for refining union types. - It's a compile-time-only transformation; runtime checks are still required for safety.
- Use NonNullable
with generics and mapped types to express stricter APIs. - Mix NonNullable
with validation libraries like Zod for runtime guarantees. - Be careful with deep nested types and intersections; apply NonNullable selectively.
Prerequisites & Setup
To follow along you'll need:
- TypeScript 4.x or later (the utility type has been stable for many versions)
- A developer environment with type-checking (VS Code recommended)
- Optionally, a runtime validation library (Zod or Yup) for runtime correctness
If you want to practice the examples locally, create a new npm project, install TypeScript, and initialize tsconfig with strict settings (especially "strictNullChecks": true). For runtime checks, see our integration guide for using Zod or Yup with TypeScript types: Using Zod or Yup for Runtime Validation with TypeScript Types (Integration).
Main Tutorial Sections
1) What NonNullable Actually Does (Short Technical Definition)
NonNullable
Example:
type MaybeString = string | null | undefined; type NotNullableString = NonNullable<MaybeString>; // string
This makes NonNullable
2) NonNullable vs. the Non-null Assertion Operator (!)
Both NonNullable
Example comparison:
declare const maybeName: string | undefined; // Using NonNullable in a type alias type Name = NonNullable<typeof maybeName>; // string // Using non-null assertion operator at a usage site const name1 = maybeName!; // compile-time ok, runtime may still be undefined // Best practice: prefer NonNullable in types and runtime checks for assertions
For more about when to use the non-null operator versus safer patterns, see Non-null Assertion Operator (!) Explained.
3) Practical Example: Narrowing API Responses
Imagine an API that returns optional fields. You can model both the raw response and a cleaned version where certain fields are guaranteed.
type ApiUser = {
id: string;
name?: string | null;
email?: string | null;
};
// After validation or defaulting, we want certain fields to be non-nullable
type CleanUser = {
id: string;
name: NonNullable<ApiUser['name']>;
email: NonNullable<ApiUser['email']>;
};This is useful in code paths where you've already validated or defaulted values. To ensure the cleaning step is robust, integrate runtime validation (e.g., Zod): Using Zod or Yup for Runtime Validation with TypeScript Types (Integration).
4) Combining with Mapped Types and Partial
When you use mapped types like Partial
Example:
type Config = {
host: string;
port: number;
timeout?: number | null;
};
// Make everything optional then remove null/undefined for specific keys
type PartialConfig = Partial<Config>; // host?: string | undefined, port?: number | undefined
type RequiredTimeoutConfig = {
[K in keyof PartialConfig]: K extends 'timeout' ? NonNullable<PartialConfig[K]> : PartialConfig[K]
};If you're using Partial
5) Using NonNullable in Generics and Function Signatures
You can use NonNullable
function unwrap<T>(value: T | null | undefined): NonNullable<T> {
if (value == null) throw new Error('value is null or undefined');
return value as NonNullable<T>;
}
const num = unwrap<number | undefined>(5); // num: numberHere unwrap performs a runtime check and narrows the returned type. When writing generic utilities, remember to combine runtime checks and compile-time types. If you're working with generics more broadly, our introductions to generics and generic functions are useful references: Introduction to Generics: Writing Reusable Code and Generic Functions: Typing Functions with Type Variables.
6) Deep and Nested NonNullable — When to Apply It
NonNullable
Shallow example:
type DeepMaybe = { a?: { b: string | null } | null } | null;
type Shallow = NonNullable<DeepMaybe>; // removes top-level null
// Shallow is { a?: { b: string | null } | null }To remove deeper nulls, write a recursive NonNullableDeep
7) Creating a NonNullableDeep Utility
Here's a practical recursive implementation that strips null/undefined deeply for objects and arrays:
type NonNullableDeep<T> = T extends Function
? T
: T extends Array<infer U>
? Array<NonNullableDeep<NonNullable<U>>>
: T extends object
? { [K in keyof T]-?: NonNullableDeep<NonNullable<T[K]>> }
: NonNullable<T>;
// Usage
type Test = NonNullableDeep<{ a?: { b: string | null } | null } | null>;
// Test => { a: { b: string } }Note: this aggressively converts optional properties to required because it removes undefined; adjust behavior if you want to preserve optional fields.
8) Interplay with Union & Intersection Types
NonNullable
type U = string | number | null | undefined; type R = NonNullable<U>; // string | number
When using intersections, NonNullable applies to the whole type. Be careful with complex intersections and branded types. For patterns that rely heavily on unions and intersections, our guide to union & intersection typing is a good read: Typing Libraries That Use Union and Intersection Types Extensively.
9) Runtime Validation — Don't Rely on Types Alone
NonNullable
Example with Zod (pseudo):
import { z } from 'zod';
const schema = z.object({ name: z.string().nullable().optional() });
const result = schema.safeParse(raw);
if (!result.success) throw new Error('invalid');
// After runtime defaults/cleanup
const clean = { name: result.data.name ?? 'default' };
type CleanType = {
name: NonNullable<typeof clean.name>
};For detailed integration examples, refer to: Using Zod or Yup for Runtime Validation with TypeScript Types (Integration).
10) Applying NonNullable When Typing APIs and Configuration
NonNullable
For patterns on typing request/response payloads and stricter configurations, see these guides: Typing API Request and Response Payloads with Strictness and Typing Configuration Objects in TypeScript: Strictness and Validation.
Example workflow:
- Define RawPayload allowing null/undefined.
- Validate and fill defaults at runtime.
- Create ProcessedPayload that uses NonNullable
for keys you require.
This separation makes it explicit where validation happens and what invariants your business logic can rely on.
Advanced Techniques
- Combine NonNullable
with advanced mapped types: selectively apply NonNullable only to keys satisfying a condition (using conditional mapped types) to preserve optional properties where necessary. - Use helper types to convert optional properties into required ones carefully. For example, use [K in keyof T as ...] remapping to adjust property optionality.
- Build lightweight runtime validators that return values typed as NonNullable
after asserting non-null. This pattern reduces repetitive type assertions across your codebase. - When writing libraries, expose both nullable and cleaned types: Raw
for input and Clean for the sanitized output. This is useful if you're writing API client libraries or SDKs.
If you're working on generic-heavy libraries, our guide on typing libraries with complex generic signatures will help you apply these patterns without sacrificing ergonomics: Typing Libraries With Complex Generic Signatures — Practical Patterns.
Best Practices & Common Pitfalls
Dos:
- Do use NonNullable
to express compile-time guarantees when you have runtime checks or defaulting logic that enforces those guarantees. - Do pair NonNullable
with explicit runtime validation for external input. - Do prefer explicit type transforms (NonNullable, mapped types) over scattered non-null assertions (!).
Don'ts:
- Don't assume NonNullable
provides runtime safety — it's compile-time only. - Don't overuse deep recursive NonNullable transforms in very large types; they can slow down the type checker.
- Don't mix unchecked type assertions with NonNullable in a way that hides potential runtime errors.
Troubleshooting tips:
- If you see an unexpected never type, inspect whether T includes null or undefined in conditional branches.
- For complex mapped types, break transformations into smaller intermediate types to make type errors clearer.
- Use editor hover and the TypeScript language service to inspect what NonNullable
resolves to in complex scenarios.
For common patterns when working with unions and literal types that intersect with nullability, our guide on union types provides useful patterns: Using Union Types Effectively with Literal Types.
Real-World Applications
- API clients: define RawResponse with nullable fields, validate + convert to CleanResponse using NonNullable
. - Configuration loaders: load optional config from environment or files, normalize defaults, then expose a typed Config with guaranteed fields using NonNullable
. - Library APIs: accept flexible input types but return strict output types for consumers by removing null/undefined in documented return shapes.
For specifics on typing configs and APIs with strong guarantees, review these guides: Typing Configuration Objects in TypeScript: Strictness and Validation and Typing API Request and Response Payloads with Strictness.
Conclusion & Next Steps
NonNullable
Enhanced FAQ
Q: What exactly does NonNullable
Q: Is NonNullable
Q: How does NonNullable
Q: Does NonNullable
Q: Can NonNullable
Q: How should I handle arrays or tuples with nullable elements? A: Map over array element types using NonNullable. For example, NonNullable<Array<T | null>> becomes Array<NonNullable<T | null>>. For tuples, apply NonNullable to each element via a mapped tuple type.
Q: Are there performance considerations when using NonNullable extensively? A: Moderate. Simple uses are cheap, but deep recursive mapped types or large unions with many nested transforms can slow down TypeScript's type checker. If you hit slowdowns, simplify types, split transformations into steps, or avoid deep recursion where possible.
Q: What's the recommended workflow for using NonNullable
Q: Can I make only specific keys non-nullable in an object type? A: Yes. Use conditional mapped types to target specific keys. For example:
type MakeKeysNonNullable<T, K extends keyof T> = {
[P in keyof T]: P extends K ? NonNullable<T[P]> : T[P]
};This lets you keep other keys optional while enforcing non-null on selected ones.
Q: How does NonNullable
Q: Where can I learn more about related patterns such as Partial
Q: Should library authors export only non-nullable types? A: It depends. If your library performs validation and normalization internally, exposing non-nullable types for consumers may be more ergonomic. If you accept raw data and leave validation to consumers, keep nullable types visible. For guidance on library authoring with generics and global exports, see Typing Libraries That Are Primarily Class-Based in TypeScript and Typing Libraries That Export Global Variables in TypeScript.
Q: Any final tips for avoiding misuse?
A: Avoid sprinkling non-null assertions (!) as a substitute for explicit validation. Use NonNullable
If you liked this tutorial, consider reading more on utility types and advanced generics to further solidify your type system skills: Introduction to Utility Types: Transforming Existing Types and Typing Libraries With Complex Generic Signatures — Practical Patterns.
