TypeScript Tips for Better Code
TypeScript offers powerful features that can significantly improve your code quality and developer experience. In this comprehensive guide, we'll explore advanced patterns and techniques that will help you write more robust, maintainable, and type-safe code.
Advanced Type Patterns
1. Utility Types
TypeScript provides built-in utility types that can save you time and make your code more expressive:
typescript
// Make all properties optional
type Partial = {
[P in keyof T]?: T[P];
};
// Make all properties required
type Required = {
[P in keyof T]-?: T[P];
};
// Pick specific properties
type User = {
id: number;
name: string;
email: string;
password: string;
};
type PublicUser = Pick;
type UserUpdate = Partial>;
2. Conditional Types
Conditional types allow you to create types that depend on other types:
typescript
// Basic conditional type
type NonNullable = T extends null | undefined ? never : T;
// More complex conditional type
type ApiResponse = T extends string
? { message: T }
: T extends number
? { count: T }
: { data: T };
// Usage
type StringResponse = ApiResponse; // { message: string }
type NumberResponse = ApiResponse; // { count: number }
type ObjectResponse = ApiResponse; // { data: User }
3. Template Literal Types
Create types based on string patterns:
typescript
type EventName = on${Capitalize} ;
type ActionName = ${T}Action;
// Usage
type ClickEvent = EventName<'click'>; // 'onClick'
type LoadAction = ActionName<'load'>; // 'loadAction'
// More complex example
type CSSProperty = --${T};
type ThemeColor = CSSProperty<'primary' | 'secondary'>; // '--primary' | '--secondary'
Advanced Generic Patterns
1. Generic Constraints
Use constraints to limit what types can be used with your generics:
typescript
// Basic constraint
function getProperty(obj: T, key: K): T[K] {
return obj[key];
}
// Multiple constraints
interface Lengthwise {
length: number;
}
function logLength(arg: T): T {
console.log(arg.length);
return arg;
}
// Constraint with conditional types
type NonEmptyArray = T[] & { 0: T };
function getFirst(arr: NonEmptyArray): T {
return arr[0];
}
2. Mapped Types
Create new types by transforming existing ones:
typescript
// Make all properties readonly
type Readonly = {
readonly [P in keyof T]: T[P];
};
// Create a type with all properties as functions
type FunctionProperty = {
[K in keyof T]: () => T[K];
};
// Create a type with optional properties
type Optional = {
[K in keyof T]?: T[K];
};
Error Handling Patterns
1. Result Type Pattern
Create a robust error handling system:
typescript
type Result =
| { success: true; data: T }
| { success: false; error: E };
async function fetchUser(id: string): Promise> {
try {
const response = await fetch(/api/users/${id});
if (!response.ok) {
return { success: false, error: 'User not found' };
}
const user = await response.json();
return { success: true, data: user };
} catch (error) {
return { success: false, error: 'Network error' };
}
}
// Usage
const result = await fetchUser('123');
if (result.success) {
console.log(result.data.name); // TypeScript knows this is User
} else {
console.error(result.error); // TypeScript knows this is string
}
2. Branded Types
Create distinct types for better type safety:
typescript
// Branded types for different ID types
type UserId = string & { __brand: 'UserId' };
type ProductId = string & { __brand: 'ProductId' };
function createUserId(id: string): UserId {
return id as UserId;
}
function createProductId(id: string): ProductId {
return id as ProductId;
}
// This prevents mixing up different types of IDs
function getUser(id: UserId): User {
// Implementation
}
const userId = createUserId('123');
const productId = createProductId('456');
getUser(userId); // ✅ OK
getUser(productId); // ❌ TypeScript error
Advanced Function Patterns
1. Function Overloads
Create functions that behave differently based on input types:
typescript
// Overload signatures
function process(value: string): string;
function process(value: number): number;
function process(value: boolean): boolean;
// Implementation signature
function process(value: string | number | boolean): string | number | boolean {
if (typeof value === 'string') {
return value.toUpperCase();
} else if (typeof value === 'number') {
return value * 2;
} else {
return !value;
}
}
// Usage with proper type inference
const result1 = process('hello'); // Type: string
const result2 = process(42); // Type: number
const result3 = process(true); // Type: boolean
2. Curried Functions
Create reusable function factories:
typescript
// Basic currying
function add(a: number) {
return (b: number) => a + b;
}
const add5 = add(5);
console.log(add5(3)); // 8
// Advanced currying with generics
function createApiCall(baseUrl: string) {
return (endpoint: string) => {
return (params: Record): Promise => {
const url = new URL(endpoint, baseUrl);
Object.entries(params).forEach(([key, value]) => {
url.searchParams.set(key, String(value));
});
return fetch(url.toString()).then(res => res.json());
};
};
}
// Usage
const apiCall = createApiCall('https://api.example.com');
const getUser = apiCall('/users');
const user = await getUser({ id: '123' });
Configuration and Best Practices
1. Strict TypeScript Configuration
Enable strict mode for better type checking:
json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true
}
}
2. Type Guards
Create runtime type checking functions:
typescript
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value &&
'email' in value
);
}
// Usage
function processValue(value: unknown) {
if (isString(value)) {
// TypeScript knows value is string here
console.log(value.toUpperCase());
}
if (isUser(value)) {
// TypeScript knows value is User here
console.log(value.name);
}
}
Performance Considerations
1. Type-Only Imports
Use type-only imports to reduce bundle size:
typescript
import type { User, ApiResponse } from './types';
import { fetchUser } from './api';
// This import is removed at compile time
2. Avoid Complex Conditional Types
Complex conditional types can slow down compilation:
typescript
// ❌ Avoid overly complex conditional types
type ComplexType = T extends infer U
? U extends string
? U extends ${string}-${string}
? U
: never
: never
: never;
// ✅ Prefer simpler, more readable types
type KebabCase = T extends ${infer A}${infer B}
? ${A}-${KebabCase}
: T;
Conclusion
TypeScript's advanced features can significantly improve your code quality and developer experience. Start with the basics and gradually incorporate these patterns into your projects. Remember, the goal is to make your code more maintainable, not more complex.
Focus on:
- Using strict type checking
- Creating reusable type patterns
- Implementing proper error handling
- Writing self-documenting code through types
The investment in learning these patterns will pay off in reduced bugs, better IDE support, and more maintainable codebases.
1. Utility Types
TypeScript provides built-in utility types that can save you time and make your code more expressive:
typescript
// Make all properties optional
type Partial = {
[P in keyof T]?: T[P];
};
// Make all properties required
type Required = {
[P in keyof T]-?: T[P];
};
// Pick specific properties
type User = {
id: number;
name: string;
email: string;
password: string;
};
type PublicUser = Pick;
type UserUpdate = Partial>;
2. Conditional Types
Conditional types allow you to create types that depend on other types:
typescript
// Basic conditional type
type NonNullable = T extends null | undefined ? never : T;
// More complex conditional type
type ApiResponse = T extends string
? { message: T }
: T extends number
? { count: T }
: { data: T };
// Usage
type StringResponse = ApiResponse; // { message: string }
type NumberResponse = ApiResponse; // { count: number }
type ObjectResponse = ApiResponse; // { data: User }
3. Template Literal Types
Create types based on string patterns:
typescript
type EventName = on${Capitalize} ;
type ActionName = ${T}Action;
// Usage
type ClickEvent = EventName<'click'>; // 'onClick'
type LoadAction = ActionName<'load'>; // 'loadAction'
// More complex example
type CSSProperty = --${T};
type ThemeColor = CSSProperty<'primary' | 'secondary'>; // '--primary' | '--secondary'
Advanced Generic Patterns
1. Generic Constraints
Use constraints to limit what types can be used with your generics:
typescript
// Basic constraint
function getProperty(obj: T, key: K): T[K] {
return obj[key];
}
// Multiple constraints
interface Lengthwise {
length: number;
}
function logLength(arg: T): T {
console.log(arg.length);
return arg;
}
// Constraint with conditional types
type NonEmptyArray = T[] & { 0: T };
function getFirst(arr: NonEmptyArray): T {
return arr[0];
}
2. Mapped Types
Create new types by transforming existing ones:
typescript
// Make all properties readonly
type Readonly = {
readonly [P in keyof T]: T[P];
};
// Create a type with all properties as functions
type FunctionProperty = {
[K in keyof T]: () => T[K];
};
// Create a type with optional properties
type Optional = {
[K in keyof T]?: T[K];
};
Error Handling Patterns
1. Result Type Pattern
Create a robust error handling system:
typescript
type Result =
| { success: true; data: T }
| { success: false; error: E };
async function fetchUser(id: string): Promise> {
try {
const response = await fetch(/api/users/${id});
if (!response.ok) {
return { success: false, error: 'User not found' };
}
const user = await response.json();
return { success: true, data: user };
} catch (error) {
return { success: false, error: 'Network error' };
}
}
// Usage
const result = await fetchUser('123');
if (result.success) {
console.log(result.data.name); // TypeScript knows this is User
} else {
console.error(result.error); // TypeScript knows this is string
}
2. Branded Types
Create distinct types for better type safety:
typescript
// Branded types for different ID types
type UserId = string & { __brand: 'UserId' };
type ProductId = string & { __brand: 'ProductId' };
function createUserId(id: string): UserId {
return id as UserId;
}
function createProductId(id: string): ProductId {
return id as ProductId;
}
// This prevents mixing up different types of IDs
function getUser(id: UserId): User {
// Implementation
}
const userId = createUserId('123');
const productId = createProductId('456');
getUser(userId); // ✅ OK
getUser(productId); // ❌ TypeScript error
Advanced Function Patterns
1. Function Overloads
Create functions that behave differently based on input types:
typescript
// Overload signatures
function process(value: string): string;
function process(value: number): number;
function process(value: boolean): boolean;
// Implementation signature
function process(value: string | number | boolean): string | number | boolean {
if (typeof value === 'string') {
return value.toUpperCase();
} else if (typeof value === 'number') {
return value * 2;
} else {
return !value;
}
}
// Usage with proper type inference
const result1 = process('hello'); // Type: string
const result2 = process(42); // Type: number
const result3 = process(true); // Type: boolean
2. Curried Functions
Create reusable function factories:
typescript
// Basic currying
function add(a: number) {
return (b: number) => a + b;
}
const add5 = add(5);
console.log(add5(3)); // 8
// Advanced currying with generics
function createApiCall(baseUrl: string) {
return (endpoint: string) => {
return (params: Record): Promise => {
const url = new URL(endpoint, baseUrl);
Object.entries(params).forEach(([key, value]) => {
url.searchParams.set(key, String(value));
});
return fetch(url.toString()).then(res => res.json());
};
};
}
// Usage
const apiCall = createApiCall('https://api.example.com');
const getUser = apiCall('/users');
const user = await getUser({ id: '123' });
Configuration and Best Practices
1. Strict TypeScript Configuration
Enable strict mode for better type checking:
json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true
}
}
2. Type Guards
Create runtime type checking functions:
typescript
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value &&
'email' in value
);
}
// Usage
function processValue(value: unknown) {
if (isString(value)) {
// TypeScript knows value is string here
console.log(value.toUpperCase());
}
if (isUser(value)) {
// TypeScript knows value is User here
console.log(value.name);
}
}
Performance Considerations
1. Type-Only Imports
Use type-only imports to reduce bundle size:
typescript
import type { User, ApiResponse } from './types';
import { fetchUser } from './api';
// This import is removed at compile time
2. Avoid Complex Conditional Types
Complex conditional types can slow down compilation:
typescript
// ❌ Avoid overly complex conditional types
type ComplexType = T extends infer U
? U extends string
? U extends ${string}-${string}
? U
: never
: never
: never;
// ✅ Prefer simpler, more readable types
type KebabCase = T extends ${infer A}${infer B}
? ${A}-${KebabCase}
: T;
Conclusion
TypeScript's advanced features can significantly improve your code quality and developer experience. Start with the basics and gradually incorporate these patterns into your projects. Remember, the goal is to make your code more maintainable, not more complex.
Focus on:
- Using strict type checking
- Creating reusable type patterns
- Implementing proper error handling
- Writing self-documenting code through types
The investment in learning these patterns will pay off in reduced bugs, better IDE support, and more maintainable codebases.
typescript
// Make all properties optional
type Partial = {
[P in keyof T]?: T[P];
};
// Make all properties required
type Required = {
[P in keyof T]-?: T[P];
};
// Pick specific properties
type User = {
id: number;
name: string;
email: string;
password: string;
};
type PublicUser = Pick;
type UserUpdate = Partial>;
Conditional types allow you to create types that depend on other types:
typescript
// Basic conditional type
type NonNullable = T extends null | undefined ? never : T;
// More complex conditional type
type ApiResponse = T extends string
? { message: T }
: T extends number
? { count: T }
: { data: T };
// Usage
type StringResponse = ApiResponse; // { message: string }
type NumberResponse = ApiResponse; // { count: number }
type ObjectResponse = ApiResponse; // { data: User }
3. Template Literal Types
Create types based on string patterns:
typescript
type EventName = on${Capitalize} ;
type ActionName = ${T}Action;
// Usage
type ClickEvent = EventName<'click'>; // 'onClick'
type LoadAction = ActionName<'load'>; // 'loadAction'
// More complex example
type CSSProperty = --${T};
type ThemeColor = CSSProperty<'primary' | 'secondary'>; // '--primary' | '--secondary'
Advanced Generic Patterns
1. Generic Constraints
Use constraints to limit what types can be used with your generics:
typescript
// Basic constraint
function getProperty(obj: T, key: K): T[K] {
return obj[key];
}
// Multiple constraints
interface Lengthwise {
length: number;
}
function logLength(arg: T): T {
console.log(arg.length);
return arg;
}
// Constraint with conditional types
type NonEmptyArray = T[] & { 0: T };
function getFirst(arr: NonEmptyArray): T {
return arr[0];
}
2. Mapped Types
Create new types by transforming existing ones:
typescript
// Make all properties readonly
type Readonly = {
readonly [P in keyof T]: T[P];
};
// Create a type with all properties as functions
type FunctionProperty = {
[K in keyof T]: () => T[K];
};
// Create a type with optional properties
type Optional = {
[K in keyof T]?: T[K];
};
Error Handling Patterns
1. Result Type Pattern
Create a robust error handling system:
typescript
type Result =
| { success: true; data: T }
| { success: false; error: E };
async function fetchUser(id: string): Promise> {
try {
const response = await fetch(/api/users/${id});
if (!response.ok) {
return { success: false, error: 'User not found' };
}
const user = await response.json();
return { success: true, data: user };
} catch (error) {
return { success: false, error: 'Network error' };
}
}
// Usage
const result = await fetchUser('123');
if (result.success) {
console.log(result.data.name); // TypeScript knows this is User
} else {
console.error(result.error); // TypeScript knows this is string
}
2. Branded Types
Create distinct types for better type safety:
typescript
// Branded types for different ID types
type UserId = string & { __brand: 'UserId' };
type ProductId = string & { __brand: 'ProductId' };
function createUserId(id: string): UserId {
return id as UserId;
}
function createProductId(id: string): ProductId {
return id as ProductId;
}
// This prevents mixing up different types of IDs
function getUser(id: UserId): User {
// Implementation
}
const userId = createUserId('123');
const productId = createProductId('456');
getUser(userId); // ✅ OK
getUser(productId); // ❌ TypeScript error
Advanced Function Patterns
1. Function Overloads
Create functions that behave differently based on input types:
typescript
// Overload signatures
function process(value: string): string;
function process(value: number): number;
function process(value: boolean): boolean;
// Implementation signature
function process(value: string | number | boolean): string | number | boolean {
if (typeof value === 'string') {
return value.toUpperCase();
} else if (typeof value === 'number') {
return value * 2;
} else {
return !value;
}
}
// Usage with proper type inference
const result1 = process('hello'); // Type: string
const result2 = process(42); // Type: number
const result3 = process(true); // Type: boolean
2. Curried Functions
Create reusable function factories:
typescript
// Basic currying
function add(a: number) {
return (b: number) => a + b;
}
const add5 = add(5);
console.log(add5(3)); // 8
// Advanced currying with generics
function createApiCall(baseUrl: string) {
return (endpoint: string) => {
return (params: Record): Promise => {
const url = new URL(endpoint, baseUrl);
Object.entries(params).forEach(([key, value]) => {
url.searchParams.set(key, String(value));
});
return fetch(url.toString()).then(res => res.json());
};
};
}
// Usage
const apiCall = createApiCall('https://api.example.com');
const getUser = apiCall('/users');
const user = await getUser({ id: '123' });
Configuration and Best Practices
1. Strict TypeScript Configuration
Enable strict mode for better type checking:
json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true
}
}
2. Type Guards
Create runtime type checking functions:
typescript
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value &&
'email' in value
);
}
// Usage
function processValue(value: unknown) {
if (isString(value)) {
// TypeScript knows value is string here
console.log(value.toUpperCase());
}
if (isUser(value)) {
// TypeScript knows value is User here
console.log(value.name);
}
}
Performance Considerations
1. Type-Only Imports
Use type-only imports to reduce bundle size:
typescript
import type { User, ApiResponse } from './types';
import { fetchUser } from './api';
// This import is removed at compile time
2. Avoid Complex Conditional Types
Complex conditional types can slow down compilation:
typescript
// ❌ Avoid overly complex conditional types
type ComplexType = T extends infer U
? U extends string
? U extends ${string}-${string}
? U
: never
: never
: never;
// ✅ Prefer simpler, more readable types
type KebabCase = T extends ${infer A}${infer B}
? ${A}-${KebabCase}
: T;
Conclusion
TypeScript's advanced features can significantly improve your code quality and developer experience. Start with the basics and gradually incorporate these patterns into your projects. Remember, the goal is to make your code more maintainable, not more complex.
Focus on:
- Using strict type checking
- Creating reusable type patterns
- Implementing proper error handling
- Writing self-documenting code through types
The investment in learning these patterns will pay off in reduced bugs, better IDE support, and more maintainable codebases.
typescript
type EventName = on${Capitalize} ;
type ActionName = ${T}Action;
// Usage
type ClickEvent = EventName<'click'>; // 'onClick'
type LoadAction = ActionName<'load'>; // 'loadAction'
// More complex example
type CSSProperty = --${T};
type ThemeColor = CSSProperty<'primary' | 'secondary'>; // '--primary' | '--secondary'
1. Generic Constraints
Use constraints to limit what types can be used with your generics:
typescript
// Basic constraint
function getProperty(obj: T, key: K): T[K] {
return obj[key];
}
// Multiple constraints
interface Lengthwise {
length: number;
}
function logLength(arg: T): T {
console.log(arg.length);
return arg;
}
// Constraint with conditional types
type NonEmptyArray = T[] & { 0: T };
function getFirst(arr: NonEmptyArray): T {
return arr[0];
}
2. Mapped Types
Create new types by transforming existing ones:
typescript
// Make all properties readonly
type Readonly = {
readonly [P in keyof T]: T[P];
};
// Create a type with all properties as functions
type FunctionProperty = {
[K in keyof T]: () => T[K];
};
// Create a type with optional properties
type Optional = {
[K in keyof T]?: T[K];
};
Error Handling Patterns
1. Result Type Pattern
Create a robust error handling system:
typescript
type Result =
| { success: true; data: T }
| { success: false; error: E };
async function fetchUser(id: string): Promise> {
try {
const response = await fetch(/api/users/${id});
if (!response.ok) {
return { success: false, error: 'User not found' };
}
const user = await response.json();
return { success: true, data: user };
} catch (error) {
return { success: false, error: 'Network error' };
}
}
// Usage
const result = await fetchUser('123');
if (result.success) {
console.log(result.data.name); // TypeScript knows this is User
} else {
console.error(result.error); // TypeScript knows this is string
}
2. Branded Types
Create distinct types for better type safety:
typescript
// Branded types for different ID types
type UserId = string & { __brand: 'UserId' };
type ProductId = string & { __brand: 'ProductId' };
function createUserId(id: string): UserId {
return id as UserId;
}
function createProductId(id: string): ProductId {
return id as ProductId;
}
// This prevents mixing up different types of IDs
function getUser(id: UserId): User {
// Implementation
}
const userId = createUserId('123');
const productId = createProductId('456');
getUser(userId); // ✅ OK
getUser(productId); // ❌ TypeScript error
Advanced Function Patterns
1. Function Overloads
Create functions that behave differently based on input types:
typescript
// Overload signatures
function process(value: string): string;
function process(value: number): number;
function process(value: boolean): boolean;
// Implementation signature
function process(value: string | number | boolean): string | number | boolean {
if (typeof value === 'string') {
return value.toUpperCase();
} else if (typeof value === 'number') {
return value * 2;
} else {
return !value;
}
}
// Usage with proper type inference
const result1 = process('hello'); // Type: string
const result2 = process(42); // Type: number
const result3 = process(true); // Type: boolean
2. Curried Functions
Create reusable function factories:
typescript
// Basic currying
function add(a: number) {
return (b: number) => a + b;
}
const add5 = add(5);
console.log(add5(3)); // 8
// Advanced currying with generics
function createApiCall(baseUrl: string) {
return (endpoint: string) => {
return (params: Record): Promise => {
const url = new URL(endpoint, baseUrl);
Object.entries(params).forEach(([key, value]) => {
url.searchParams.set(key, String(value));
});
return fetch(url.toString()).then(res => res.json());
};
};
}
// Usage
const apiCall = createApiCall('https://api.example.com');
const getUser = apiCall('/users');
const user = await getUser({ id: '123' });
Configuration and Best Practices
1. Strict TypeScript Configuration
Enable strict mode for better type checking:
json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true
}
}
2. Type Guards
Create runtime type checking functions:
typescript
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value &&
'email' in value
);
}
// Usage
function processValue(value: unknown) {
if (isString(value)) {
// TypeScript knows value is string here
console.log(value.toUpperCase());
}
if (isUser(value)) {
// TypeScript knows value is User here
console.log(value.name);
}
}
Performance Considerations
1. Type-Only Imports
Use type-only imports to reduce bundle size:
typescript
import type { User, ApiResponse } from './types';
import { fetchUser } from './api';
// This import is removed at compile time
2. Avoid Complex Conditional Types
Complex conditional types can slow down compilation:
typescript
// ❌ Avoid overly complex conditional types
type ComplexType = T extends infer U
? U extends string
? U extends ${string}-${string}
? U
: never
: never
: never;
// ✅ Prefer simpler, more readable types
type KebabCase = T extends ${infer A}${infer B}
? ${A}-${KebabCase}
: T;
Conclusion
TypeScript's advanced features can significantly improve your code quality and developer experience. Start with the basics and gradually incorporate these patterns into your projects. Remember, the goal is to make your code more maintainable, not more complex.
Focus on:
- Using strict type checking
- Creating reusable type patterns
- Implementing proper error handling
- Writing self-documenting code through types
The investment in learning these patterns will pay off in reduced bugs, better IDE support, and more maintainable codebases.
typescript
// Basic constraint
function getProperty(obj: T, key: K): T[K] {
return obj[key];
}
// Multiple constraints
interface Lengthwise {
length: number;
}
function logLength(arg: T): T {
console.log(arg.length);
return arg;
}
// Constraint with conditional types
type NonEmptyArray = T[] & { 0: T };
function getFirst(arr: NonEmptyArray): T {
return arr[0];
}
Create new types by transforming existing ones:
typescript
// Make all properties readonly
type Readonly = {
readonly [P in keyof T]: T[P];
};
// Create a type with all properties as functions
type FunctionProperty = {
[K in keyof T]: () => T[K];
};
// Create a type with optional properties
type Optional = {
[K in keyof T]?: T[K];
};
Error Handling Patterns
1. Result Type Pattern
Create a robust error handling system:
typescript
type Result =
| { success: true; data: T }
| { success: false; error: E };
async function fetchUser(id: string): Promise> {
try {
const response = await fetch(/api/users/${id});
if (!response.ok) {
return { success: false, error: 'User not found' };
}
const user = await response.json();
return { success: true, data: user };
} catch (error) {
return { success: false, error: 'Network error' };
}
}
// Usage
const result = await fetchUser('123');
if (result.success) {
console.log(result.data.name); // TypeScript knows this is User
} else {
console.error(result.error); // TypeScript knows this is string
}
2. Branded Types
Create distinct types for better type safety:
typescript
// Branded types for different ID types
type UserId = string & { __brand: 'UserId' };
type ProductId = string & { __brand: 'ProductId' };
function createUserId(id: string): UserId {
return id as UserId;
}
function createProductId(id: string): ProductId {
return id as ProductId;
}
// This prevents mixing up different types of IDs
function getUser(id: UserId): User {
// Implementation
}
const userId = createUserId('123');
const productId = createProductId('456');
getUser(userId); // ✅ OK
getUser(productId); // ❌ TypeScript error
Advanced Function Patterns
1. Function Overloads
Create functions that behave differently based on input types:
typescript
// Overload signatures
function process(value: string): string;
function process(value: number): number;
function process(value: boolean): boolean;
// Implementation signature
function process(value: string | number | boolean): string | number | boolean {
if (typeof value === 'string') {
return value.toUpperCase();
} else if (typeof value === 'number') {
return value * 2;
} else {
return !value;
}
}
// Usage with proper type inference
const result1 = process('hello'); // Type: string
const result2 = process(42); // Type: number
const result3 = process(true); // Type: boolean
2. Curried Functions
Create reusable function factories:
typescript
// Basic currying
function add(a: number) {
return (b: number) => a + b;
}
const add5 = add(5);
console.log(add5(3)); // 8
// Advanced currying with generics
function createApiCall(baseUrl: string) {
return (endpoint: string) => {
return (params: Record): Promise => {
const url = new URL(endpoint, baseUrl);
Object.entries(params).forEach(([key, value]) => {
url.searchParams.set(key, String(value));
});
return fetch(url.toString()).then(res => res.json());
};
};
}
// Usage
const apiCall = createApiCall('https://api.example.com');
const getUser = apiCall('/users');
const user = await getUser({ id: '123' });
Configuration and Best Practices
1. Strict TypeScript Configuration
Enable strict mode for better type checking:
json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true
}
}
2. Type Guards
Create runtime type checking functions:
typescript
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value &&
'email' in value
);
}
// Usage
function processValue(value: unknown) {
if (isString(value)) {
// TypeScript knows value is string here
console.log(value.toUpperCase());
}
if (isUser(value)) {
// TypeScript knows value is User here
console.log(value.name);
}
}
Performance Considerations
1. Type-Only Imports
Use type-only imports to reduce bundle size:
typescript
import type { User, ApiResponse } from './types';
import { fetchUser } from './api';
// This import is removed at compile time
2. Avoid Complex Conditional Types
Complex conditional types can slow down compilation:
typescript
// ❌ Avoid overly complex conditional types
type ComplexType = T extends infer U
? U extends string
? U extends ${string}-${string}
? U
: never
: never
: never;
// ✅ Prefer simpler, more readable types
type KebabCase = T extends ${infer A}${infer B}
? ${A}-${KebabCase}
: T;
Conclusion
TypeScript's advanced features can significantly improve your code quality and developer experience. Start with the basics and gradually incorporate these patterns into your projects. Remember, the goal is to make your code more maintainable, not more complex.
Focus on:
- Using strict type checking
- Creating reusable type patterns
- Implementing proper error handling
- Writing self-documenting code through types
The investment in learning these patterns will pay off in reduced bugs, better IDE support, and more maintainable codebases.
Create a robust error handling system:
typescript
type Result =
| { success: true; data: T }
| { success: false; error: E };
async function fetchUser(id: string): Promise> {
try {
const response = await fetch(/api/users/${id});
if (!response.ok) {
return { success: false, error: 'User not found' };
}
const user = await response.json();
return { success: true, data: user };
} catch (error) {
return { success: false, error: 'Network error' };
}
}
// Usage
const result = await fetchUser('123');
if (result.success) {
console.log(result.data.name); // TypeScript knows this is User
} else {
console.error(result.error); // TypeScript knows this is string
}
2. Branded Types
Create distinct types for better type safety:
typescript
// Branded types for different ID types
type UserId = string & { __brand: 'UserId' };
type ProductId = string & { __brand: 'ProductId' };
function createUserId(id: string): UserId {
return id as UserId;
}
function createProductId(id: string): ProductId {
return id as ProductId;
}
// This prevents mixing up different types of IDs
function getUser(id: UserId): User {
// Implementation
}
const userId = createUserId('123');
const productId = createProductId('456');
getUser(userId); // ✅ OK
getUser(productId); // ❌ TypeScript error
Advanced Function Patterns
1. Function Overloads
Create functions that behave differently based on input types:
typescript
// Overload signatures
function process(value: string): string;
function process(value: number): number;
function process(value: boolean): boolean;
// Implementation signature
function process(value: string | number | boolean): string | number | boolean {
if (typeof value === 'string') {
return value.toUpperCase();
} else if (typeof value === 'number') {
return value * 2;
} else {
return !value;
}
}
// Usage with proper type inference
const result1 = process('hello'); // Type: string
const result2 = process(42); // Type: number
const result3 = process(true); // Type: boolean
2. Curried Functions
Create reusable function factories:
typescript
// Basic currying
function add(a: number) {
return (b: number) => a + b;
}
const add5 = add(5);
console.log(add5(3)); // 8
// Advanced currying with generics
function createApiCall(baseUrl: string) {
return (endpoint: string) => {
return (params: Record): Promise => {
const url = new URL(endpoint, baseUrl);
Object.entries(params).forEach(([key, value]) => {
url.searchParams.set(key, String(value));
});
return fetch(url.toString()).then(res => res.json());
};
};
}
// Usage
const apiCall = createApiCall('https://api.example.com');
const getUser = apiCall('/users');
const user = await getUser({ id: '123' });
Configuration and Best Practices
1. Strict TypeScript Configuration
Enable strict mode for better type checking:
json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true
}
}
2. Type Guards
Create runtime type checking functions:
typescript
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value &&
'email' in value
);
}
// Usage
function processValue(value: unknown) {
if (isString(value)) {
// TypeScript knows value is string here
console.log(value.toUpperCase());
}
if (isUser(value)) {
// TypeScript knows value is User here
console.log(value.name);
}
}
Performance Considerations
1. Type-Only Imports
Use type-only imports to reduce bundle size:
typescript
import type { User, ApiResponse } from './types';
import { fetchUser } from './api';
// This import is removed at compile time
2. Avoid Complex Conditional Types
Complex conditional types can slow down compilation:
typescript
// ❌ Avoid overly complex conditional types
type ComplexType = T extends infer U
? U extends string
? U extends ${string}-${string}
? U
: never
: never
: never;
// ✅ Prefer simpler, more readable types
type KebabCase = T extends ${infer A}${infer B}
? ${A}-${KebabCase}
: T;
Conclusion
TypeScript's advanced features can significantly improve your code quality and developer experience. Start with the basics and gradually incorporate these patterns into your projects. Remember, the goal is to make your code more maintainable, not more complex.
Focus on:
- Using strict type checking
- Creating reusable type patterns
- Implementing proper error handling
- Writing self-documenting code through types
The investment in learning these patterns will pay off in reduced bugs, better IDE support, and more maintainable codebases.
typescript
// Branded types for different ID types
type UserId = string & { __brand: 'UserId' };
type ProductId = string & { __brand: 'ProductId' };
function createUserId(id: string): UserId {
return id as UserId;
}
function createProductId(id: string): ProductId {
return id as ProductId;
}
// This prevents mixing up different types of IDs
function getUser(id: UserId): User {
// Implementation
}
const userId = createUserId('123');
const productId = createProductId('456');
getUser(userId); // ✅ OK
getUser(productId); // ❌ TypeScript error
1. Function Overloads
Create functions that behave differently based on input types:
typescript
// Overload signatures
function process(value: string): string;
function process(value: number): number;
function process(value: boolean): boolean;
// Implementation signature
function process(value: string | number | boolean): string | number | boolean {
if (typeof value === 'string') {
return value.toUpperCase();
} else if (typeof value === 'number') {
return value * 2;
} else {
return !value;
}
}
// Usage with proper type inference
const result1 = process('hello'); // Type: string
const result2 = process(42); // Type: number
const result3 = process(true); // Type: boolean
2. Curried Functions
Create reusable function factories:
typescript
// Basic currying
function add(a: number) {
return (b: number) => a + b;
}
const add5 = add(5);
console.log(add5(3)); // 8
// Advanced currying with generics
function createApiCall(baseUrl: string) {
return (endpoint: string) => {
return (params: Record): Promise => {
const url = new URL(endpoint, baseUrl);
Object.entries(params).forEach(([key, value]) => {
url.searchParams.set(key, String(value));
});
return fetch(url.toString()).then(res => res.json());
};
};
}
// Usage
const apiCall = createApiCall('https://api.example.com');
const getUser = apiCall('/users');
const user = await getUser({ id: '123' });
Configuration and Best Practices
1. Strict TypeScript Configuration
Enable strict mode for better type checking:
json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true
}
}
2. Type Guards
Create runtime type checking functions:
typescript
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value &&
'email' in value
);
}
// Usage
function processValue(value: unknown) {
if (isString(value)) {
// TypeScript knows value is string here
console.log(value.toUpperCase());
}
if (isUser(value)) {
// TypeScript knows value is User here
console.log(value.name);
}
}
Performance Considerations
1. Type-Only Imports
Use type-only imports to reduce bundle size:
typescript
import type { User, ApiResponse } from './types';
import { fetchUser } from './api';
// This import is removed at compile time
2. Avoid Complex Conditional Types
Complex conditional types can slow down compilation:
typescript
// ❌ Avoid overly complex conditional types
type ComplexType = T extends infer U
? U extends string
? U extends ${string}-${string}
? U
: never
: never
: never;
// ✅ Prefer simpler, more readable types
type KebabCase = T extends ${infer A}${infer B}
? ${A}-${KebabCase}
: T;
Conclusion
TypeScript's advanced features can significantly improve your code quality and developer experience. Start with the basics and gradually incorporate these patterns into your projects. Remember, the goal is to make your code more maintainable, not more complex.
Focus on:
- Using strict type checking
- Creating reusable type patterns
- Implementing proper error handling
- Writing self-documenting code through types
The investment in learning these patterns will pay off in reduced bugs, better IDE support, and more maintainable codebases.
typescript
// Overload signatures
function process(value: string): string;
function process(value: number): number;
function process(value: boolean): boolean;
// Implementation signature
function process(value: string | number | boolean): string | number | boolean {
if (typeof value === 'string') {
return value.toUpperCase();
} else if (typeof value === 'number') {
return value * 2;
} else {
return !value;
}
}
// Usage with proper type inference
const result1 = process('hello'); // Type: string
const result2 = process(42); // Type: number
const result3 = process(true); // Type: boolean
Create reusable function factories:
typescript
// Basic currying
function add(a: number) {
return (b: number) => a + b;
}
const add5 = add(5);
console.log(add5(3)); // 8
// Advanced currying with generics
function createApiCall(baseUrl: string) {
return (endpoint: string) => {
return (params: Record): Promise => {
const url = new URL(endpoint, baseUrl);
Object.entries(params).forEach(([key, value]) => {
url.searchParams.set(key, String(value));
});
return fetch(url.toString()).then(res => res.json());
};
};
}
// Usage
const apiCall = createApiCall('https://api.example.com');
const getUser = apiCall('/users');
const user = await getUser({ id: '123' });
Configuration and Best Practices
1. Strict TypeScript Configuration
Enable strict mode for better type checking:
json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true
}
}
2. Type Guards
Create runtime type checking functions:
typescript
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value &&
'email' in value
);
}
// Usage
function processValue(value: unknown) {
if (isString(value)) {
// TypeScript knows value is string here
console.log(value.toUpperCase());
}
if (isUser(value)) {
// TypeScript knows value is User here
console.log(value.name);
}
}
Performance Considerations
1. Type-Only Imports
Use type-only imports to reduce bundle size:
typescript
import type { User, ApiResponse } from './types';
import { fetchUser } from './api';
// This import is removed at compile time
2. Avoid Complex Conditional Types
Complex conditional types can slow down compilation:
typescript
// ❌ Avoid overly complex conditional types
type ComplexType = T extends infer U
? U extends string
? U extends ${string}-${string}
? U
: never
: never
: never;
// ✅ Prefer simpler, more readable types
type KebabCase = T extends ${infer A}${infer B}
? ${A}-${KebabCase}
: T;
Conclusion
TypeScript's advanced features can significantly improve your code quality and developer experience. Start with the basics and gradually incorporate these patterns into your projects. Remember, the goal is to make your code more maintainable, not more complex.
Focus on:
- Using strict type checking
- Creating reusable type patterns
- Implementing proper error handling
- Writing self-documenting code through types
The investment in learning these patterns will pay off in reduced bugs, better IDE support, and more maintainable codebases.
Enable strict mode for better type checking:
json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true
}
}
2. Type Guards
Create runtime type checking functions:
typescript
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value &&
'email' in value
);
}
// Usage
function processValue(value: unknown) {
if (isString(value)) {
// TypeScript knows value is string here
console.log(value.toUpperCase());
}
if (isUser(value)) {
// TypeScript knows value is User here
console.log(value.name);
}
}
Performance Considerations
1. Type-Only Imports
Use type-only imports to reduce bundle size:
typescript
import type { User, ApiResponse } from './types';
import { fetchUser } from './api';
// This import is removed at compile time
2. Avoid Complex Conditional Types
Complex conditional types can slow down compilation:
typescript
// ❌ Avoid overly complex conditional types
type ComplexType = T extends infer U
? U extends string
? U extends ${string}-${string}
? U
: never
: never
: never;
// ✅ Prefer simpler, more readable types
type KebabCase = T extends ${infer A}${infer B}
? ${A}-${KebabCase}
: T;
Conclusion
TypeScript's advanced features can significantly improve your code quality and developer experience. Start with the basics and gradually incorporate these patterns into your projects. Remember, the goal is to make your code more maintainable, not more complex.
Focus on:
- Using strict type checking
- Creating reusable type patterns
- Implementing proper error handling
- Writing self-documenting code through types
The investment in learning these patterns will pay off in reduced bugs, better IDE support, and more maintainable codebases.
typescript
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value &&
'email' in value
);
}
// Usage
function processValue(value: unknown) {
if (isString(value)) {
// TypeScript knows value is string here
console.log(value.toUpperCase());
}
if (isUser(value)) {
// TypeScript knows value is User here
console.log(value.name);
}
}
1. Type-Only Imports
Use type-only imports to reduce bundle size:
typescript
import type { User, ApiResponse } from './types';
import { fetchUser } from './api';
// This import is removed at compile time
2. Avoid Complex Conditional Types
Complex conditional types can slow down compilation:
typescript
// ❌ Avoid overly complex conditional types
type ComplexType = T extends infer U
? U extends string
? U extends ${string}-${string}
? U
: never
: never
: never;
// ✅ Prefer simpler, more readable types
type KebabCase = T extends ${infer A}${infer B}
? ${A}-${KebabCase}
: T;
Conclusion
TypeScript's advanced features can significantly improve your code quality and developer experience. Start with the basics and gradually incorporate these patterns into your projects. Remember, the goal is to make your code more maintainable, not more complex.
Focus on:
- Using strict type checking
- Creating reusable type patterns
- Implementing proper error handling
- Writing self-documenting code through types
The investment in learning these patterns will pay off in reduced bugs, better IDE support, and more maintainable codebases.
typescript
import type { User, ApiResponse } from './types';
import { fetchUser } from './api';
// This import is removed at compile time
Complex conditional types can slow down compilation:
typescript
// ❌ Avoid overly complex conditional types
type ComplexType = T extends infer U
? U extends string
? U extends ${string}-${string}
? U
: never
: never
: never;
// ✅ Prefer simpler, more readable types
type KebabCase = T extends ${infer A}${infer B}
? ${A}-${KebabCase}
: T;