Type predicates and custom type guards
Type Predicates and Custom Type Guards
TypeScript's type system allows developers to enhance type inference capabilities through type predicates and custom type guards. These two mechanisms are particularly useful in complex type scenarios, helping the compiler understand type changes in the code.
Basic Concepts of Type Predicates
A type predicate is a special return type annotation in TypeScript, formatted as parameterName is Type
. It is typically used in user-defined type guard functions to inform the compiler that if the function returns true
, the parameter belongs to a specific type.
interface Cat {
meow(): void;
}
interface Dog {
bark(): void;
}
function isCat(animal: Cat | Dog): animal is Cat {
return (animal as Cat).meow !== undefined;
}
In this example, the isCat
function not only returns a boolean but also tells TypeScript through the animal is Cat
predicate that when the function returns true
, the parameter animal
must be of type Cat
.
Implementation of Custom Type Guards
Custom type guards are essentially functions that return type predicates. They can encapsulate complex type-checking logic, making the code clearer and type-safe.
type Primitive = string | number | boolean | symbol | null | undefined;
function isPrimitive(value: unknown): value is Primitive {
return (
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean' ||
typeof value === 'symbol' ||
value === null ||
value === undefined
);
}
function processValue(value: unknown) {
if (isPrimitive(value)) {
// Here, `value` is inferred as type `Primitive`
console.log(value.toString());
} else {
// Here, `value` is inferred as type `object`
console.log(Object.keys(value));
}
}
Union Types and Type Guards
Type guards are especially useful when working with union types, as they help TypeScript narrow down the type range.
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; sideLength: number }
| { kind: 'triangle'; base: number; height: number };
function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
// Here, `shape` is inferred as `{ kind: 'circle'; radius: number }`
return Math.PI * shape.radius ** 2;
case 'square':
return shape.sideLength ** 2;
case 'triangle':
return (shape.base * shape.height) / 2;
}
}
Examples of Complex Type Guards
For more complex scenarios, you can create type guards that combine multiple conditions.
interface Admin {
role: 'admin';
permissions: string[];
}
interface User {
role: 'user';
lastLogin: Date;
}
type Person = Admin | User;
function isAdmin(person: Person): person is Admin {
return person.role === 'admin';
}
function hasPermission(person: Person, permission: string): boolean {
if (isAdmin(person)) {
// Here, `person` is inferred as type `Admin`
return person.permissions.includes(permission);
}
return false;
}
Advanced Usage of Type Predicates
Type predicates can be combined with generics to create more flexible type guards.
function isArrayOf<T>(
arr: unknown,
check: (item: unknown) => item is T
): arr is T[] {
return Array.isArray(arr) && arr.every(check);
}
function isString(value: unknown): value is string {
return typeof value === 'string';
}
const data: unknown = ['a', 'b', 'c'];
if (isArrayOf(data, isString)) {
// Here, `data` is inferred as `string[]`
data.forEach(s => console.log(s.toUpperCase()));
}
Runtime Type Checking and Type Guards
Type guards are often used to convert dynamic data (e.g., API responses) into static types.
interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
}
function isApiResponse<T>(
response: unknown,
checkData?: (data: unknown) => data is T
): response is ApiResponse<T> {
if (typeof response !== 'object' || response === null) {
return false;
}
const res = response as Record<string, unknown>;
if (typeof res.success !== 'boolean') {
return false;
}
if (res.success && checkData && 'data' in res) {
return checkData(res.data);
}
return true;
}
// Usage example
const rawResponse = await fetch('/api/data');
const response = await rawResponse.json();
if (isApiResponse<string[]>(response, isArrayOf(isString))) {
if (response.success) {
console.log(response.data); // `string[]`
} else {
console.error(response.error);
}
}
Performance Considerations for Type Guards
While type guards provide strong type safety, their runtime overhead should be considered.
// Simple but efficient type guard
function isNumber(value: unknown): value is number {
return typeof value === 'number';
}
// Complex but potentially inefficient type guard
function isComplexObject(value: unknown): value is ComplexType {
if (typeof value !== 'object' || value === null) return false;
const obj = value as Record<string, unknown>;
return (
typeof obj.id === 'string' &&
typeof obj.timestamp === 'number' &&
Array.isArray(obj.items) &&
// More complex checks...
);
}
Differences Between Type Guards and Type Assertions
Type guards and type assertions (as
) are fundamentally different: type guards perform runtime checks, while type assertions simply tell the compiler, "Trust me."
// Unsafe type assertion
const unsafeCast = someValue as string;
// Safe type guard
if (typeof someValue === 'string') {
// Here, `someValue` is safely inferred as `string`
const safeValue = someValue;
}
Limitations and Considerations for Type Predicates
Although powerful, type predicates have their limitations:
- Type predicates cannot check for non-existent properties.
- Overuse may complicate the code.
- Incorrectly implemented type guards may lead to runtime errors.
// Incorrect type guard implementation
function isBadGuard(value: unknown): value is string {
// This implementation does not guarantee `value` is a string
return true;
}
// Using an incorrect type guard
const test: unknown = 123;
if (isBadGuard(test)) {
console.log(test.toUpperCase()); // Runtime error!
}
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn