Type guards help you narrow types safely at runtime, while type assertions let you override TypeScript's type inference. Type guards are safe and recommended; type assertions bypass safety checks and should be used carefully. Understanding both is crucial for working with dynamic data, APIs, and complex type scenarios. Let's master type guards and learn when assertions are appropriate!
Built-in Type Guards
TypeScript provides several built-in type guards that automatically narrow types based on runtime checks.
typeof Type Guard
// typeof narrows primitive types
function processValue(value: string | number | boolean): string {
if (typeof value === "string") {
// TypeScript knows value is string here
return value.toUpperCase();
}
if (typeof value === "number") {
// TypeScript knows value is number here
return value.toFixed(2);
}
// TypeScript knows value is boolean here
return value ? "Yes" : "No";
}
console.log(processValue("hello")); // "HELLO"
console.log(processValue(123.456)); // "123.46"
console.log(processValue(true)); // "Yes"
// typeof with unknown
function parseInput(input: unknown): string {
if (typeof input === "string") {
return input;
}
if (typeof input === "number") {
return input.toString();
}
if (typeof input === "boolean") {
return input ? "true" : "false";
}
return "unknown";
}
console.log(parseInput("text")); // "text"
console.log(parseInput(42)); // "42"
console.log(parseInput(true)); // "true"
console.log(parseInput({})); // "unknown"
// Multiple typeof checks
function formatValue(value: string | number | null | undefined): string {
if (typeof value === "undefined") {
return "Not provided";
}
if (value === null) {
return "Null value";
}
if (typeof value === "string") {
return `"${value}"`;
}
return value.toFixed(2);
}
console.log(formatValue(undefined)); // "Not provided"
console.log(formatValue(null)); // "Null value"
console.log(formatValue("hello")); // ""hello""
console.log(formatValue(99.99)); // "99.99"instanceof Type Guard
// instanceof narrows class instances
class User {
constructor(public name: string, public email: string) {}
displayInfo(): void {
console.log(`User: ${this.name} (${this.email})`);
}
}
class Admin extends User {
constructor(
name: string,
email: string,
public permissions: string[]
) {
super(name, email);
}
displayInfo(): void {
console.log(`Admin: ${this.name} (${this.email})`);
console.log(`Permissions: ${this.permissions.join(", ")}`);
}
}
function processAccount(account: User | Admin): void {
if (account instanceof Admin) {
// TypeScript knows account is Admin here
console.log(`Admin with ${account.permissions.length} permissions`);
account.displayInfo();
} else {
// TypeScript knows account is User here
account.displayInfo();
}
}
const user = new User("Yusuf", "yusuf@example.com");
const admin = new Admin("Amina", "amina@example.com", ["read", "write", "delete"]);
processAccount(user);
processAccount(admin);
// instanceof with built-in types
function handleValue(value: Date | RegExp | Error): string {
if (value instanceof Date) {
return value.toISOString();
}
if (value instanceof RegExp) {
return value.source;
}
// TypeScript knows value is Error here
return value.message;
}
console.log(handleValue(new Date())); // "2024-12-25T10:30:00.000Z"
console.log(handleValue(/[a-z]+/)); // "[a-z]+"
console.log(handleValue(new Error("Oops"))); // "Oops"
// instanceof with arrays
function processData(data: string[] | Set<string> | Map<string, string>): number {
if (data instanceof Array) {
return data.length;
}
if (data instanceof Set) {
return data.size;
}
// TypeScript knows data is Map here
return data.size;
}
console.log(processData(["a", "b", "c"])); // 3
console.log(processData(new Set(["x", "y", "z"]))); // 3
console.log(processData(new Map([["k1", "v1"], ["k2", "v2"]]))); // 2in Operator Type Guard
// 'in' checks if property exists
type Circle = {
kind: "circle";
radius: number;
};
type Square = {
kind: "square";
size: number;
};
type Rectangle = {
kind: "rectangle";
width: number;
height: number;
};
type Shape = Circle | Square | Rectangle;
function getArea(shape: Shape): number {
if ("radius" in shape) {
// TypeScript knows shape is Circle
return Math.PI * shape.radius ** 2;
}
if ("size" in shape) {
// TypeScript knows shape is Square
return shape.size ** 2;
}
// TypeScript knows shape is Rectangle
return shape.width * shape.height;
}
const circle: Circle = { kind: "circle", radius: 5 };
const square: Square = { kind: "square", size: 10 };
const rectangle: Rectangle = { kind: "rectangle", width: 20, height: 15 };
console.log(`Circle area: ${getArea(circle)}`); // 78.54
console.log(`Square area: ${getArea(square)}`); // 100
console.log(`Rectangle area: ${getArea(rectangle)}`); // 300
// 'in' with optional properties
type User = {
name: string;
email: string;
phone?: string;
};
function displayContact(user: User): void {
console.log(`Name: ${user.name}`);
console.log(`Email: ${user.email}`);
if ("phone" in user && user.phone !== undefined) {
console.log(`Phone: ${user.phone}`);
}
}
displayContact({ name: "Yusuf", email: "yusuf@example.com" });
displayContact({ name: "Amina", email: "amina@example.com", phone: "+234-803-123-4567" });
// Multiple property checks
type ApiSuccess = {
status: "success";
data: any;
};
type ApiError = {
status: "error";
error: string;
};
type ApiResponse = ApiSuccess | ApiError;
function handleResponse(response: ApiResponse): void {
if ("data" in response) {
// TypeScript knows response is ApiSuccess
console.log("Success:", response.data);
} else {
// TypeScript knows response is ApiError
console.error("Error:", response.error);
}
}Equality Narrowing
// Equality checks narrow types
function processStatus(status: "pending" | "approved" | "rejected"): string {
if (status === "pending") {
return "Waiting for approval";
}
if (status === "approved") {
return "Request approved";
}
// TypeScript knows status is "rejected"
return "Request rejected";
}
// Comparing with null/undefined
function greet(name: string | null | undefined): string {
if (name === null) {
return "Hello, guest (null)";
}
if (name === undefined) {
return "Hello, guest (undefined)";
}
// TypeScript knows name is string
return `Hello, ${name}!`;
}
console.log(greet("Yusuf")); // "Hello, Yusuf!"
console.log(greet(null)); // "Hello, guest (null)"
console.log(greet(undefined)); // "Hello, guest (undefined)"
// Checking against specific values
type LoadingState = { state: "loading" };
type SuccessState = { state: "success"; data: string };
type ErrorState = { state: "error"; message: string };
type State = LoadingState | SuccessState | ErrorState;
function renderState(state: State): string {
if (state.state === "loading") {
return "Loading...";
}
if (state.state === "success") {
// TypeScript knows state is SuccessState
return `Success: ${state.data}`;
}
// TypeScript knows state is ErrorState
return `Error: ${state.message}`;
}
console.log(renderState({ state: "loading" }));
console.log(renderState({ state: "success", data: "User data" }));
console.log(renderState({ state: "error", message: "Failed to load" }));Custom Type Guards
Create your own type guard functions using the is keyword. These are reusable, testable, and provide clear type narrowing.
Basic Custom Type Guards
// Custom type guard with 'is' keyword
interface User {
name: string;
email: string;
}
interface Admin {
name: string;
email: string;
role: string;
permissions: string[];
}
// Type predicate function
function isAdmin(account: User | Admin): account is Admin {
return "role" in account && "permissions" in account;
}
function displayAccount(account: User | Admin): void {
console.log(`Name: ${account.name}`);
console.log(`Email: ${account.email}`);
if (isAdmin(account)) {
// TypeScript knows account is Admin here
console.log(`Role: ${account.role}`);
console.log(`Permissions: ${account.permissions.join(", ")}`);
} else {
// TypeScript knows account is User here
console.log("Regular user");
}
}
const user: User = {
name: "Yusuf",
email: "yusuf@example.com"
};
const admin: Admin = {
name: "Amina",
email: "amina@example.com",
role: "admin",
permissions: ["read", "write", "delete"]
};
displayAccount(user);
displayAccount(admin);
// Type guard for arrays
function isStringArray(value: unknown): value is string[] {
return (
Array.isArray(value) &&
value.every(item => typeof item === "string")
);
}
function processArray(value: unknown): void {
if (isStringArray(value)) {
// TypeScript knows value is string[]
console.log(`Strings: ${value.join(", ")}`);
value.forEach(str => console.log(str.toUpperCase()));
} else {
console.log("Not a string array");
}
}
processArray(["a", "b", "c"]); // Works
processArray([1, 2, 3]); // "Not a string array"
processArray("not an array"); // "Not a string array"Complex Type Guards
// Comprehensive type guard for API response
interface ApiSuccessResponse {
status: "success";
data: {
id: number;
name: string;
email: string;
};
timestamp: string;
}
function isApiSuccessResponse(response: unknown): response is ApiSuccessResponse {
if (typeof response !== "object" || response === null) {
return false;
}
const obj = response as any;
return (
obj.status === "success" &&
typeof obj.data === "object" &&
obj.data !== null &&
typeof obj.data.id === "number" &&
typeof obj.data.name === "string" &&
typeof obj.data.email === "string" &&
typeof obj.timestamp === "string"
);
}
function handleApiResponse(response: unknown): void {
if (isApiSuccessResponse(response)) {
// Safe to use all properties
console.log(`User: ${response.data.name}`);
console.log(`Email: ${response.data.email}`);
console.log(`Time: ${response.timestamp}`);
} else {
console.error("Invalid API response");
}
}
// Valid response
handleApiResponse({
status: "success",
data: { id: 1, name: "Yusuf", email: "yusuf@example.com" },
timestamp: "2024-12-25T10:30:00Z"
});
// Invalid responses
handleApiResponse({ status: "error" });
handleApiResponse(null);
handleApiResponse("not an object");
// Type guard with generics
function isArrayOf<T>(
value: unknown,
itemGuard: (item: unknown) => item is T
): value is T[] {
return Array.isArray(value) && value.every(itemGuard);
}
function isNumber(value: unknown): value is number {
return typeof value === "number";
}
function isString(value: unknown): value is string {
return typeof value === "string";
}
const data1 = [1, 2, 3, 4, 5];
const data2 = ["a", "b", "c"];
const data3 = [1, "two", 3];
console.log(isArrayOf(data1, isNumber)); // true
console.log(isArrayOf(data2, isString)); // true
console.log(isArrayOf(data3, isNumber)); // falseReusable Type Guard Helpers
// Utility type guards
function isDefined<T>(value: T | undefined | null): value is T {
return value !== undefined && value !== null;
}
function isNotNull<T>(value: T | null): value is T {
return value !== null;
}
function hasProperty<K extends string>(
obj: unknown,
key: K
): obj is Record<K, unknown> {
return typeof obj === "object" && obj !== null && key in obj;
}
// Using utility guards
const values = [1, undefined, 2, null, 3, undefined, 4];
const defined = values.filter(isDefined);
console.log(defined); // [1, 2, 3, 4]
const nullable = [1, null, 2, null, 3];
const notNull = nullable.filter(isNotNull);
console.log(notNull); // [1, 2, 3]
// Type guard for non-empty array
function isNonEmptyArray<T>(arr: T[]): arr is [T, ...T[]] {
return arr.length > 0;
}
function processItems(items: string[]): void {
if (isNonEmptyArray(items)) {
// TypeScript knows first item exists
const [first, ...rest] = items;
console.log(`First: ${first}`);
console.log(`Rest: ${rest.join(", ")}`);
} else {
console.log("Empty array");
}
}
processItems(["a", "b", "c"]); // "First: a" "Rest: b, c"
processItems([]); // "Empty array"
// Type guard for specific object shape
interface Product {
id: number;
name: string;
price: number;
}
function isProduct(value: unknown): value is Product {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value &&
"price" in value &&
typeof (value as any).id === "number" &&
typeof (value as any).name === "string" &&
typeof (value as any).price === "number"
);
}
function processProduct(data: unknown): void {
if (isProduct(data)) {
console.log(`Product: ${data.name}, Price: ₦${data.price}`);
} else {
console.error("Invalid product data");
}
}
processProduct({ id: 1, name: "Laptop", price: 450000 }); // Works
processProduct({ name: "Invalid" }); // Error messageAssertion Functions
Assertion functions throw errors if a condition isn't met, and they narrow types for the rest of the scope.
// Assertion function with 'asserts' keyword
function assert(condition: boolean, message: string): asserts condition {
if (!condition) {
throw new Error(message);
}
}
function processValue(value: string | null): void {
assert(value !== null, "Value cannot be null");
// TypeScript knows value is string here (not null)
console.log(value.toUpperCase());
}
processValue("hello"); // "HELLO"
// processValue(null); // Throws: "Value cannot be null"
// Type predicate assertion
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error(`Expected string, got ${typeof value}`);
}
}
function processInput(input: unknown): void {
assertIsString(input);
// TypeScript knows input is string here
console.log(input.toUpperCase());
}
processInput("hello"); // "HELLO"
// processInput(123); // Throws: "Expected string, got number"
// Assertion for object shape
interface User {
name: string;
email: string;
}
function assertIsUser(value: unknown): asserts value is User {
if (
typeof value !== "object" ||
value === null ||
!("name" in value) ||
!("email" in value) ||
typeof (value as any).name !== "string" ||
typeof (value as any).email !== "string"
) {
throw new Error("Invalid user object");
}
}
function registerUser(data: unknown): void {
assertIsUser(data);
// TypeScript knows data is User here
console.log(`Registering ${data.name} with email ${data.email}`);
}
registerUser({ name: "Yusuf", email: "yusuf@example.com" }); // Works
// registerUser({ name: "Invalid" }); // Throws error
// Non-null assertion function
function assertDefined<T>(value: T | null | undefined): asserts value is T {
if (value === null || value === undefined) {
throw new Error("Value is null or undefined");
}
}
function getElement(id: string): HTMLElement | null {
return document.getElementById(id);
}
function updateElement(id: string): void {
const element = getElement(id);
assertDefined(element);
// TypeScript knows element is HTMLElement (not null)
element.textContent = "Updated";
}Type Assertions
Type assertions tell TypeScript to treat a value as a specific type. Use with caution—they bypass type checking and can lead to runtime errors.
as Keyword
// Type assertion with 'as' keyword
let value: unknown = "hello";
// Assert that value is a string
let str = value as string;
console.log(str.toUpperCase()); // "HELLO"
// Without assertion, this would error
// console.log(value.toUpperCase()); // ❌ Error: unknown has no methods
// Asserting to more specific type
interface User {
name: string;
email: string;
}
let data: any = { name: "Yusuf", email: "yusuf@example.com" };
let user = data as User;
console.log(`User: ${user.name}`);
// Dangerous: Assertion without validation
let dangerous: unknown = { title: "Not a user" };
let fakeUser = dangerous as User;
// console.log(fakeUser.name.toUpperCase()); // Runtime error!
// Double assertion (escape hatch - very dangerous!)
let num = 42;
// let str = num as string; // ❌ Error: number not assignable to string
let str2 = num as unknown as string; // ✅ Compiles (but wrong!)
// console.log(str2.toUpperCase()); // Runtime error!
// Const assertion
let obj = {
name: "Yusuf",
age: 25
} as const;
// obj.name = "Amina"; // ❌ Error: readonly
// obj.age = 30; // ❌ Error: readonly
// Type of obj is: { readonly name: "Yusuf"; readonly age: 25; }
// Array const assertion
let colors = ["red", "green", "blue"] as const;
// Type: readonly ["red", "green", "blue"]
// colors.push("yellow"); // ❌ Error: readonly
// colors[0] = "purple"; // ❌ Error: readonlyNon-null Assertion Operator
// Non-null assertion with !
function getElement(id: string): HTMLElement | null {
return document.getElementById(id);
}
// Without assertion
const element1 = getElement("myButton");
if (element1) {
element1.textContent = "Click me";
}
// With non-null assertion (risky!)
const element2 = getElement("myButton")!; // Assert it's not null
element2.textContent = "Click me"; // Might crash if element is null!
// Safer: Use optional chaining
const element3 = getElement("myButton");
element3?.addEventListener("click", () => {
console.log("Clicked");
});
// Non-null assertion in object access
type Config = {
host: string;
port?: number;
};
const config: Config = { host: "localhost" };
// With assertion
const port1 = config.port!; // Assert port exists (dangerous!)
console.log(port1 + 1000); // Might be NaN if port is undefined!
// Without assertion (safer)
const port2 = config.port ?? 8080;
console.log(port2 + 1000); // Always works
// Array access with non-null assertion
const users = ["Yusuf", "Amina", "Chidi"];
// Dangerous
const firstUser = users[0]!; // Assert first element exists
// Safer
const secondUser = users[1];
if (secondUser) {
console.log(secondUser.toUpperCase());
}Warning: Type assertions and non-null assertions bypass TypeScript's safety checks. They don't perform any runtime validation. Only use them when you're absolutely certain about the type, and prefer type guards when possible.
Practical Examples
Example 1: Form Validation
// Form data validation with type guards
interface FormData {
name: string;
email: string;
age: number;
terms: boolean;
}
function isValidEmail(value: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(value);
}
function isFormData(data: unknown): data is FormData {
if (typeof data !== "object" || data === null) {
return false;
}
const obj = data as any;
return (
typeof obj.name === "string" &&
obj.name.length >= 2 &&
typeof obj.email === "string" &&
isValidEmail(obj.email) &&
typeof obj.age === "number" &&
obj.age >= 18 &&
obj.age <= 120 &&
typeof obj.terms === "boolean" &&
obj.terms === true
);
}
function submitForm(data: unknown): void {
if (isFormData(data)) {
console.log("Form submitted successfully!");
console.log(`Name: ${data.name}`);
console.log(`Email: ${data.email}`);
console.log(`Age: ${data.age}`);
} else {
console.error("Invalid form data");
}
}
// Valid submission
submitForm({
name: "Yusuf Ibrahim",
email: "yusuf@example.com",
age: 25,
terms: true
});
// Invalid submissions
submitForm({ name: "A", email: "invalid", age: 15, terms: false });
submitForm({ name: "Yusuf" }); // Missing fieldsExample 2: API Response Handler
// Type-safe API response handling
type ApiSuccess<T> = {
status: 200 | 201;
data: T;
};
type ApiError = {
status: 400 | 401 | 404 | 500;
error: {
code: string;
message: string;
};
};
type ApiResponse<T> = ApiSuccess<T> | ApiError;
function isApiSuccess<T>(response: ApiResponse<T>): response is ApiSuccess<T> {
return response.status >= 200 && response.status < 300;
}
function isApiError<T>(response: ApiResponse<T>): response is ApiError {
return response.status >= 400;
}
interface User {
id: number;
name: string;
email: string;
}
async function fetchUser(id: number): Promise<User> {
// Simulate API call
const response: ApiResponse<User> = {
status: 200,
data: { id, name: "Yusuf Ibrahim", email: "yusuf@example.com" }
};
if (isApiSuccess(response)) {
// TypeScript knows response has 'data'
return response.data;
}
if (isApiError(response)) {
// TypeScript knows response has 'error'
throw new Error(`API Error: ${response.error.message}`);
}
throw new Error("Unexpected response");
}
// Usage
fetchUser(1)
.then(user => console.log(`User: ${user.name}`))
.catch(error => console.error(error.message));Example 3: Safe JSON Parsing
// Type-safe JSON parsing
interface Product {
id: number;
name: string;
price: number;
inStock: boolean;
}
function isProduct(value: unknown): value is Product {
if (typeof value !== "object" || value === null) {
return false;
}
const obj = value as any;
return (
typeof obj.id === "number" &&
typeof obj.name === "string" &&
typeof obj.price === "number" &&
obj.price > 0 &&
typeof obj.inStock === "boolean"
);
}
function parseProductJSON(json: string): Product {
let parsed: unknown;
try {
parsed = JSON.parse(json);
} catch (error) {
throw new Error("Invalid JSON");
}
if (isProduct(parsed)) {
return parsed;
}
throw new Error("Invalid product data");
}
// Valid JSON
const validJSON = '{"id":1,"name":"Laptop","price":450000,"inStock":true}';
const product = parseProductJSON(validJSON);
console.log(`Product: ${product.name}, Price: ₦${product.price}`);
// Invalid JSON
try {
const invalidJSON = '{"id":"not a number","name":"Laptop"}';
parseProductJSON(invalidJSON);
} catch (error) {
console.error((error as Error).message); // "Invalid product data"
}
// Array of products
function isProductArray(value: unknown): value is Product[] {
return Array.isArray(value) && value.every(isProduct);
}
function parseProductArrayJSON(json: string): Product[] {
const parsed = JSON.parse(json);
if (isProductArray(parsed)) {
return parsed;
}
throw new Error("Invalid product array");
}
const arrayJSON = '[{"id":1,"name":"Laptop","price":450000,"inStock":true}]';
const products = parseProductArrayJSON(arrayJSON);
console.log(`Products: ${products.length}`);Test Your Knowledge
Type Safety Check
Which type guard pattern is safest?
type User = { name: string; email: string };
type Admin = { name: string; email: string; role: string };
function processUser(user: User | Admin) {
// Option A
console.log((user as Admin).role);
// Option B
if ("role" in user) {
console.log(user.role);
}
// Option C
function isAdmin(user: User | Admin): user is Admin {
return "role" in user;
}
if (isAdmin(user)) {
console.log(user.role);
}
// Option D
console.log(user.role);
}Common TypeScript Error
// Unsafe type assertion
interface User {
name: string;
email: string;
}
function processData(data: unknown) {
// Unsafe assertion - no runtime check!
const user = data as User;
// This might crash if data doesn't match User shape
console.log(user.name.toUpperCase());
console.log(user.email.toLowerCase());
}
// Calling with wrong data
processData({ title: "Not a user" }); // Runtime crash!
processData(null); // Runtime crash!❌ TypeError: Cannot read property 'toUpperCase' of undefined (runtime error)
What's Wrong?
Type assertions bypass TypeScript's type checking. They tell TypeScript 'trust me, I know what I'm doing' but provide no runtime safety. If the data doesn't match, you get runtime errors.
// Safe approach with type guard
interface User {
name: string;
email: string;
}
function isUser(data: unknown): data is User {
return (
typeof data === "object" &&
data !== null &&
"name" in data &&
"email" in data &&
typeof (data as any).name === "string" &&
typeof (data as any).email === "string"
);
}
function processData(data: unknown) {
if (isUser(data)) {
// Safe to use - type guard validated the data
console.log(data.name.toUpperCase());
console.log(data.email.toLowerCase());
} else {
console.error("Invalid data: not a User");
}
}
// Now safe with any input
processData({ name: "Yusuf", email: "yusuf@example.com" }); // ✅ Works
processData({ title: "Not a user" }); // ✅ Handles gracefully
processData(null); // ✅ Handles gracefullySolution: Use type guards to validate data before using it
Best Practices
- Prefer type guards over assertions - safer and more maintainable
- Create reusable type guard functions for common patterns
- Validate unknown data from APIs, user input, or external sources
- Use assertion functions for precondition checks
- Avoid non-null assertions (!) unless absolutely necessary
- Document why you use type assertions when you must
- Test type guards with valid and invalid data
- Use const assertions for literal types
- Combine guards for complex validation
- Use optional chaining instead of non-null assertions
// ✅ Good: Type guard with validation
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"name" in value &&
"email" in value &&
typeof (value as any).name === "string" &&
typeof (value as any).email === "string"
);
}
function processUser(data: unknown): void {
if (isUser(data)) {
console.log(data.name); // Safe
}
}
// ❌ Avoid: Assertion without validation
function badProcessUser(data: unknown): void {
const user = data as User; // Unsafe!
console.log(user.name); // Might crash
}
// ✅ Good: Assertion function for preconditions
function assertPositive(value: number): asserts value is number {
if (value <= 0) {
throw new Error("Value must be positive");
}
}
function calculateDiscount(price: number, percent: number): number {
assertPositive(price);
assertPositive(percent);
return price * (percent / 100);
}
// ❌ Avoid: Non-null assertion
const element = document.getElementById("btn")!; // Risky
element.click(); // Might crash
// ✅ Better: Optional chaining
const safeElement = document.getElementById("btn");
safeElement?.click(); // Safe
// ✅ Good: Const assertion for literals
const config = {
host: "localhost",
port: 8080,
secure: false
} as const;
// ❌ Avoid: Double assertion (very dangerous)
const num = 42;
const str = num as unknown as string; // Don't do this!Key Takeaways
- Type guards safely narrow types with runtime checks
- Built-in guards:
typeof,instanceof,in, equality checks - Custom type guards use
iskeyword for reusable validation - Assertion functions use
assertsto narrow types after validation - Type assertions (
as) bypass safety checks—use with caution - Non-null assertion (
!) assumes value isn't null—dangerous - Always validate unknown data from external sources
- Prefer type guards over assertions for maintainable code
- Use const assertions (
as const) for literal types - Optional chaining (
?.) is safer than non-null assertions
What's Next?
Excellent! You've mastered type guards and type assertions, completing your understanding of safe type narrowing. Next, we'll explore Literal Types and Type Narrowing in even more depth. You'll learn about string literals, numeric literals, boolean literals, template literal types, and advanced narrowing patterns. These features give you incredible precision in your type definitions!
Get ready to work with ultra-precise types!