TypeScript with React, Vue, and Angular
/ 6 min read
TypeScript with React
1. Component Types
// Function Componentinterface Props { name: string; age?: number;}
const UserProfile: React.FC<Props> = ({ name, age = 0 }) => { return ( <div> <h1>{name}</h1> <p>Age: {age}</p> </div> );};
// Class Componentinterface State { count: number;}
class Counter extends React.Component<Props, State> { state: State = { count: 0 };
increment = () => { this.setState(prev => ({ count: prev.count + 1 })); };
render() { return ( <button onClick={this.increment}> Count: {this.state.count} </button> ); }}2. Hooks with TypeScript
// useStateconst [count, setCount] = useState<number>(0);
// useRefconst inputRef = useRef<HTMLInputElement>(null);
// useEffectuseEffect(() => { const handler = (event: MouseEvent) => { console.log(event.clientX, event.clientY); };
window.addEventListener('mousemove', handler); return () => window.removeEventListener('mousemove', handler);}, []);
// Custom Hookfunction useLocalStorage<T>(key: string, initialValue: T) { const [storedValue, setStoredValue] = useState<T>(() => { try { const item = window.localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; } catch (error) { return initialValue; } });
const setValue = (value: T | ((val: T) => T)) => { try { const valueToStore = value instanceof Function ? value(storedValue) : value; setStoredValue(valueToStore); window.localStorage.setItem(key, JSON.stringify(valueToStore)); } catch (error) { console.log(error); } };
return [storedValue, setValue] as const;}3. Event Handling
// Event Typesconst handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { console.log(event.currentTarget.value);};
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => { console.log(event.target.value);};
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault(); // Handle form submission};
// Component with Eventsconst Form: React.FC = () => { return ( <form onSubmit={handleSubmit}> <input type="text" onChange={handleChange} /> <button onClick={handleClick}>Submit</button> </form> );};TypeScript with Vue
1. Vue 3 Component with TypeScript
// Using Composition APIimport { defineComponent, ref, computed } from 'vue';
interface User { id: number; name: string;}
export default defineComponent({ name: 'UserList', props: { title: { type: String, required: true } }, setup(props) { const users = ref<User[]>([]); const selectedUser = ref<User | null>(null);
const userCount = computed(() => users.value.length);
const addUser = (user: User) => { users.value.push(user); };
return { users, selectedUser, userCount, addUser }; }});2. Vue Class Components
// Using vue-class-componentimport { Vue, Component, Prop } from 'vue-property-decorator';
interface User { id: number; name: string;}
@Componentexport default class UserList extends Vue { @Prop({ required: true }) readonly title!: string;
users: User[] = []; selectedUser: User | null = null;
get userCount(): number { return this.users.length; }
addUser(user: User): void { this.users.push(user); }}3. Vue 3 Composables
// Custom composableimport { ref, onMounted, onUnmounted } from 'vue';
export function useMousePosition() { const x = ref(0); const y = ref(0);
function update(event: MouseEvent) { x.value = event.pageX; y.value = event.pageY; }
onMounted(() => { window.addEventListener('mousemove', update); });
onUnmounted(() => { window.removeEventListener('mousemove', update); });
return { x, y };}
// Usage in componentimport { defineComponent } from 'vue';import { useMousePosition } from './composables';
export default defineComponent({ setup() { const { x, y } = useMousePosition(); return { x, y }; }});TypeScript with Angular
1. Component Definition
// Component with TypeScriptimport { Component, Input, Output, EventEmitter } from '@angular/core';
interface User { id: number; name: string;}
@Component({ selector: 'app-user-list', template: ` <div *ngFor="let user of users"> {{ user.name }} <button (click)="selectUser(user)">Select</button> </div> `})export class UserListComponent { @Input() users: User[] = []; @Output() userSelected = new EventEmitter<User>();
selectUser(user: User): void { this.userSelected.emit(user); }}2. Services
// Service with TypeScriptimport { Injectable } from '@angular/core';import { HttpClient } from '@angular/common/http';import { Observable } from 'rxjs';import { map } from 'rxjs/operators';
interface User { id: number; name: string;}
@Injectable({ providedIn: 'root'})export class UserService { private apiUrl = 'api/users';
constructor(private http: HttpClient) {}
getUsers(): Observable<User[]> { return this.http.get<User[]>(this.apiUrl); }
getUserById(id: number): Observable<User> { return this.http.get<User>(`${this.apiUrl}/${id}`); }
createUser(user: Omit<User, 'id'>): Observable<User> { return this.http.post<User>(this.apiUrl, user); }}3. Guards and Interceptors
// Route Guardimport { Injectable } from '@angular/core';import { CanActivate, Router } from '@angular/router';import { AuthService } from './auth.service';
@Injectable({ providedIn: 'root'})export class AuthGuard implements CanActivate { constructor( private authService: AuthService, private router: Router ) {}
canActivate(): boolean { if (this.authService.isAuthenticated()) { return true; }
this.router.navigate(['/login']); return false; }}
// HTTP Interceptorimport { Injectable } from '@angular/core';import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent} from '@angular/common/http';import { Observable } from 'rxjs';
@Injectable()export class AuthInterceptor implements HttpInterceptor { intercept( request: HttpRequest<any>, next: HttpHandler ): Observable<HttpEvent<any>> { const token = localStorage.getItem('token');
if (token) { request = request.clone({ setHeaders: { Authorization: `Bearer ${token}` } }); }
return next.handle(request); }}Common Patterns Across Frameworks
1. Type Definitions
// Shared interfacesinterface User { id: number; name: string; email: string;}
interface ApiResponse<T> { data: T; status: number; message: string;}
// Type guardsfunction isUser(obj: any): obj is User { return 'id' in obj && 'name' in obj && 'email' in obj;}2. API Services
// Generic API serviceclass ApiService<T> { constructor(private baseUrl: string) {}
async get(id: number): Promise<T> { const response = await fetch(`${this.baseUrl}/${id}`); return response.json(); }
async create(data: Omit<T, 'id'>): Promise<T> { const response = await fetch(this.baseUrl, { method: 'POST', body: JSON.stringify(data) }); return response.json(); }}3. Form Handling
// Form interfacesinterface LoginForm { email: string; password: string;}
interface RegisterForm extends LoginForm { name: string; confirmPassword: string;}
// Form validationfunction validateForm<T>(form: T, rules: ValidationRules<T>): ValidationErrors { // Implementation}Best Practices
1. Type Safety
// Use strict type checking{ "compilerOptions": { "strict": true, "noImplicitAny": true, "strictNullChecks": true }}
// Use type inference when possible// Badconst numbers: number[] = [1, 2, 3];
// Goodconst numbers = [1, 2, 3];2. Component Props
// Use interfaces for propsinterface ButtonProps { label: string; onClick: () => void; disabled?: boolean; variant?: 'primary' | 'secondary';}
// Use required vs optional props appropriatelyinterface TableProps<T> { data: T[]; // Required columns: Column[]; // Required onRowClick?: (row: T) => void; // Optional}3. State Management
// Type-safe state managementinterface State { user: User | null; loading: boolean; error: string | null;}
// Actionstype Action = | { type: 'SET_USER'; payload: User } | { type: 'SET_LOADING'; payload: boolean } | { type: 'SET_ERROR'; payload: string };Testing with TypeScript
1. Unit Testing
// Jest with TypeScriptdescribe('UserService', () => { it('should fetch user by id', async () => { const user: User = { id: 1, name: 'John' }; const service = new UserService();
jest.spyOn(service, 'getUser').mockResolvedValue(user);
const result = await service.getUser(1); expect(result).toEqual(user); });});2. Component Testing
// React Testing Libraryimport { render, fireEvent } from '@testing-library/react';
test('button click handler', () => { const handleClick = jest.fn(); const { getByText } = render( <Button onClick={handleClick}>Click me</Button> );
fireEvent.click(getByText('Click me')); expect(handleClick).toHaveBeenCalled();});Conclusion
TypeScript provides excellent support for modern frontend frameworks, enabling type-safe development and better developer experience. Each framework has its own TypeScript integration patterns, but the core principles remain consistent.
Series Navigation
- Previous: TypeScript Modules and Namespaces
- Next: Testing in TypeScript