Back to blog

TypeScript Best Practices for Modern Web Development

3 min readBy Mustafa Akkaya
#TypeScript#JavaScript#Best Practices

TypeScript has become the standard for building scalable web applications. Let's explore the best practices that will make your TypeScript code more robust and maintainable.

Why TypeScript Matters

TypeScript brings:

- Type Safety - Catch errors before runtime

- Better IDE Support - Enhanced autocomplete and refactoring

- Self-Documenting Code - Types serve as inline documentation

- Confidence in Refactoring - Compiler catches breaking changes

- Enterprise Ready - Scales well for large codebases

Essential Best Practices

1. Use Strict Mode

Always enable strict mode in your tsconfig.json:

data.json

{

class="keyword">class="string">"compilerOptions": {

class="keyword">class="string">"strict": true,

class="keyword">class="string">"noUncheckedIndexedAccess": true,

class="keyword">class="string">"noImplicitOverride": true

}

}

2. Prefer Interfaces Over Type Aliases

For object shapes, interfaces are more extensible:

app.ts

class="keyword">class="comment">// Good

class="keyword">interface User {

id: string;

name: string;

email: string;

}

class="keyword">class="comment">// For unions and primitives, use class="keyword">type

class="keyword">type Status = class="keyword">class="string">"active" | class="keyword">class="string">"inactive" | class="keyword">class="string">"pending";

3. Use Unknown Instead of Any

Unknown is type-safe and forces you to check types:

app.ts

class="keyword">class="comment">// Avoid

class="keyword">class="keyword">function process(data: any) {

class="keyword">class="keyword">return data.value;

}

class="keyword">class="comment">// Better

class="keyword">class="keyword">function process(data: unknown) {

class="keyword">class="keyword">if (class="keyword">typeof data === class="keyword">class="string">"object" && data !== class="keyword">null && class="keyword">class="string">"value" in data) {

class="keyword">class="keyword">return (data as { value: string }).value;

}

}

4. Leverage Type Guards

Create custom type guards for complex checks:

app.ts

class="keyword">interface APIResponse {

success: boolean;

data?: unknown;

error?: string;

}

class="keyword">class="keyword">function isSuccessResponse(

response: APIResponse

): response is APIResponse & { data: unknown } {

class="keyword">class="keyword">return response.success && response.data !== class="keyword">undefined;

}

5. Use Const Assertions

For immutable values and literal types:

app.ts

class="keyword">class="keyword">const routes = {

home: class="keyword">class="string">"/",

about: class="keyword">class="string">"/about",

contact: class="keyword">class="string">"/contact",

} as class="keyword">class="keyword">const;

class="keyword">type Route = (class="keyword">typeof routes)[keyof class="keyword">typeof routes];

Advanced Patterns

Generic Constraints

Make generics more specific:

app.ts

class="keyword">class="keyword">function getPropertyclass="keyword">extends keyof T>(obj: T, key: K): T[K] {

class="keyword">class="keyword">return obj[key];

}

Utility Types

Leverage built-in utility types:

app.ts

class="keyword">type PartialUser = Partial;

class="keyword">type ReadonlyUser = Readonly;

class="keyword">type UserWithoutId = Omitclass="keyword">class="string">"id">;

class="keyword">type UserPickedFields = Pickclass="keyword">class="string">"name" | class="keyword">class="string">"email">;

Discriminated Unions

For type-safe state management:

app.ts

class="keyword">type LoadingState = { status: class="keyword">class="string">"loading" };

class="keyword">type SuccessState = { status: class="keyword">class="string">"success"; data: string };

class="keyword">type ErrorState = { status: class="keyword">class="string">"error"; error: Error };

class="keyword">type State = LoadingState | SuccessState | ErrorState;

class="keyword">class="keyword">function handleState(state: State) {

class="keyword">switch (state.status) {

class="keyword">case class="keyword">class="string">"loading":

class="keyword">class="comment">// TypeScript knows no data/error here

class="keyword">break;

class="keyword">case class="keyword">class="string">"success":

class="keyword">class="comment">// TypeScript knows data exists

console.log(state.data);

class="keyword">break;

class="keyword">case class="keyword">class="string">"error":

class="keyword">class="comment">// TypeScript knows error exists

console.error(state.error);

class="keyword">break;

}

}

Common Pitfalls to Avoid

- Don't use any - Use unknown or proper types

- Avoid type assertions - Use type guards instead

- Don't ignore errors - Configure strict mode

- Avoid deep nesting - Extract complex types

- Don't over-engineer - Keep types simple and readable

Tools and Resources

- TSConfig Cheat Sheet - Understand compiler options

- TypeScript Playground - Test and share code snippets

- ESLint TypeScript Plugin - Catch additional issues

- ts-reset - Improve built-in type definitions

Conclusion

TypeScript is a powerful tool that improves code quality and developer experience. By following these best practices, you'll write more maintainable and reliable applications.

Remember: the goal is not to fight TypeScript, but to let it help you write better code.

Happy typing! 💪