TypeScript Beyond Basics
TypeScript enhances JavaScript by adding powerful type features, enabling developers to write safer, more predictable, and maintainable code using arrow functions. By enforcing type checks at compile time, it helps catch errors early, improves code clarity, and makes large-scale applications easier to manage.
1. Advanced Types and Generics
TypeScript allows you to define types dynamically and flexibly, making your code more readable and less error-prone.
Example: Generics in a function
const wrapInArray = <T>(value: T): T[] => [value];
const numbers = wrapInArray(42); // number[]
const strings = wrapInArray("hello"); // string[]
Advanced Types: Union, Intersection, Conditional
type Success<T> = { data: T; success: true };
type Failure = { error: string; success: false };
type Result<T> = Success<T> | Failure;
const handleResult = <T>(result: Result<T>) => {
if (result.success) {
console.log(result.data);
} else {
console.error(result.error);
}
};
Difference between any and unknown
any: disables type checking, can be assigned to/from any type, unsafe.unknown: safer alternative, requires type checking before using.
let valueAny: any = 10;
let valueUnknown: unknown = 10;
// OK
const num1: number = valueAny;
// Error, must check type first
if (typeof valueUnknown === 'number') {
const num2: number = valueUnknown;
}
Creating constants in TypeScript
const API_URL: string = 'https://api.example.com';
const MAX_RETRIES: number = 5;
- Use
constfor values that should not change. - Optionally, type annotations improve clarity.
2. Type-Safe APIs and Backend Integration
One of the strongest points of TypeScript is type safety across your API calls. You can ensure that your frontend and backend always agree on the data structure.
Example: Fetching typed API data
interface User { id: number; name: string; email: string; }
const fetchUsers = async (): Promise<User[]> => {
const response = await fetch('/api/users');
const data = await response.json();
return data as User[];
};
fetchUsers().then(users => console.log(users));
Benefits:
- Avoid runtime type errors
- Auto-completion in editors
- Easier refactoring
3. Practical Design Patterns in TypeScript
TypeScript works well with classic design patterns. Using interfaces, generics, and advanced types allows you to implement these patterns safely.
Singleton Example:
const Logger = (() => {
let instance: { log: (msg: string) => void } | null = null;
const createInstance = () => ({ log: (message: string) => console.log(message) });
return {
getInstance: () => {
if (!instance) instance = createInstance();
return instance;
}
};
})();
const logger = Logger.getInstance();
logger.log("App started");
Observer Pattern Example:
interface Observer { update: (data: any) => void; }
const Subject = () => {
const observers: Observer[] = [];
const subscribe = (observer: Observer) => observers.push(observer);
const notify = (data: any) => observers.forEach(o => o.update(data));
return { subscribe, notify };
};
Why it matters:
- Enforces structure in large applications
- Reduces bugs and duplication
- Combines the power of TypeScript types with proven design patterns
4. Utility Types and Practical Usage
TypeScript comes with several utility types that help you write more concise and safe code.
Partial: Makes all properties of a type optional.
interface User { id: number; name: string; email: string; }
const updateUser = (user: Partial<User>) => { /* ... */ };
Required: Makes all properties required.
interface Config { apiUrl?: string; retries?: number; }
const initConfig = (config: Required<Config>) => { /* ... */ };
Record: Creates an object type with specific keys and value types.
const userRoles: Record<string, string> = {
admin: 'all-access',
guest: 'read-only'
};
Using these utility types improves readability, type safety, and reduces boilerplate in your code.
5. Integrating TypeScript with Frontend Frameworks
Using TypeScript with frameworks like React or Vue ensures type safety across components and props.
React Example:
interface ButtonProps { label: string; onClick: () => void; }
const Button = ({ label, onClick }: ButtonProps) => (
<button onClick={onClick}>{label}</button>
);
Vue 3 Example (Composition API):
import { defineComponent, ref } from 'vue';
const Counter = defineComponent({
setup: () => {
const count = ref<number>(0);
const increment = () => count.value++;
return { count, increment };
}
});
Benefits:
- Catch errors at compile time
- Better auto-completion and developer experience
- Easier refactoring and maintenance
Conclusion
Going beyond basic TypeScript allows you to write robust, maintainable, and scalable code. Leveraging advanced types, generics, type-safe APIs, constants, utility types, design patterns, and integration with frontend frameworks using arrow functions exclusively improves both developer productivity and code quality.
Bartłomiej Nowak
Programmer
Programmer focused on performance, simplicity, and good architecture. I enjoy working with modern JavaScript, TypeScript, and backend logic — building tools that scale and make sense.