Skip to main content

avoiding-common-gotchas

Avoiding Common 'Gotchas':

Understand 'any' Type Risks:

Be cautious when using the any type, as it can bypass the compiler's type checking and lead to runtime errors.

let riskyAny: any = "I could be anything";
let strLength: number = riskyAny.length; // Compiles, but risky if 'riskyAny' changes to non-string type later.

Never Assume Object Shapes:

Don't assume the shape of an object. Always declare the type or interface of an object to ensure its structure.

interface User {
name: string;
age: number;
}

function greet(user: User) {
console.log(`Hello, ${user.name}`);
}

// This will throw an error if you try to pass an object with a different shape.
greet({ name: "Alice", age: 25 }); // Correct usage

Triple Equals for Comparison:

Use triple equals (===) for comparisons to avoid unexpected type coercion that can occur with double equals (==).

let value1: string = "5";
let value2: number = 5;

// This will be false, as the types are different.
let comparison = value1 === value2;

Proper Use of this:

Remember that this can behave differently in callbacks. Use arrow functions to maintain the context of this or explicitly bind it.

class MyClass {
value: number = 10;

printValue() {
setTimeout(() => {
console.log(this.value); // 'this' correctly refers to the instance of MyClass due to the arrow function
}, 1000);
}
}

Null Checks:

Perform null checks or use optional chaining (?.) to prevent null or undefined access errors.

function printAddress(street: string | null) {
if (street) {
console.log(street);
}
}

// or with optional chaining
function printAddressOptional(user: { address?: { street: string } }) {
console.log(user.address?.street);
}

Initialization Errors:

Always initialize variables. Accessing uninitialized variables can lead to undefined errors.

let myVar: number; // Uninitialized, can lead to undefined errors
myVar = 5; // Proper initialization

Array Mutation Awareness:

Be aware of array methods that mutate the array. Use immutable methods to avoid unintended side effects.

let originalArray = [1, 2, 3];

// Mutation: pushes a new value, changing the original array
originalArray.push(4);

// Immutable approach: creates a new array with the new value
let newArray = [...originalArray, 4];

Consistent Enum Usage:

When using enums, be consistent with either using the enum member or its value to avoid confusion and errors.

enum Color {
Red = "RED",
Green = "GREEN",
Blue = "BLUE"
}

let myColor: Color = Color.Red; // Use enum member

// Consistently use 'Color.Red', not 'RED' or 'Color["Red"]'
function paint(color: Color) {
console.log(color);
}

In these snippets, TypeScript's type system is used to prevent common errors and ensure code reliability, which is essential for maintaining a robust codebase.

Function Overloads Correctly:

When overloading functions, make sure the most general type signatures come last, so the more specific ones get matched first.

function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
if (d !== undefined && y !== undefined) {
return new Date(y, mOrTimestamp, d);
} else {
return new Date(mOrTimestamp);
}
}

const dateFromTimestamp = makeDate(1627688441086); // uses the first overload
const specificDate = makeDate(7, 30, 2021); // uses the second overload

Type Assertions:

Use type assertions wisely. Asserting a type tells the compiler to trust you, which can hide real issues.

let someValue: any = "this is a string";

// Asserting type as string
let strLength: number = (someValue as string).length;

Understanding null vs. undefined:

Understand the difference between null and undefined and when TypeScript expects each.

let uninitializedVar: string | undefined; // TypeScript initializes variables as undefined by default
let nullableVar: string | null = null; // Use null to represent an intentional absence of value

Avoid declare for Side Effects:

Avoid using declare for global variables or modules that have side effects. Instead, use proper module imports and typings.

// Instead of declaring a global
declare const MY_GLOBAL: any;

// Properly import the module or variable
import { MY_GLOBAL } from 'some-module';

Intersection Types with interface:

When creating intersection types, be cautious when combining interfaces that could have overlapping properties with incompatible types.

interface Runnable {
run(): void;
}

interface Flyable {
fly(): void;
}

// Combining both interfaces into one type
type Action = Runnable & Flyable;

let action: Action = {
run() {
console.log('Running');
},
fly() {
console.log('Flying');
}
};

Asynchronous Pitfalls:

Remember to handle promises and asynchronous code correctly. Use async/await with proper error handling to avoid unhandled promise rejections.

async function fetchData() {
try {
let response = await fetch('https://api.example.com/data');
let data = await response.json();
console.log(data);
} catch (error) {
console.error('An error occurred:', error);
}
}

Type Inference Reliance:

Rely on type inference when appropriate, but don't shy away from declaring types explicitly when it adds clarity to the code.

// Type inference is clear here
let inferredString = 'This is a clearly inferred string';

// Explicit type adds clarity here
let notSoObvious: number = calculateComplexThing();

These examples demonstrate prudent practices for function overloading, type assertions, handling null and undefined, proper module usage, intersection types, asynchronous patterns, and the balance of type inference with explicit typing.