Web Animations: Framer Motion and CSS for React Developers
A practical guide to animations in React — CSS transitions and keyframes, Framer Motion for complex interactions, performance optimization, and accessibility considerations.
A practical introduction to TypeScript in frontend projects — type annotations, interfaces, generics, and the compiler settings that make React and API code safer without slowing you down.
By codefunc
JavaScript runs in browsers. TypeScript runs in your editor — catching typos, wrong argument types, and missing properties before you refresh the page. The tradeoff is explicit: you write more annotations, and the compiler tells you when your code contradicts itself. For frontend projects with API responses, component props, and state management, that tradeoff pays off fast.
Bottom line: TypeScript adds a type layer on top of JavaScript. You define shapes — interfaces, types, enums — and the compiler checks your code against them. Start with strict mode on new projects. Add types to existing JavaScript incrementally. Let the tooling catch bugs; let you focus on features.
| Benefit | How it helps |
|---|---|
| Autocomplete | Editor knows every property on your objects |
| Refactoring | Rename a field, find every usage instantly |
| API contracts | Response types catch backend changes early |
| Prop validation | Component props documented and enforced |
| Team onboarding | Types are documentation that never drifts |
| Fewer runtime errors | Catch undefined access before production |
TypeScript does not change runtime behavior. It compiles to JavaScript. The types disappear — but the bugs they prevented stay gone.
npm create vite@latest my-app -- --template react-ts
# or
npx create-next-app@latest --typescript
Modern scaffolds default to TypeScript.
npm install -D typescript @types/react @types/react-dom
npx tsc --init
Rename files from .js to .ts (or .jsx to .tsx for React). Fix errors incrementally.
{
"compilerOptions": {
"target": "ES2022",
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"skipLibCheck": true,
"jsx": "react-jsx"
},
"include": ["src"]
}
Start strict. Relaxing later is easy; tightening an existing codebase is painful.
const name: string = "JSON Formatter";
const count: number = 72;
const isActive: boolean = true;
const nothing: null = null;
const missing: undefined = undefined;
TypeScript infers types from values — explicit annotations are often unnecessary:
const name = "JSON Formatter"; // inferred as string
const tools: string[] = ["json-formatter", "jwt-decoder"];
const counts: Array<number> = [1, 2, 3]; // equivalent
const tool: { name: string; slug: string } = {
name: "JSON Formatter",
slug: "json-formatter",
};
For reusable shapes, use interfaces or type aliases.
Both define object shapes. The difference is subtle:
// Interface — extendable, mergeable
interface Tool {
name: string;
slug: string;
}
interface Tool {
category: string; // declaration merging: adds to existing Tool
}
// Type alias — more flexible, not mergeable
type ToolType = {
name: string;
slug: string;
};
type Category = "formatters" | "encoders" | "converters";
| Use | Recommendation |
|---|---|
| Object shapes | Interface (convention in React ecosystem) |
| Union types | Type alias (type Status = "idle" | "loading" | "error") |
| Function types | Either works; type alias often cleaner |
| Extending shapes | Interface with extends |
| Library definitions | Interface (allows consumer extension) |
In practice, pick one convention per project and stay consistent.
function formatJson(input: string, indent: number = 2): string {
return JSON.stringify(JSON.parse(input), null, indent);
}
// Arrow function
const formatJson = (input: string, indent = 2): string => {
return JSON.stringify(JSON.parse(input), null, indent);
};
type Formatter = (input: string) => string;
const uppercase: Formatter = (input) => input.toUpperCase();
function greet(name: string, greeting?: string): string {
return `${greeting ?? "Hello"}, ${name}`;
}
greet("Ada"); // "Hello, Ada"
greet("Ada", "Hi"); // "Hi, Ada"
A value that can be one of several types:
type Result =
| { ok: true; data: string }
| { ok: false; error: string };
function handleResult(result: Result) {
if (result.ok) {
console.log(result.data); // TypeScript knows data exists
} else {
console.error(result.error); // TypeScript knows error exists
}
}
The ok property acts as a discriminant — TypeScript narrows the type based on its value.
type ApiResponse<T> =
| { status: "success"; data: T }
| { status: "error"; message: string }
| { status: "loading" };
function render(response: ApiResponse<User>) {
switch (response.status) {
case "success":
return <UserCard user={response.data} />;
case "error":
return <Error message={response.message} />;
case "loading":
return <Spinner />;
}
}
Generics let you write reusable code that works with multiple types:
function first<T>(array: T[]): T | undefined {
return array[0];
}
const tool = first(["json-formatter", "jwt-decoder"]); // string | undefined
const count = first([1, 2, 3]); // number | undefined
interface ApiResponse<T> {
data: T;
timestamp: number;
}
type ToolResponse = ApiResponse<Tool>;
type UserResponse = ApiResponse<User>;
interface HasId {
id: string;
}
function findById<T extends HasId>(items: T[], id: string): T | undefined {
return items.find((item) => item.id === id);
}
interface ButtonProps {
label: string;
onClick: () => void;
variant?: "primary" | "secondary";
disabled?: boolean;
}
function Button({ label, onClick, variant = "primary", disabled }: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
className={variant}
>
{label}
</button>
);
}
interface CardProps {
title: string;
children: React.ReactNode;
}
function Card({ title, children }: CardProps) {
return (
<div className="card">
<h2>{title}</h2>
{children}
</div>
);
}
function Form() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// ...
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
return (
<form onSubmit={handleSubmit}>
<input onChange={handleChange} />
</form>
);
}
// Inferred from initial value
const [count, setCount] = useState(0); // number
// Explicit type for complex state
const [user, setUser] = useState<User | null>(null);
// Union state
type Status = "idle" | "loading" | "success" | "error";
const [status, setStatus] = useState<Status>("idle");
// DOM element ref
const inputRef = useRef<HTMLInputElement>(null);
// Mutable value ref
const countRef = useRef<number>(0);
interface User {
id: string;
email: string;
name: string;
createdAt: string;
}
interface ApiError {
code: string;
message: string;
}
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error("Failed to fetch user");
}
return response.json();
}
When integrating a new API, paste a sample response into the JSON to TypeScript converter to bootstrap your interface. Review and refine — inferred types from one response may miss optional fields or union possibilities.
Validate API response samples with the JSON Validator before generating types.
TypeScript types disappear at runtime. For API responses, consider runtime validation:
import { z } from "zod";
const UserSchema = z.object({
id: z.string(),
email: z.string().email(),
name: z.string(),
createdAt: z.string().datetime(),
});
type User = z.infer<typeof UserSchema>;
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
return UserSchema.parse(data); // throws if invalid
}
TypeScript includes built-in types for common transformations:
| Utility | Effect |
|---|---|
Partial<T> |
All properties optional |
Required<T> |
All properties required |
Readonly<T> |
All properties readonly |
Pick<T, K> |
Subset of properties |
Omit<T, K> |
Exclude properties |
Record<K, V> |
Object with keys K and values V |
interface Tool {
name: string;
slug: string;
description: string;
category: string;
}
// For update forms — all fields optional
type ToolUpdate = Partial<Tool>;
// Just the identifiers
type ToolRef = Pick<Tool, "name" | "slug">;
// Everything except category
type ToolWithoutCategory = Omit<Tool, "category">;
// Lookup table
type ToolsBySlug = Record<string, Tool>;
type Status = "idle" | "loading" | "success" | "error";
function assertNever(x: never): never {
throw new Error(`Unexpected value: ${x}`);
}
function getStatusMessage(status: Status): string {
switch (status) {
case "idle": return "Ready";
case "loading": return "Loading...";
case "success": return "Done!";
case "error": return "Failed";
default: return assertNever(status); // compile error if case missing
}
}
interface Dog {
kind: "dog";
bark: () => void;
}
interface Cat {
kind: "cat";
meow: () => void;
}
type Pet = Dog | Cat;
function isDog(pet: Pet): pet is Dog {
return pet.kind === "dog";
}
function handlePet(pet: Pet) {
if (isDog(pet)) {
pet.bark(); // TypeScript knows it's a Dog
} else {
pet.meow(); // TypeScript knows it's a Cat
}
}
const CATEGORIES = ["formatters", "encoders", "converters"] as const;
type Category = typeof CATEGORIES[number]; // "formatters" | "encoders" | "converters"
Enable these in tsconfig.json for maximum safety:
| Flag | Effect |
|---|---|
strict |
Enables all strict checks |
noUncheckedIndexedAccess |
Array/object index access returns T | undefined |
exactOptionalPropertyTypes |
Distinguishes missing from undefined |
noImplicitAny |
Errors on implicit any |
strictNullChecks |
null and undefined are distinct types |
const tools = ["json-formatter", "jwt-decoder"];
// Without flag: string
// With flag: string | undefined
const first = tools[0];
// Forces you to handle undefined
if (first) {
console.log(first.toUpperCase());
}
.js → .ts, .jsx → .tsxstrict: false — fix errors incrementallyunknown over any — forces type narrowingWhen fighting the compiler, you have options:
// Type assertion (you know better)
const element = document.getElementById("root") as HTMLDivElement;
// Non-null assertion (you're sure it exists)
const element = document.getElementById("root")!;
// any (last resort)
const data: any = unknownThirdPartyLib.getData();
Use sparingly. Each escape hatch is a place where TypeScript can't help you.
TypeScript catches errors at compile time that JavaScript catches at runtime (or doesn't catch at all). Start new projects with strict mode. Define interfaces for component props and API responses. Use union types for state machines. Generate initial types from JSON samples, then refine. The investment in type annotations pays back in autocomplete, refactoring confidence, and bugs caught before deployment.
A practical guide to animations in React — CSS transitions and keyframes, Framer Motion for complex interactions, performance optimization, and accessibility considerations.
A practical guide to authentication for frontend developers — understanding OAuth flows, JWT structure, session cookies, and implementing secure auth in React applications.
A practical guide to using GraphQL in frontend applications — writing queries and mutations, client-side caching with Apollo and urql, and patterns for React integration.
Find a developer tool