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.