Introduction
Getting Started
- QuickStart
Patterns
- Languages
- Supported Languages
- Python
- Java
- JavaScript
- TypeScript
- Node.js
- React
- Fastify
- Next.js
- Terraform
- C#
- C++
- C
- Go
- Rust
- Swift
- React Native
- Spring Boot
- Kotlin
- Flutter
- Ruby
- PHP
- Scala
- Perl
- R
- Dart
- Elixir
- Erlang
- Haskell
- Lua
- Julia
- Clojure
- Groovy
- Fortran
- COBOL
- Pascal
- Assembly
- Bash
- PowerShell
- SQL
- PL/SQL
- T-SQL
- MATLAB
- Objective-C
- VBA
- ABAP
- Apex
- Apache Camel
- Crystal
- D
- Delphi
- Elm
- F#
- Hack
- Lisp
- OCaml
- Prolog
- Racket
- Scheme
- Solidity
- Verilog
- VHDL
- Zig
- MongoDB
- ClickHouse
- MySQL
- GraphQL
- Redis
- Cassandra
- Elasticsearch
- Security
- Performance
Integrations
- Code Repositories
- Team Messengers
- Ticketing
Enterprise
TypeScript is a strongly typed programming language that builds on JavaScript, giving you better tooling at any scale. It adds static types to JavaScript, helping catch errors early and making code more maintainable.
TypeScript, while improving upon JavaScript with static typing, still has common anti-patterns that can lead to bugs, performance issues, and maintenance problems. Here are the most important anti-patterns to avoid when writing TypeScript code.
// Anti-pattern: Overusing 'any'
function processData(data: any): any {
return data.map((item: any) => item.value);
}
// Better approach: Define proper types
interface DataItem {
value: string;
id: number;
}
function processData(data: DataItem[]): string[] {
return data.map(item => item.value);
}
Using any
defeats the purpose of TypeScript’s type system. It bypasses type checking and can lead to runtime errors that TypeScript is designed to prevent.
// Anti-pattern: Unsafe type assertion
const userData = JSON.parse(response) as UserData;
// Better approach: Validate before assertion
function isUserData(obj: any): obj is UserData {
return obj && typeof obj.name === 'string' && typeof obj.id === 'number';
}
const parsedData = JSON.parse(response);
if (isUserData(parsedData)) {
const userData: UserData = parsedData;
// Use userData safely
}
Type assertions (as
keyword) tell the compiler to trust you without verification. Use type guards to validate data at runtime.
// Anti-pattern: Mutable interface properties
interface Config {
apiUrl: string;
maxRetries: number;
}
// Better approach: Use readonly for immutable properties
interface Config {
readonly apiUrl: string;
readonly maxRetries: number;
}
Use the readonly
modifier for properties that shouldn’t change after initialization to prevent accidental mutations.
// Anti-pattern: Using Object as a type
function processObject(obj: Object) {
// ...
}
// Better approach: Use more specific types or generics
function processObject<T extends Record<string, unknown>>(obj: T) {
// ...
}
The Object
type is too general and doesn’t provide useful type information. Use more specific types or generics.
// Anti-pattern: Complex type checking
interface Square {
kind?: string;
size: number;
}
interface Rectangle {
kind?: string;
width: number;
height: number;
}
type Shape = Square | Rectangle;
function area(shape: Shape) {
if ('size' in shape) {
return shape.size * shape.size;
}
if ('width' in shape) {
return shape.width * shape.height;
}
}
// Better approach: Use discriminated unions
interface Square {
kind: 'square';
size: number;
}
interface Rectangle {
kind: 'rectangle';
width: number;
height: number;
}
type Shape = Square | Rectangle;
function area(shape: Shape) {
switch (shape.kind) {
case 'square':
return shape.size * shape.size;
case 'rectangle':
return shape.width * shape.height;
}
}
Discriminated unions (also called tagged unions) make working with union types safer and more maintainable by adding a common property that can be used to distinguish between types.
// Anti-pattern: Using function types for complex callbacks
type FetchCallback = (data: any, error: Error | null) => void;
// Better approach: Use interfaces for complex callbacks
interface FetchCallback {
(data: any, error: Error | null): void;
cancel?: () => void;
retry?: () => void;
}
For complex function types, especially those with additional properties, use interfaces instead of simple function types.
// Anti-pattern: Not using strict null checks
// tsconfig.json: { "strictNullChecks": false }
function getLength(text: string) {
return text.length; // text could be null or undefined
}
// Better approach: Enable strict null checks
// tsconfig.json: { "strictNullChecks": true }
function getLength(text: string | null | undefined) {
return text?.length ?? 0; // Safely handle null/undefined
}
Always enable strictNullChecks
in your TypeScript configuration to catch null and undefined errors at compile time.
// Anti-pattern: Using string enums for simple cases
enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT'
}
// Better approach: Use literal types for simple cases
type Direction = 'UP' | 'DOWN' | 'LEFT' | 'RIGHT';
For simple string constants, string literal unions are often more appropriate than enums, as they result in less code and are more straightforward.
// Anti-pattern: Incorrect index signature
interface StringDictionary {
[key: string]: string;
length: number; // Error: Property 'length' of type 'number' is not assignable to string index type 'string'
}
// Better approach: Use correct index signature
interface StringDictionary {
[key: string]: string | number; // Allow both string and number values
length: number; // Now this is valid
}
When using index signatures, make sure they’re compatible with all properties in the interface.
// Anti-pattern: Manually creating derived types
interface User {
id: number;
name: string;
email: string;
}
interface PartialUser {
id?: number;
name?: string;
email?: string;
}
// Better approach: Use utility types
interface User {
id: number;
name: string;
email: string;
}
// Use built-in utility types
type PartialUser = Partial<User>;
type ReadonlyUser = Readonly<User>;
type UserKeys = keyof User;
TypeScript provides many built-in utility types like Partial
, Readonly
, Pick
, Omit
, etc. Use them instead of manually creating derived types.
// Anti-pattern: Using namespaces
namespace Utils {
export function format(date: Date): string {
return date.toISOString();
}
}
// Better approach: Use ES modules
// utils.ts
export function format(date: Date): string {
return date.toISOString();
}
// app.ts
import { format } from './utils';
Namespaces are an older way of organizing code in TypeScript. Modern TypeScript code should use ES modules (import/export) instead.
// Anti-pattern: Type casting without checks
function processValue(value: string | number) {
const numValue = value as number;
return numValue.toFixed(2); // Runtime error if value is a string
}
// Better approach: Use type guards
function processValue(value: string | number) {
if (typeof value === 'number') {
return value.toFixed(2);
}
return value;
}
Use type guards (typeof
, instanceof
, or custom predicates) to narrow types safely instead of type assertions.
// Anti-pattern: Using types for public APIs
export type User = {
id: number;
name: string;
};
// Better approach: Use interfaces for public APIs
export interface User {
id: number;
name: string;
}
Interfaces are generally preferred for public APIs because they can be extended later without breaking changes, while types cannot.
// Anti-pattern: Using non-null assertion
function getUser(id: string): User | null {
// Implementation might return null
return findUser(id);
}
const user = getUser('123')!; // Dangerous: assumes getUser never returns null
console.log(user.name); // Might cause runtime error
// Better approach: Use proper null checking
const user = getUser('123');
if (user) {
console.log(user.name);
}
The non-null assertion operator (!
) tells TypeScript to ignore the possibility of null
or undefined
. This can lead to runtime errors if the value is actually null.
// Anti-pattern: Using any for API responses
async function fetchData(): Promise<any> {
const response = await fetch('/api/data');
return response.json();
}
// Better approach: Use unknown with type guards
async function fetchData(): Promise<unknown> {
const response = await fetch('/api/data');
return response.json();
}
// Usage with type guard
const data = await fetchData();
if (isUserData(data)) {
// Now TypeScript knows data is UserData
console.log(data.name);
}
Use unknown
instead of any
for values from external sources like API responses, then use type guards to narrow the type safely.
// Anti-pattern: Using primitive types for IDs
interface User {
id: string;
name: string;
}
interface Order {
id: string;
userId: string; // Could accidentally use orderId here
}
// Better approach: Use branded types
type UserId = string & { readonly _brand: unique symbol };
type OrderId = string & { readonly _brand: unique symbol };
function createUserId(id: string): UserId {
return id as UserId;
}
function createOrderId(id: string): OrderId {
return id as OrderId;
}
interface User {
id: UserId;
name: string;
}
interface Order {
id: OrderId;
userId: UserId; // Type safety prevents using OrderId here
}
Branded types (also called nominal types) add type safety to primitive types like strings and numbers, preventing accidental use of the wrong ID type.
// Anti-pattern: Missing exhaustiveness check
type Status = 'pending' | 'fulfilled' | 'rejected';
function handleStatus(status: Status) {
switch (status) {
case 'pending':
return 'Loading...';
case 'fulfilled':
return 'Success!';
// Missing case for 'rejected'
}
}
// Better approach: Use exhaustiveness checking
function handleStatus(status: Status): string {
switch (status) {
case 'pending':
return 'Loading...';
case 'fulfilled':
return 'Success!';
case 'rejected':
return 'Error!';
default:
// This ensures all cases are handled
const _exhaustiveCheck: never = status;
return _exhaustiveCheck;
}
}
Exhaustiveness checking ensures that all possible values of a union type are handled, catching errors when new values are added to the union.
// Anti-pattern: Modifying third-party types directly
// Directly modifying Express Request type (error-prone)
interface Request {
user: User; // Error: Duplicate identifier 'Request'
}
// Better approach: Use proper module augmentation
// In a declaration file (e.g., types.d.ts)
declare namespace Express {
interface Request {
user?: User;
}
}
When extending third-party types, use proper module augmentation instead of trying to modify the original types directly.
// Anti-pattern: Duplicating type logic
type StringOrNumberArray<T> = T extends string ? string[] : number[];
// Separate functions for different types
function processStrings(input: string): string[] {
return input.split('');
}
function processNumbers(input: number): number[] {
return [input, input * 2];
}
// Better approach: Use conditional types
type ProcessResult<T> = T extends string ? string[] : T extends number ? number[] : never;
function process<T extends string | number>(input: T): ProcessResult<T> {
if (typeof input === 'string') {
return input.split('') as ProcessResult<T>;
} else if (typeof input === 'number') {
return [input, input * 2] as ProcessResult<T>;
}
throw new Error('Unsupported input type');
}
Conditional types allow you to create flexible, reusable type definitions that depend on the properties of input types.