My Honest Take on the TypeScript forEach Loop

You know what? I use TypeScript every day. And I reach for forEach a lot. It’s fast to write. It reads clean. But it’s not perfect. So here’s my plain, real review—what I love, what trips me up, and what I use instead when it fights me. I dug even deeper in my honest take on the TypeScript forEach loop if you want the blow-by-blow version.

What even is forEach?

forEach runs a function for every item in a list. In TypeScript, it keeps types tidy. It’s simple:
In case you need a quick refresher, here’s a succinct glossary entry for forEach that covers the signature.

  • You can read the item.
  • You can also read the index.
  • You cannot stop early.

That last bit matters. A lot. For a deeper exploration of iteration techniques in TypeScript, check out this practical overview on Improving Code.

Real code from my week

1) Clean and collect numbers from strings

I had a list coming from a form. Some values had spaces. Some had junk. I just needed valid numbers.

const rawValues: string[] = [" 42 ", "hi", "100", " 7x", "0"];
const clean: number[] = [];

rawValues.forEach((val) => {
  const num = Number(val.trim());
  if (!Number.isNaN(num)) {
    clean.push(num);
  }
});

console.log(clean); // [42, 100, 0]

Simple. Clear. A tiny bit boring, which I like. That same pattern shows up when I’m slurping CSV rows into a database—something I covered in this Lambda + DynamoDB CSV import write-up.

2) Build a quick lookup map

I often build maps for fast reads. This one groups users by role.

type User = { id: string; name: string; role: "admin" | "staff" | "guest" };

const users: User[] = [
  { id: "1", name: "Ana", role: "admin" },
  { id: "2", name: "Bo", role: "staff" },
  { id: "3", name: "Cam", role: "staff" },
];

const byRole = new Map<User["role"], User[]>();

users.forEach((u) => {
  if (!byRole.has(u.role)) byRole.set(u.role, []);
  byRole.get(u.role)!.push(u);
});

// Quick check
console.log(byRole.get("staff")?.length); // 2

TypeScript keeps the role keys safe. No mystery strings sneaking in. As an aside, when you refactor a model and need to rename a field without losing its JSDoc, I’ve found a few tricks that help—documented in my hands-on guide to safely renaming a TypeScript field.

3) forEach on Map and Set

Yep, forEach works on other built-ins too.

const scores = new Map<string, number>([
  ["Ana", 9],
  ["Bo", 7],
  ["Cam", 10],
]);

scores.forEach((value, key) => {
  console.log(`${key}: ${value}`);
});

const seen = new Set<string>();
["a", "b", "a"].forEach((v) => seen.add(v));
console.log(seen.has("a")); // true

Clear as day.

4) The async trap (I stepped in it, again)

I thought this would wait. It didn’t.

const ids = [1, 2, 3];

async function fetchUser(id: number) {
  // pretend to fetch
  return { id, name: `U${id}` };
}

const usersA: Array<{ id: number; name: string }> = [];

// This looks fine... but it's not
ids.forEach(async (id) => {
  const u = await fetchUser(id);
  usersA.push(u);
});

console.log(usersA); // Often []

forEach doesn’t wait for async work. It runs and moves on. My fix? Use for...of when I need await.

const usersB: Array<{ id: number; name: string }> = [];

for (const id of ids) {
  const u = await fetchUser(id);
  usersB.push(u);
}

console.log(usersB.length); // 3

When I’m racing all calls, I use Promise.all:

const usersC = await Promise.all(ids.map((id) => fetchUser(id)));

Handling asynchronous file processing is also where a solid file-path argument pattern in TypeScript can save you from a ton of boilerplate.

When it shines

  • I want clean, readable loops.
  • I don’t need to stop early.
  • I’m pushing to a result list or map.
  • I want types to guide me while I code.

Where it bites

  • Can’t break early. No break. No return to exit the loop.
  • Async code doesn’t “wait.”
  • Easy to hide side effects. You mutate stuff in place, sometimes by accident.
  • Folks mix it up with for...in. That one loops over keys, not items — a mistake the linter’s no-for-in-array rule loves to catch.

For a language-level comparison—why these pain points feel milder to me than similar ones in Python—check out my real-life take comparing TypeScript vs. Python.

A quick compare in plain talk

  • forEach: clean and simple; no early exit; beware async.
  • for...of: great for await, and you can break.
  • for...in: keys on objects; not for arrays in most cases.
  • .map: returns a new array. Don’t use it for side effects.
  • .filter, .some, .every: sometimes better than a manual loop.

Here’s how I break early without forEach:

const nums = [1, 3, 5, 6, 7];

// Need the first even? Use for...of
let firstEven: number | undefined;
for (const n of nums) {
  if (n % 2 === 0) {
    firstEven = n;
    break;
  }
}

Or use .find:

const firstEven2 = nums.find((n) => n % 2 === 0);

TypeScript types feel nice here

Type hints inside forEach help me avoid silly bugs.

type Order = { id: string; total: number | null };

const orders: Order[] = [
  { id: "a1", total: 12 },
  { id: "b2", total: null },
];

let sum = 0;

orders.forEach((o) => {
  if (o.total !== null) {
    sum += o.total; // TS knows total is number here
  }
});

console.log(sum); // 12

That little null check saves me from crashes. I like that a lot. Keeping those little null-checks clear is exactly why I sprinkle thoughtful inline notes; see my real-world approach to comments in TypeScript for some concrete patterns.

Small UI note (a quick detour)

I used this with DOM nodes, too. Works fine in TS when the types are set.

const buttons: NodeListOf<HTMLButtonElement> =
  document.querySelectorAll("button.save");

buttons.forEach((btn, i) => {
  btn.textContent = `Save ${i + 1}`;
});

Back to the main thing—forEach reads like a to-do list. That’s why I keep using it.

Tiny cheat sheet

// Basic
arr.forEach((item, index, array) => { /* work */ });

// Map
map.forEach((value, key) => { /* work */ });

// Set
set.forEach((value) => { /* work */ });

// Need to break? Use for...of
for (const item of arr) { if (stop) break; }

// Need async? Use for...of or Promise.all
for (const id of ids) { const x = await fetch(id); }
const all = await Promise.all(ids.map(fetch));

If you enjoy patterns that make call sites obvious, you might also like my experiment with named constructor arguments in TypeScript.

While we’re talking about things that are delightfully free—forEach comes right

I ran nodejs-poolController on my pool. Here’s what actually worked.

I’m Kayla. I have a Pentair EasyTouch 8, an IntelliFlo VSF pump, and an IntelliChlor IC40. I used to shuffle out to the pad, press tiny buttons, and guess what speed the pump needed. Fun? Not really. So I set up nodejs-poolController on a Raspberry Pi 4 and lived with it for a few months.

If you want the play-by-play of that first weekend—including every wiring photo and log snippet—I laid it all out here: the full “I ran nodejs-poolController on my pool” diary on ImprovingCode.

You know what? It felt like getting my time back.

My gear and why I tried it

  • Raspberry Pi 4 (2 GB), running Raspberry Pi OS Lite
  • FTDI-based USB-to-RS485 adapter (the Waveshare one)
  • Two wires from the Pi to the EasyTouch COM port (A/B) — yes, the little green block inside the panel

My goals were simple: control the pump, spa, lights, and heater from my phone or Home Assistant. And stop walking outside in slippers at night. That was the dream.

Setup: pretty smooth, but the wires matter

I won’t sugarcoat the wiring. The RS-485 A/B lines can be flipped. If you get no data, swap A and B. I had to. Once I got a heartbeat, I smiled like a kid. A quick refresher on differential signaling that I skimmed over at ImprovingCode saved me from spending an evening swapping conductors at random.

For anyone wanting the official wiring diagrams and programming details straight from the manufacturer, the Pentair EasyTouch Control System User’s Guide is an invaluable companion.

On the Pi, I installed Node.js and ran the app. I used the project’s quick-start steps and let it scan the bus. It found my EasyTouch, the pump, and the chlorinator. First try? Nope. Second try? Yes.

If you’re still piecing things together after the scan, the project’s own GitHub Wiki for nodejs-poolController walks through installation, MQTT setup, and more advanced tweaks.

Here’s the tiny config bit that mattered most on my box:

{
  "serial": {
    "port": "/dev/ttyUSB0",
    "baud": 9600
  },
  "web": {
    "port": 4200
  },
  "logLevel": "info"
}

I set the serial port to the RS-485 adapter and left the speed alone. The web service came up on 4200. The dash panel ran on another port (mine was 5150). I pinned both on my phone.

First run: real control, right away

I tested three things right off:

  1. Turn the spa on
    I tapped Spa in the dash panel. The relay clicked. The heater icon woke up. Warm water, fast. No more “Did I leave it on?” panic.
    If you’re lounging poolside with your phone while the spa heats, and you’re in the mood for some flirty conversation, you can drop into free sexting chat rooms where anonymous users swap playful messages and keep boredom at bay until the jets are ready.
    For readers in South Orange County who’d rather line up an in-person encounter once the bubbles stop, the curated AdultLook San Juan Capistrano listings deliver vetted profiles, photos, and clear availability details so you can arrange a discreet meetup without wading through endless generic ads.

  2. Change the pump speed when the cleaner runs
    I set a rule so the cleaner circuit bumps the pump to 2800 RPM. When the cleaner shuts off, it drops to 1600 RPM. Quiet again. My dog appreciates that part.

  3. Light shows
    I have Pentair IntelliBrite lights. The app let me pick a color show. I used “Sunset” for a birthday dinner. It was cheesy and sweet. We loved it.

For kicks, I also hit the API from my laptop:

curl localhost:4200/state

And to toggle a circuit by id (my Cleaner was 5):

curl -X POST localhost:4200/circuit/5/toggle

Simple. It felt like the gear finally listened.

Daily life with it: fewer trips outside

Most days I use these buttons:

  • Pool heat mode: Heater vs. Solar
  • Spa set point: 101 on cold days, 99 on others
  • Pump: 1600 RPM for filter, 2200 when someone’s swimming, 2800 for the cleaner
  • Lights: white at night, colors on weekends

There’s a freeze protect mode too. I live in Texas, so that matters some winters. When temps drop, it spins the pump and saves me from a burst pipe mess. It’s not fancy. It’s just there.

Home Assistant: worked well after MQTT

I do run Home Assistant. In nodejs-poolController, I turned on MQTT and pointed it at my broker. After a restart, new entities popped up: pump speed, heater state, spa, lights, salt level, and more. I made two tiny automations:

  • When we turn the spa on, set pump to 3000 RPM, and lock it there till the spa turns off.
  • If it’s freezing and the pump isn’t running, start it at 2000 RPM. Belt and suspenders.

I like the MQTT part. It feels steady. Commands land. States update fast.

Stuff I liked (and a few things I didn’t)

Pros:

  • Real control over my Pentair gear without the pricey add-ons
  • Dash panel is clear and fast on a phone
  • Schedules, circuits, light shows, pump speeds — all in one place
  • API and MQTT make nerd things easy
  • It found my equipment on its own after I fixed wiring

Cons:

  • Docs live across a few places; not all bits are in one spot
  • First wire-up is a “is A actually A?” puzzle
  • I hit a weird conflict with a pump schedule I made in the panel vs. one I made in the app — I deleted the panel’s version and let the app own it
  • One chlorinator reading was off after a power blip; a restart cleared it

If you’re on the fence about whether keeping Node.js running long-term on a Pi is even safe, my candid take—complete with crash graphs and uptime screenshots—is here: “Is Node.js Safe?” after shipping real stuff with it.

Little wins and gotchas

  • Use an FTDI-based RS-485 adapter. Cheap ones can be noisy.
  • Label your circuits in the app. Names beat numbers when you’re half awake.
  • Back up your config file. I keep a copy on the Pi and my laptop. For the exact Node.js snippet I use to prune old backups (a quick fs.unlink helper), see my honest take on deleting files with Node.js—with real code.
  • If lights don’t respond, make sure the app knows the right light type. Mine needed “IntelliBrite,” not generic.
  • Keep only one place for schedules. The panel or the app. Not both.

Here’s one schedule I used that felt right for summer:

  • Filter: 7 am–11 am at 1600 RPM (quiet coffee time)
  • Cleaner: 11 am–1 pm at 2800 RPM
  • Filter again: 7 pm–10 pm at 1600 RPM
  • Lights: sunset to 11 pm, soft white

Simple, cheap to run, and the water stayed clear.

Reliability and updates

I’ve had it running for three months. The Pi reboots after power loss and the app comes back on its own. Logs make sense. Memory use stayed low. I updated once to pick up a bug fix. It took five minutes.

There was a stretch where I considered wiping Node.js and starting clean—version mismatch headaches were real. I documented every trial, error, and rollback in the saga of uninstalling Node.js three separate times. Spoiler: nuke-and-pave isn’t always the answer, but sometimes it helps.

I did have one day where the RS-485 bus got chatty and dropped a few reads. Turned out a loose ground at the panel. Tightened it. Rock solid again.

If you ever decide you’re done with Node.js on a machine entirely, I also chronicled the clean-remove approach that finally stuck: how I deleted Node.js on all my boxes without leaving crumbs.

Who should try it?

  • You have Pentair gear (EasyTouch, IntelliTouch, IntelliCenter, IntelliBrite, IntelliChlor).
  • You

I removed Node.js from my machines. Here’s how it actually went

I’m Kayla, and I code for work and for fun. I also break stuff. Then I fix it. Last winter, I had to remove Node.js from three places: my MacBook Pro, my Windows PC, and an Ubuntu box. I wanted a clean slate. New Node. Fresh tools. Fewer weird bugs.

Easy, right? Kind of. But not always.

My short review: removing Node.js is a 3.5 out of 5. It works, but crumbs get left behind. And those crumbs bite.

Let me explain with real examples.

For a detailed checklist and some extra troubleshooting tips, I leaned on this handy write-up over at ImprovingCode.

Why I even did this

  • I had Node 14 from an old project.
  • I needed 18 for new work.
  • My global packages fought each other. Yarn and pnpm were not happy.
  • My PATH was a mess. I saw “node: command not found” on one tab and a different Node on another. Yikes.

You know what? Clean beats chaos.

Just like stripping Node.js and every lingering global folder off my machines left them utterly bare, you can explore a more literal take on “going bare” in this gallery of nude pics—it offers high-resolution, tastefully shot images that can provide a quick creative reset before you jump back into debugging.

My MacBook Pro (Homebrew install): pretty smooth, but watch for leftovers

This Mac had Node from Homebrew. So I did the normal thing first: brew uninstall node.

It removed most of it. But a few bits stuck around.

What I found:

  • which node still pointed to a path for a while. I restarted my Terminal and it went away.
  • /usr/local/lib/node_modules had old global packages (like npm, yarn, and gulp). I deleted that folder.
  • ~/.npm held cache junk. I wiped it. Felt good.

Pro: fast and simple.
Con: those global folders can hang around and cause weird errors.
If you want a more structured walkthrough for macOS, the folks at MUO cover the full process in their guide.

Tiny tip: run hash -r in your shell, or just open a new Terminal window. Bash and zsh like to remember old paths.

For an end-to-end journal of doing almost the same Homebrew cleanup, I loved the walk-through in this companion post.

My other Mac (nvm install): the easiest one by far

This Mac used nvm. Removing Node was, honestly, lovely.

What I did:

  • nvm list to see versions.
  • nvm uninstall 18 and nvm uninstall 14 to yank them out.
  • nvm cache clear because I love a tidy house.

I also checked my shell files. I kept the nvm lines in ~/.zshrc, but you can remove them if you’re done with nvm.

Pro: one command per version. Clean.
Con: global packages tied to that version go poof. That’s normal, but still a surprise if you forget.

Side note: when I switched to nvm, I had fewer PATH issues. Worth it.

Windows 11 (official installer): not hard, but the PATH got spicy

On my Windows PC, I used the standard “Apps & features” route.

What I did:

  • Open Settings → Apps → Installed apps → Node.js → Uninstall.
  • Deleted C:Program Filesnodejs after the uninstall finished. It had a couple files left.
  • Cleaned up C:UsersMyUserAppDataRoamingnpm and C:UsersMyUserAppDataRoamingnpm-cache. That killed ghost npx and old CLIs.
  • Checked the PATH in System Environment Variables. Removed any extra Node paths.

Then I rebooted. I know… but on Windows, it helps.

Pro: the basic uninstall works.
Con: that roaming npm folder can trick you. I once typed npx and it ran, even after uninstall. Not cool.

Extra: when I used Chocolatey once, I ran choco uninstall nodejs and then did the same cleanup above. Same deal.

I’m clearly not the only one who had to redo the process—someone else actually went through three separate uninstalls and wrote about what worked and what didn’t.

Ubuntu server (apt install): fine, unless you built from source

On my Ubuntu box, I had Node from the NodeSource repo.

What I did:

  • sudo apt remove nodejs
  • sudo apt purge nodejs
  • sudo rm -rf /usr/local/lib/node_modules if I saw leftovers
  • which node and whereis node to confirm it was gone

There’s also a concise tutorial on using the Linux command line to remove Node, if you prefer an external checklist, over at GeeksforGeeks.

One time, I had compiled Node from source (past me was too bold). I had to remove /usr/local/bin/node and /usr/local/bin/npm by hand.

Pro: apt is clear and fast.
Con: source builds are a pain to clean. If you forgot how you installed Node, you’ll spend time hunting.

If you’re primarily on Linux and need a quick sanity check, there’s a concise checklist in this Node-deletion guide.

The weird stuff I ran into

  • Ghost npx: even after uninstall, npx still worked on Windows. That was from the roaming npm folder. Deleting it fixed the issue.
  • Yarn and pnpm: both kept old links in my PATH. Removing global folders and restarting the shell helped. On Mac, I removed ~/.pnpm-store and ~/.yarn.
  • VS Code terminals: the PATH inside VS Code sometimes lagged. Closing all VS Code windows and reopening fixed it.
  • Volta: on a test machine, I had Node with Volta. I ran volta uninstall node and later deleted ~/.volta when I was done with it. That was smooth.

What worked best for me (ranked)

  1. nvm: nvm uninstall <version> and done.
  2. Homebrew: brew uninstall node, then delete leftovers.
  3. Windows Apps & features: okay, but check PATH and roaming folders.
  4. apt: fine, unless you built from source.

Little checklist I now keep

  • Figure out how Node was installed first. nvm? brew? installer? apt? Volta?
  • Remove Node with that same tool.
  • Delete global folders:
    • Mac/Linux: ~/.npm, ~/.config/yarn, ~/.pnpm-store, /usr/local/lib/node_modules
    • Windows: %AppData%npm, %AppData%npm-cache
  • Restart the shell (or the whole machine on Windows).
  • Run node -v and npm -v. They should fail if removed.
  • If you’ll install again, consider nvm (Mac/Linux) or nvs/Volta (cross-platform). It keeps things sane.

Need a totally different kind of fresh start while your machine reboots? If you happen to be in East Texas, you can discover new, verified connections on the AdultLook Tyler listings—perfect for lining up an after-hours meet-up as quickly as you reinstall Node, with photos and reviews that make choosing simple.

Final take

Removing Node.js isn’t hard, but it’s not one click either. It leaves crumbs. If you know where they hide, you’re fine. If you don’t, you’ll chase ghosts for an hour. I’ve been there. Twice.

Would I do it again? Yes. But now I write down how I installed it in the first place. Then cleanup is fast. And I stick with a version manager, so next time, it’s one command, a deep breath, and I move on.

If you’re stuck, don’t stress. Check the install path, clean the global folders, and restart the shell. Simple beats clever here.

—Kayla Sox

React vs TypeScript: My Hands-On Take (From a Real Project Mess)

You know what? I used to think “React vs TypeScript” made sense as a face-off. Like two tools in a ring. But that’s not how it works. React builds what you see. TypeScript checks the code you write. They’re different. Still, I’ve run projects with plain React, and projects with React + TypeScript. The feel is night and day.

If you’d like the unabridged soap-opera version of that first week, I break it down blow-by-blow in React vs TypeScript: My Hands-On Take From a Real Project Mess.

So…what are they?

  • React: a UI library. It draws the screen, updates it, and gives you hooks and components. Buttons, lists, forms—the fun stuff.
  • TypeScript: a typed layer on top of JavaScript. It catches mistakes before your app runs. Think spell-check, but for code.

Simple. And yet, this pairing changed how my week felt.

The bug that made me switch

I built a small plant shop for a friend. We called it Leafy Lane. Cute name. React only. No TypeScript. I was moving fast. I had a cart page that added price and tax. It worked…until it didn’t.

Here’s the silly code I shipped:

const price = "199";
const tax = "10";
const total = price + tax; // "19910" … oops

I didn’t notice right away. A customer did. They messaged, “Why is my total 19910?” My stomach sank. It was a string bug. Basic, but costly.

Now look at the same thing with TypeScript:

const price: number = 199;
const tax: number = 10;
const total = price + tax; // 209, clean

If I try to mix a string and a number, TypeScript yells before I even run the app. I like that kind of yelling. It saves me from late-night panic.

Props that lie (and how TS called them out)

Another one. I had a Button that used a label and an onClick handler. With plain React, I did this:

function Button({ label, onClick }) {
  return <button onClick={() => onClick(label)}>{label}</button>;
}

// Later...
<Button label="Buy" onClick={(n) => console.log(n + 1)} /> // trouble

“Buy” is a string. The handler expects a number. JavaScript lets it slide. It fails at runtime.

My TypeScript rewrite:

type ButtonProps = {
  label: string;
  onClick: (label: string) => void;
};

function Button({ label, onClick }: ButtonProps) {
  return <button onClick={() => onClick(label)}>{label}</button>;
}

// Now this fails at build time:
<Button label="Buy" onClick={(n: number) => console.log(n + 1)} />;

The error shows up in my editor. I fix it in seconds. No mystery bug. No guessing.

Day 1 speed vs week 4 speed

Here’s the thing: React alone feels fast on day one. You code. You see a button. Done. But week four? You trip on small stuff. I did, again and again.

React + TypeScript feels slower on day one. You add types. You think a bit more. But week four? You’re calm. The editor helps you. The app feels solid. My commits got smaller. My fixes got simpler.

Autocomplete that feels like a teammate

When I built a dashboard with charts, I used React + TypeScript + Vite. My API returned “orders.” With plain JS, I’d forget field names. Was it “orderId” or “id”? I’d guess, run, fail, sigh.

With TypeScript, I wrote:

type Order = {
  id: string;
  total: number;
  createdAt: string;
};

function OrderRow({ order }: { order: Order }) {
  return <div>{order.total}</div>;
}

Now my editor knows “order.total” is a number. If I type “order.totla” by mistake, it flags it at once. Feels small. Saves hours.

And when I do need to rename a field, I lean on the pattern I walk through in Renaming a TypeScript field and keeping JSDoc.

Hooks I trust more with types

A few spots where TypeScript helped me breathe:

  • useState with clear shape:

    type User = { id: string; name: string } | null;
    const [user, setUser] = useState<User>(null);
    

    No “undefined” surprise later.

  • Event values in forms:

    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
      setAmount(Number(e.target.value));
    };
    

    No “value is maybe null” drama.

  • useRef for elements:

    const inputRef = useRef<HTMLInputElement | null>(null);
    

    I don’t poke at a ref like it’s always there. It might be null. TS makes me check.

When you step outside React and start building plain classes, giving your constructors named arguments can keep that same clarity—here’s my take on how to do that cleanly in TypeScript.

A small team story (and a coffee spill)

I had two interns on a React app. First week, we used plain React. I spent so much time in PRs saying, “This can be undefined,” or “This returns an object, not an array.” I got short. Coffee spilled. Not proud.

We switched to React + TypeScript the next sprint. Our PRs got shorter. The review chat turned into real design talk, not shape talk. The codebase felt teachable. New folks ramped faster. I didn’t change as a person. The guardrails helped me act better.

Where TypeScript fought me

It’s not all sunshine. TypeScript can be loud. Sometimes I just want to map over data and move on.

If the humble forEach is giving you grief, I give it a reality-check in my honest take on the TypeScript forEach loop.

My rule now:

  • Type the public edges: API calls, props, context, hooks.
  • Be light inside small functions. If it’s obvious, let the compiler infer it.
  • Avoid “any.” But if I must use it—wrap it and fix it later.

Even something as simple as a file-path string deserves a real type. I show the lightweight way to do that in this article on giving a TypeScript file-path argument some love.

This keeps the noise low. If you’re looking for a broader set of guardrails and suggestions, I recommend skimming through the insights in the Using TypeScript with React: Best Practices article—they align closely with what I landed on during this project.

For the moments when types aren't enough and you need a quick note for future-you, I share the comment styles that have actually aged well in Comments in TypeScript: My Real-World Take.

Tooling that didn’t hurt

What I used and liked:

  • Vite for React + TypeScript. Fast starts. Easy builds.
  • ESLint + TypeScript rules. Catches silly stuff as I type.
  • Zod or Yup to validate API data at runtime. Types help at build time; these help when the server lies.

None of this felt heavy once it was set.

If your next React + TypeScript side-project leans toward live video or chat features, it’s worth analyzing how established webcam platforms structure their flows. The in-depth Camster review breaks down the real-world UX patterns, latency tricks, and revenue hooks those sites rely on, giving you a solid checklist of ideas to borrow—or potholes to avoid.

Likewise, when your UI needs to support city-specific listings—think profiles, search filters, and clear calls to action—scanning a live directory can spark layout ideas; the Fairfield landing page on AdultLook showcases a straightforward grid and filtering sidebar that you can dissect for component patterns and user-flow cues.

If you’d like an even deeper dive into fine-tuning your React + TypeScript environment, I have a full walkthrough over on Improving Code. For a guided video-style setup, check out Setting Up: React and TypeScript | React with TypeScript | Steve Kinney.

When I’d choose each

If you’re asking “React vs TypeScript,” here’s how

I Tried Node.js Authentication So You Don’t Panic Later

I build apps for a living, but I still drink cold coffee over login bugs. You know what? Node.js auth can be smooth. It can also be messy. I’ve used a bunch of ways. Some easy, some fussy, some fast.

If you want the step-by-step story of every experiment and pitfall, I wrote up a full walkthrough of my Node.js authentication trials that dives even deeper into the nitty-gritty.

Here’s what worked for me, what broke, and what I use now.

The quick take

  • Need speed with Next.js? NextAuth.js felt easy and fast.
  • Need “works out of the box” with guard rails? Auth0 felt polished.
  • Need total control in Express? Rolling my own worked, but it took care.
  • Old-school and steady? Passport.js still does the job.

I’ll explain. And I’ll show tiny bits of code I actually used.

What I tried, and why

My apps weren’t all the same. One was a tiny tool for a local shop. One was a social app with Google login. Another needed two-factor. So I tried different stacks:

If you’re curious about commerce-specific lessons, check out my recap on building three different e-commerce stores with Node.js for a no-fluff look at carts, payments, and scale.

  • NextAuth.js in a Next.js 14 app (with the Prisma adapter)
  • Auth0 for a startup MVP (Google + email login)
  • Passport.js in an older Express app
  • Plain Express with cookies, JWT, bcrypt, and Redis
  • TOTP codes with Speakeasy, and WebAuthn with @simplewebauthn

Different jobs, different keys. That’s how it went.

Passport.js — the old friend that still shows up

Passport felt like a classic wrench. It’s not fancy. It’s steady. I used local strategy and Google OAuth. Setup was okay, but the boilerplate was a bit long. I had to wire sessions, serializing users, and stores.

What was nice:

  • Tons of strategies
  • Clear flow: login, serialize, session

What was annoying:

  • Lots of glue code
  • Docs felt spread out
  • TypeScript types needed a little nudging

Did it work? Yep. It ran for months with Redis as the session store. No drama.

NextAuth.js — easy street if you’re on Next

This was the fastest path for my Next.js app. I added GitHub and email login within an afternoon. The Prisma adapter made user records simple. I liked the callback hooks too. I could tweak tokens in one spot.

What I liked:

  • Quick setup
  • Good email sign-in experience
  • Adapters for common databases

What tripped me up:

  • Session vs JWT mode felt confusing at first
  • Edge runtime quirks with some providers
  • You do need to read the docs slowly

If you need a reference while you tinker, the official NextAuth.js documentation is a solid starting point.

Still, I’d use it again for any Next app. It saved me time, plain and simple.

Auth0 — polished, with a price tag

I used Auth0 for a client who needed to ship fast. The dashboard felt clean. Rules and Actions were clear. Social login worked on day one. Logs helped me fix silly mistakes, like a bad callback URL.

What felt great:

  • Clear docs and strong UI
  • Good error logs
  • Social login without tears

What I watched for:

  • Costs go up as users grow
  • Vendor lock-in is a real thing
  • Tuning JWT claims needed care

If you want smooth and have a budget, it’s solid. Their extensive Auth0 docs cover almost every corner case you’ll bump into.

The roll-your-own route — Express, JWT, and cookies

Sometimes I just want full control. I built a small system with Express, bcrypt, jsonwebtoken, and cookie sessions. I used Redis for blacklisting refresh tokens after logout. It took more time, but I understood every step.

Tiny signup flow I used:

// signup.js
import express from 'express';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';

const router = express.Router();
const users = new Map(); // demo only; I used Postgres in real code

router.post('/signup', async (req, res) => {
  const { email, password } = req.body || {};
  if (!email || !password) return res.status(400).json({ error: 'Missing fields' });

  const hash = await bcrypt.hash(password, 12);
  users.set(email, { email, hash, id: Date.now().toString() });

  res.status(201).json({ ok: true });
});

router.post('/login', async (req, res) => {
  const { email, password } = req.body || {};
  const user = users.get(email);
  if (!user) return res.status(401).json({ error: 'No user' });

  const match = await bcrypt.compare(password, user.hash);
  if (!match) return res.status(401).json({ error: 'Bad password' });

  const access = jwt.sign({ sub: user.id }, process.env.JWT_SECRET, { expiresIn: '15m' });
  const refresh = jwt.sign({ sub: user.id, type: 'refresh' }, process.env.JWT_SECRET, { expiresIn: '7d' });

  res.cookie('access', access, { httpOnly: true, sameSite: 'lax', secure: true });
  res.cookie('refresh', refresh, { httpOnly: true, sameSite: 'lax', secure: true });
  res.json({ ok: true });
});

export default router;

Notes from the trenches:

  • I kept tokens in httpOnly cookies, not localStorage. Safer against XSS.
  • I rotated refresh tokens after each use. Yes, more code. Worth it.
  • CSRF mattered with cookies. I used a CSRF token header on form posts.

Did it take longer? Yes. Did I sleep better? Also yes.

Two-factor and WebAuthn — more trust, fewer texts

For two-factor, Speakeasy made TOTP simple. I showed a QR code, saved the secret, and asked for a 6-digit code on login. Users got the rhythm fast.

For passkeys, @simplewebauthn felt clean. Setup was a bit careful, but login felt great. Touch the key. Done. It’s fast and kind of fun.

Mistakes I made so you don’t repeat them

Apps that carry intimate, sometimes adult, messaging need bulletproof auth. If you want a quick reality check, look at how communities handle privacy in the world of Kik sexting — the real chat scenarios there make it crystal clear why token leaks or cookie misconfigurations can be disastrous. For another angle, skim a location-based classifieds board such as AdultLook’s Apple Valley listings to notice how much users rely on discreet, friction-free authentication to protect identities and keep conversations behind a safe login.

For a deeper breakdown of best practices and up-to-date patterns, I recommend browsing the security posts over at Improving Code.

A good primer to start with is this candid look at whether Node.js is actually safe after shipping real production apps; it lays out common attack surfaces and how to plug them.

  • Cookie flags: I forgot SameSite and Secure once. Chrome blocked the cookie on cross-site. Fix: SameSite=Lax or None + Secure.
  • Token storage: I first used localStorage. Switched to httpOnly cookies after a scare.
  • CORS: I forgot credentials: true on both server and client. Requests failed in silence. Annoying hour lost.
  • Wrong time on server: Tokens “expired” early. My VM clock was off. I still roll my eyes at that one.
  • bcrypt cost too low: I bumped it up. Argon2 is nice too, but my host was happier with bcrypt.

What I use now, for real

  • Next.js project: NextAuth.js with Prisma. Add Google later if needed.
  • Plain Express: Cookies + JWT access/refresh, CSRF token, Redis for refresh rotation.
  • Bigger team or a rush: Auth0. It’s worth it when time matters.
  • Legacy stack: Passport.js, local + one social strategy.

Tiny checklist I keep taped near my screen

  • Hash passwords (bcrypt or argon2)
  • Use httpOnly cookies
  • Set SameSite and Secure
  • Rotate refresh tokens
  • Add rate limits on login routes
  • Add CSRF if you use cookies
  • Log failed logins (without leaking data)
  • Don’t send raw errors to the client

Simple list. Big wins.

Final word (and a small confession)

Node.js auth isn’t magic. It’s knobs and gates and care. Sometimes it’s smooth. Sometimes you stare at a cookie flag at 1 a.m., and the coffee tastes like cardboard. But when you pick the right tool for your app, it feels good. Clean. Safe.

If you need fast and you’re on Next, I’d pick NextAuth.js. If you need polish and

My Hands-On Take on TypeScript’s forEach Loop

I’m Kayla. I build apps in TypeScript most days, and I’m not shy about what helps or what hurts. The array forEach loop? I’ve used it a lot—shipping features, fixing bugs, even tidying reports before lunch. It’s quick. It’s neat. And sometimes, it bites. If you’d like an even deeper dive into every corner case I’ve found, take a peek at my hands-on take on TypeScript’s forEach loop over on Improving Code. Need a refresher on the official behavior? The TypeScript forEach glossary entry lays out the mechanics in plain terms.

You know what? I still reach for it. But I’ve learned where it shines and where it falls flat. Let me show you what I mean with real code from my week. For an alternate angle, I also put together my honest take on the TypeScript forEach loop that digs into trade-offs I hit in production.

The gist, in plain words

forEach runs a function on each item in a list. It’s simple. You give it a callback, and it walks the array, left to right.

  • It’s great for side effects: logging, counters, DOM tweaks.
  • It does not return a new array.
  • It does not let you stop early.
  • And it doesn’t play nice with await.

That last one matters more than folks think.

What I like (and why I keep it in my pocket)

  • Clean and readable. Fewer braces, fewer vars.
  • TypeScript infers types well, so I write less noise.
  • Index and item are both there when I need them.
  • Map and Set have it too, so it feels familiar.

When I’m skimming code in VS Code at 7 a.m., this matters. I can “see” the flow faster. Prettier cleans it up. ESLint leaves it alone. Life’s good.

Real examples from my week

Here are some tiny slices from actual tasks I did.

1) Sum a cart (simple math, no drama)

type CartItem = { name: string; price: number; qty: number };

const cart: CartItem[] = [
  { name: 'Tea', price: 4, qty: 2 },
  { name: 'Honey', price: 8, qty: 1 },
];

let total = 0;

cart.forEach(item => {
  total += item.price * item.qty;
});

console.log(total); // 16

I could use reduce, sure. But this reads easy when I’m in a hurry.

2) Show tags with the index (tiny UI helper)

const tags: string[] = ['alpha', 'beta', 'prod'];

tags.forEach((tag, i) => {
  console.log(`${i}: ${tag}`);
});
// 0: alpha
// 1: beta
// 2: prod

Fast way to label rows or build a numbered list.

3) Narrow types while looping (yes, this works)

const mixed: Array<string | number> = [1, 'apple', 2, 'pear'];
const numbers: number[] = [];

mixed.forEach(x => {
  if (typeof x === 'number') {
    numbers.push(x);
  }
});

console.log(numbers); // [1, 2]

TypeScript narrows inside the if. No fuss.

4) Map.forEach has a twist (value, then key)

This one tripped me once during a late fix.

const scores = new Map<string, number>([
  ['Mia', 9],
  ['Noah', 7],
]);

scores.forEach((value, key) => {
  console.log(`${key}: ${value}`);
});
// Mia: 9
// Noah: 7

For arrays it’s (item, index). For Map, it’s (value, key). The order flips. I wish it didn’t. But now I remember.

5) DOM tweaks in a mini admin page

const buttons = document.querySelectorAll<HTMLButtonElement>('button.save');

buttons.forEach(btn => {
  btn.disabled = true;
});

Quick, clear, safe. I used this before a bulk save. I hit a similar quick-win recently when accepting a file-path argument in a tiny CLI—see my hands-on take on handling file-path arguments in TypeScript for the pattern.

While we’re on the subject of iterating over collections to build slick user experiences, reading up on successful products can spark design ideas. A great example is this thorough Bumble review — it breaks down the app’s UX flow, matching logic, and monetization tactics, giving engineers concrete inspiration for how they might structure their own loops and state updates. Another UX case study worth scanning—especially if you’re curious how a niche, location-based marketplace keeps gallery pagination snappy—is the AdultLook El Centro listing where you can see a lean, image-first layout that still manages to keep requests light and performance respectable, offering fresh ideas for optimizing list rendering in your own SPA.

What trips me up (and how I fix it)

Here’s the big one: forEach and async don’t work the way you think. It won’t wait for your await. It just runs the callbacks and moves on. For a deeper technical explanation of why this happens, check out this detailed answer that walks through the pitfalls.

The “it prints done too soon” bug

const ids = [1, 2, 3];

const fetchUser = async (id: number) => {
  return new Promise<{ id: number }>(res =>
    setTimeout(() => res({ id }), 100)
  );
};

ids.forEach(async id => {
  const user = await fetchUser(id);
  console.log('user:', user);
});

console.log('done');
// "done" logs before the users finish

When I first hit this, I groaned. Then I changed the loop.

Two fixes that actually work

Use for...of inside an async function:

async function loadUsers() {
  for (const id of [1, 2, 3]) {
    const user = await fetchUser(id);
    console.log('user:', user);
  }
  console.log('done');
}

loadUsers();

Or run them in parallel with Promise.all:

async function loadUsersFast() {
  const users = await Promise.all([1, 2, 3].map(fetchUser));
  console.log(users);
  console.log('done');
}

loadUsersFast();

Both are clear. Both are safe. I pick one based on whether I need order.

You also can’t break

This is small but real. You can’t stop early with forEach.

const nums = [1, 2, 3, 4];
let found: number | undefined;

nums.forEach(n => {
  if (n === 3) {
    found = n; // can't break here
  }
});

Use for...of if you need to break:

for (const n of [1, 2, 3, 4]) {
  if (n === 3) {
    found = n;
    break;
  }
}

A niche thing: passing a thisArg

I don’t use this much, but it’s handy sometimes.

class Counter {
  count = 0;
  add(n: number) {
    this.count += n;
  }
}

const nums = [1, 2, 3];
const counter = new Counter();

nums.forEach(function (n) {
  this.add(n);
}, counter);

console.log(counter.count); // 6

That second argument sets this inside the callback. I still prefer arrow functions most days. If you’re more into making your APIs self-describing, you might like my experiment with TypeScript named arguments for constructors.

When I pick something else

  • I need a new array: I use map.
  • I need to filter items: I use filter.
  • I need to stop early: I use for...of or a plain for.
  • I need speed in a hot path: I reach for a for index loop. It’s a tiny bit faster in tight loops.

Small note: forEach returns void. If you’re building and returning data, it’s not the right tool.

Tiny tips that saved me time

  • Trust inference. Let TypeScript infer the item type most of the time.
  • Name things well. A good callback name beats a comment.
  • Keep side effects clear. Don’t mix logging with building arrays.
  • Map order warning. Remember (value, key)

I tried a JavaScript to TypeScript converter. Here’s what actually happened

I’m Kayla. I run a mid-size Node + React app at work. Think 30k lines, a little messy, and full of helpers I wrote at 2 a.m. on cold coffee. I wanted TypeScript, but I didn’t want to rewrite everything by hand. So I used a JavaScript-to-TypeScript converter.

For readers who want the blow-by-blow migration diary, you can read my extended case study.

The main tool I used was ts-migrate (from Airbnb). I also used TypeStat later to fix types. And yes, I’ll show real code that changed. Some of it was great. Some parts… not so great. That’s the truth.

The setup: fast, a bit loud, but it works

I ran this on macOS using Node 18. I kept my normal build (Vite + Babel) and added TS step by step.

Here’s the exact stuff I ran on my repo:

# install once
npm i -D typescript ts-node @types/node

# init TypeScript
npx tsc --init

# quick tsconfig that worked for me
# (I turned on strict later after the first pass)

And I tweaked my tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "jsx": "react-jsx",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "strict": false,
    "noImplicitAny": false,
    "resolveJsonModule": true,
    "moduleResolution": "node",
    "baseUrl": "."
  },
  "include": ["src"]
}

Then I brought in ts-migrate:

npx ts-migrate init src
npx ts-migrate rename src
npx ts-migrate migrate src
  • init made a tsconfig if I didn’t have one.
  • rename changed .js to .ts and .jsx to .tsx.
  • migrate added types (often any) and comments to hush the compiler.

Honestly, the first run felt like a leaf blower in my code. Loud, but the yard looked cleaner.

Real example 1: a tiny helper got “any” everywhere

Before:

// src/utils/math.js
function add(a, b) {
  return a + b;
}
module.exports = { add };

After ts-migrate:

// src/utils/math.ts
export function add(a: any, b: any): any {
  return a + b;
}

It switched me to ESM export and stamped any on it. That’s normal. It’s not magic. It’s a start.

What I did next:

export function add(a: number, b: number): number {
  return a + b;
}

This took 10 seconds. But that’s the point. The tool gets you to green. You tighten types where it matters.

Real example 2: React props went from “shrug” to crisp

Before:

// src/components/Button.jsx
export default function Button({ label, onClick }) {
  return <button onClick={onClick}>{label}</button>;
}

After ts-migrate:

// src/components/Button.tsx
type Props = {
  label: any;
  onClick?: any;
};
export default function Button({ label, onClick }: Props) {
  return <button onClick={onClick}>{label}</button>;
}

I fixed it like this:

type Props = {
  label: string;
  onClick?: () => void;
};
export default function Button({ label, onClick }: Props) {
  return <button onClick={onClick}>{label}</button>;
}

React props are also a spot where I love passing in object literals; in another experiment I tried giving constructors proper named arguments and it cleaned things up even more.

I know, it’s simple. But this change made my IDE feel smart at last. Hover. Autocomplete. Safer refactors. Fewer “oops” moments.

Real example 3: a fetch wrapper got strict in a good way

Before:

// src/api/getUser.js
export async function getUser(id) {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error('Bad response');
  return res.json();
}

After ts-migrate:

// src/api/getUser.ts
export async function getUser(id: any): Promise<any> {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error('Bad response');
  return res.json();
}

Then I made a type:

type User = {
  id: string;
  name: string;
  email?: string;
};

export async function getUser(id: string): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error('Bad response');
  const data = await res.json();
  return data as User;
}

On the backend side, I recently applied the same strictness when importing CSV to DynamoDB with a Lambda function in TypeScript—another mini-project worth a peek.

Later, I dropped the as with better parsing. But day one? This was enough. Shipped and safe.

Where TypeStat helped me clean up

After ts-migrate, I had a lot of any. So I ran TypeStat to tighten types based on how code gets used.

npm i -D typestat
npx typestat --help

My simple config (typestat.json):

{
  "projects": ["tsconfig.json"],
  "transforms": ["narrowTypes", "noImplicitAny"]
}

Then:

npx typestat

A real change it made for me:

Before:

let names: any[] = [];
names.push('Ada');

After:

let names: string[] = [];

While tightening up arrays like names, I also revisited my stance on forEach versus classic loops; my hands-on take on TypeScript’s forEach loop dives into the nuances.

Small win. But when you have 100 of these, it feels big.

What I liked

  • It was fast. My first pass on 400 files took under 10 minutes.
  • It kept the app running. Tests still passed.
  • VS Code got way smarter. Hints everywhere.
  • The path to strict was clear. I could flip flags later.

What bugged me

  • Lots of any. Expected, but still a bit meh.
  • A few // @ts-ignore comments showed up. I removed most later.
  • Mixed module stuff (CJS vs ESM) needed care. I had one import loop.
  • Jest needed tweaks. I used ts-jest and had to fix paths.

Here’s the jest bit I used:

// jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1'
  }
};

If you ever trip over mysterious --file or path arguments in your own scripts, my hands-on take on taming those flags might save you an hour.

Not hard. Just a little fiddly.

Want a lighter path? JSDoc works too

If you’re not ready to rename files, you can stay in .js and turn on checkJs. Add JSDoc types and tsc will help you.

tsconfig:

{
  "compilerOptions": {
    "checkJs": true
  },
  "include": ["src"]
}

Then in code:

/**
 * @param {number} a
 * @param {number} b
 * @returns {number}
 */
function add(a, b) {
  return a + b;
}

I even had to rename a few long-standing fields, and keeping the original JSDoc while doing so was trickier than I thought—here’s my hands-on review of how to pull that off.

This is great for teams that want safety first, and .ts later.

As I was working through the migration, the succinct checklist on ImprovingCode helped me prioritize which files to tackle next.

Little tips I wish someone told me

  • Convert folder by folder. Don’t swing at the whole repo on day one.
  • Keep strict off at first. Then turn it on, bit by bit.
  • Add ESLint with typescript-eslint. It catches weird stuff early.
  • Kill dead code first. Less junk means fewer any types.
  • Write a

I forgot the type param in TypeScript. Here’s what really happens.

I use TypeScript every day. I mess up, too. One tiny thing keeps biting me: what happens when I don’t set a type parameter. You know what? TypeScript is helpful most days. But it can also be sneaky. (I collected an even deeper dive in this dedicated post on what happens when you forget the type param if you want the full story.)

Here’s my honest take, with real code I wrote at work and at home.


Quick take: what I see when I skip the type

  • TypeScript tries to guess from your input. Good when it’s clear.
  • If it can’t guess, it often gives you unknown. Sometimes any. Sometimes never.
  • Some libs add their own defaults. That can hide bugs. Or save your bacon.

Let me explain with simple cases.

For an expanded guide with more production examples, head over to my write-up on ImprovingCode.


When TypeScript can guess, you’re fine

If the function has a value it can read, it will infer the type.

function id<T>(value: T): T {
  return value;
}

const a = id(42);        // number
const b = id("pizza");   // string
const c = id({ ok: true }); // { ok: boolean }

No type param set. Still works. Clean and sweet.


When it can’t guess, you get unknown

This is the one that trips folks.

function makeValue<T>() {
  // pretend we're building something complex
  return null as unknown as T;
}

const value = makeValue();  // type: unknown
// value.toUpperCase(); // error, it's unknown

No input. No hint. So T becomes unknown. That’s pretty strict. It keeps you safe, but it might slow you down.


Empty arrays can produce never (yep, never)

This one feels weird the first time.

function first<T>(xs: T[]): T | undefined {
  return xs[0];
}

const f1 = first([1, 2, 3]); // number | undefined
const f2 = first([]);        // never | undefined

An empty array gives no clues. So it infers T as never. That reads like, “there is no possible item here.” Fair point. But it can shock you during refactors.

Tip: give the array a type.

const f3 = first<number>([]); // number | undefined
// or
const f4 = first([] as number[]);

If you later iterate over such arrays, TypeScript’s quirks around forEach can show up, too—my hands-on take on the forEach loop and my follow-up with an even more honest take cover the edge cases.


Default type parameters save the day

You can set a default type on your own generics. It helps a lot.

function wrap<T = string>(value?: T) {
  return { value } as { value: T };
}

const w1 = wrap();            // { value: string | undefined }
const w2 = wrap(123);         // { value: number }
const w3 = wrap<boolean>(true); // { value: boolean }

No type passed? It uses string (in this case). Clear and steady. (Introduced in TypeScript 2.3—see the official release notes for details.)


Real library stuff I hit in the wild

Little tour of “I didn’t set the type and here’s what I got.”

  • React useState

    const [thing, setThing] = useState(); // thing: undefined
    // Type default comes from React’s types: S = undefined when no initial value.
    

    If you want null instead:

    const [user, setUser] = useState<null | { id: string }>(null);
    
  • Promise.resolve with no value

    const p = Promise.resolve(); // Promise<void>
    

    Handy for chaining, but it’s not “any.” It’s void.

  • Map and Set with no entries

    const m = new Map(); // Map<any, any>
    const s = new Set(); // Set<any>
    

    These default to any. It’s easy, but also risky. I try to type them:

    const users = new Map<string, number>();
    
  • Axios (no generic)

    const res = await axios.get("/api/user"); // AxiosResponse<any>
    const user = res.data; // any
    

    If you want safety:

    type User = { id: string; name: string };
    const res2 = await axios.get<User>("/api/user");
    const user2 = res2.data; // User
    
  • fetch + Response.json

    const res = await fetch("/api/user");
    const data = await res.json(); // any
    

    You can guide it:

    const data2 = await res.json<{ id: string; name: string }>(); // typed
    
  • Prefer constructors with named arguments
    If you like a more explicit API, you can emulate named-argument constructors—I shared the pattern in this experiment with TypeScript named arguments.


A tiny constraint helps a lot

I like soft rails. Not walls.

function readField<T extends Record<string, unknown>>(obj: T, key: keyof T) {
  return obj[key];
}

const val = readField({ a: 1 }, "a"); // number
// Without the constraint, you can end up with unknown or any more often.

While we’re on safe refactors, keeping JSDoc in sync when you rename a field is another life-saver—here’s how I handle it in practice.


Common gotchas I’ve hit

  • Empty arrays infer never. Use a type.
  • No inputs and no defaults? You get unknown.
  • Some stdlib constructors return any when empty (Map, Set).
  • Union types can widen if you’re not careful with literals.
    const x = Math.random() < 0.5 ? "a" : "b"; // "a" | "b"
    id(x); // still "a" | "b" — nice
    id(["a", "b"]); // string[] (widened) unless you use const
    const xs = ["a", "b"] as const; // readonly ["a","b"]
    
  • CLI helpers that take a file-path argument often default to string, but you can give them a more specific type—my take on typing a file path argument shows the pattern.

My simple rules now

  • If the call has no clear input, add the generic or a default.
  • Type empty things: [], new Map(), new Set().
  • Use defaults on your APIs: <T = MyDefault>.
  • Keep noImplicitAny on. It catches leaks.
  • For data from the network, set the type near the edge.
  • Write useful comments that survive the compiler—my notes on real-world TypeScript comments explain why.

Final verdict

I like how TypeScript guesses. It feels smart. But when I skip a type parameter, I can get unknown, never, or any, based on the shape of the call and the library types. That mix is both helpful and sneaky.

Would I trust inference alone? Not always. I enjoy the speed, but I set defaults on my own generics, and I annotate the tricky spots. It’s a small habit that saves a big mess later.

In the same spirit of choosing sensible defaults, sometimes you also want a modern alternative to the default classifieds sites you use online. If Craigslist isn’t quite meeting your needs, an increasingly popular option is Doublelist—the linked guide walks you through how the platform compares to traditional personals sites, outlines its safety features, and offers practical tips for creating effective listings and meeting people locally.

If your travels or home base land you along South Carolina’s Grand Strand, you may prefer a service that focuses specifically on that area’s nightlife and dating scene. A quick read of the AdultLook Myrtle Beach guide will give you a clear overview of how the site works, current pricing, safety best practices, and insider tips for arranging low-stress meet-ups while you’re in town.

And

TypeScript Exclamation Mark: My Short, Honest Take

I code in TypeScript all day. VS Code, React 18, Node, the usual mess. And that little exclamation mark? I’ve used it a lot. It’s fast. It feels bold. Sometimes it saves me. Sometimes it blows up my build at 2 a.m.

If you want the extended deep-dive with edge cases and benchmark numbers, it's all in my TypeScript exclamation mark deep dive where I document every pitfall I’ve hit in production.

You know what? It’s like a sharp knife. Cuts clean—until it cuts you.

For context, I only began caring about nullability after I migrated a hairy JavaScript codebase to TypeScript, so the habit runs deep.

What the “!” even does

There are two flavors:

  • Non-null assertion: value!. It tells TypeScript, “Trust me, this isn’t null.”
  • Definite assignment: name!: Type. It tells TypeScript, “I’ll set this later. Promise.”

Short. Loud. Bossy.

It first showed up back in TypeScript 2.0’s release notes if you’re curious about the official rationale.

Real code I wrote (and shipped)

A React ref that needed focus

This was a login screen. Pretty simple.

const inputRef = useRef<HTMLInputElement>(null);

useEffect(() => {
  inputRef.current!.focus(); // I told TS: it's there
}, []);

And yes, the day I shipped this I actually forgot the type parameter in another component—TypeScript politely looked the other way until runtime reminded me.

This worked fine. In my app, the input does exist after mount. It feels safe here. Still, Strict Mode runs effects twice, so I saw two blinks. Not a big deal, but worth saying.

DOM query where I got cocky

I did this on a landing page:

const saveBtn = document.querySelector('#save')!;
saveBtn.addEventListener('click', () => {
  console.log('saved!');
});

On my machine it was perfect. On a slow phone, it crashed. The script ran before the button showed up. Boom: “Cannot read properties of null.” I got a bug report on Safari. Felt dumb.

Class field I said I’d set “later”

class Store {
  private db!: Database;

  async init() {
    this.db = await connect();
  }

  getUser(id: string) {
    return this.db.get(id); // crash city if init() is missed
  }
}

I forgot to call init() in one path. It blew up in QA. I knew better. But a busy day is a busy day.

When “!” actually helped me

  • I had to ship a tiny fix fast. The type was noisy. I knew the value was set by the time the code ran. One “!” and done.
  • In unit tests, after I create the DOM or mock data, I use “!” to keep tests short.
  • In my React entry file (main.tsx), I know the HTML root exists. I write a comment and use it.
// index.html always has #root (build step ensures it)
const root = document.getElementById('root')!;
createRoot(root).render(<App />);

That same 'I know it’s there' attitude is sometimes OK for infrastructure code too; for instance I recently dug into how to validate a file-path argument in a TypeScript CLI without sprinkling ! everywhere.

Clear claim. Clear risk. I accept it there.

Where it bit me, hard

  • Slow network = null at runtime. My fearless “!” turned into a crash.
  • A coworker moved a script tag. My “safe” DOM code wasn’t safe anymore.
  • I merged a refactor and forgot init(). The class field with “!” hid the risk from TypeScript, and it didn’t warn me.

Honestly, the worst part was that TypeScript tried to help. I told it to hush.

Safer moves I reach for now

1) Narrow first, then use it

const btn = document.querySelector('#save');
if (!btn) throw new Error('Missing #save button'); // loud but honest
btn.addEventListener('click', onSave);

Now if markup changes, I get a clean error. Not a weird null crash.

2) Optional chaining and friends

const name = user?.profile?.name ?? 'Guest';

No “!” needed. No surprise crash.

While we’re on safer syntax, my hot take on TypeScript’s forEach loop shows how even iterating can be less foot-gun-y when you lean on the compiler.

3) A tiny assert helper

I use this a lot:

function assertExists<T>(value: T | null | undefined, msg?: string): asserts value is T {
  if (value == null) throw new Error(msg ?? 'Expected value to exist');
}

const el = document.querySelector('#save');
assertExists(el, '#save not found');
el.addEventListener('click', onSave);

Now TypeScript narrows the type. And I get a clear message if it’s missing.

4) Constructor setup beats “I’ll set it later”

class Store {
  constructor(private db: Database) {}
  getUser(id: string) {
    return this.db.get(id);
  }
}

Sticking to constructor injection also plays nicely with large refactors—renaming a TypeScript field while preserving JSDoc is way easier when the compiler can trace every assignment.

No “!” at all. Fewer headaches.

5) React ref without bravado

if (inputRef.current) {
  inputRef.current.focus();
}

It’s one extra line. It saves future me.

For an even deeper dive into writing safer TypeScript code, check out the practical guides on Improving Code.

My quick guardrails

  • If I add “!”, I leave a comment: why it’s safe.
  • I keep strictNullChecks on. I like the noise.
  • I use @typescript-eslint/no-non-null-assertion.
  • I turned on noUncheckedIndexedAccess. It nudges me to handle missing items.
  • I ask: can I prove this with code? If yes, I skip “!”.

For linting support, the rule @typescript-eslint/no-non-null-assertion is what keeps me honest when I’m tempted to sprinkle exclamation points everywhere.

For a battle-tested approach to inline docs, my walkthrough on writing maintainable comments in TypeScript shows the exact patterns I copy-paste.

When I still use “!” without guilt

  • In tests, after I set up the DOM or mocks.
  • In main.tsx, right on the root element, with a comment.
  • When a framework guarantees a node (like a portal target) and it’s part of the build, not user input.

That’s it. Very small list.

Tiny cheatsheet

  • Non-null “!”: value must not be null at runtime, or you crash.
  • Definite “!” on fields: you promise to assign before use. If you forget, boom.
  • Better tools first: narrowing, optional chaining, assert functions, constructors.

Verdict

I give the TypeScript “!” a 3.5 out of 5.

It’s fast. It’s bold. It’s also a little reckless. Use it like a power tool—only when you can name the guard and point to the fence. And leave a note for the next person. Which might be you.

Just as you should weigh the real-world trade-offs before slapping an exclamation mark into your codebase, you might want to do a bit of homework before you hit “download” on yet another dating app. If you’re currently deciding where to invest your swipes, this no-fluff Hinge review breaks down the app’s core features, hidden costs, and success tips so you can judge whether it’s worth your time.

Node.js vs TypeScript: What I Actually Use, And When

I build apps every week. I live in Node.js. I also write a lot of TypeScript. People ask, “Which one should I pick?” Here’s the thing: they’re not the same thing. If you’re weighing the two side by side, I unpack the strengths and trade-offs in detail in this practical Node.js vs TypeScript guide.

Node.js runs your JavaScript. TypeScript is how you write your JavaScript—with types. I use them together all the time. But sometimes I don’t. Let me explain.

Wait—so what are we comparing?

  • Node.js: the runtime. It runs your code on the server.
  • TypeScript: the language layer. It adds types and checks your code before it runs.

You can write Node with plain JavaScript. Or with TypeScript. I do both. I pick based on speed, size, and risk. I even ran an experiment where I removed Node entirely from my machines—here’s how that adventure went.

If you want a straight-to-the-point guide that sharpens how you use both, swing by Improving Code for some battle-tested tips.

A real story: the bake sale API that bit me

Last spring, I built a tiny Express API for a school bake sale. Orders came in. I saved them. Easy, right? I wrote it fast with plain Node and JavaScript. No build step. No types. I shipped it before lunch.

Two days later, a bug popped up. Price came in as a string, not a number. “12.50” got stored as “12.5012.50” during a bad merge. My fault. I was tired and sipping cold coffee. TypeScript would’ve flagged that mismatch right away. It would say, “Hey, price should be a number.” Instead, I found it at 9 p.m., squinting at logs.

I rewrote that part in TypeScript the next morning. The error went away. And I slept better. If you’re tempted to reach for an automatic JS→TS converter, see what actually happened when I tried one.

But also true: tiny scripts don’t need the fuss

I write little Node scripts all the time. Rename images. Fetch a CSV and clean it. Ping a health check. For those, plain JS wins. I open a file, write ten lines, and run it. Done. No tsconfig. No build. I like that when I’m in a rush or just testing an idea.

It feels like making a sandwich instead of cooking a full meal.

Big team? TypeScript pays for itself

My team built a meal-planner API in NestJS with TypeScript. We had DTOs, interfaces, and strict types. One Friday, we changed the shape of a Recipe object. TypeScript yelled in 14 files. It was loud—but helpful. We fixed them fast. CI passed. We shipped. Missing a generic type param once broke staging; here’s what really happens when you forget one.

Later, a new dev joined. They read the types and got it. Fewer “What does this function do?” messages. Fewer Slack pings at 6 p.m. You know what? That saved real time.

Want a concise roundup of the advantages developers see after adding TypeScript? Check out Strapi’s breakdown of the key benefits.

Tooling trade-offs I feel day to day

  • TypeScript adds a build step. I use tsx or swc to keep it snappy. Plain Node runs right away.
  • Jest needs a bit more setup with TypeScript. Not hard, just… a few extra lines.
  • Debugging is nicer with TS + source maps. Stack traces make sense.
  • Types make refactors safer. Rename a thing, and TS shows you every place you’ll break.

Performance: don’t worry

TypeScript compiles to JavaScript. Node runs that JavaScript. So runtime speed is the same. The extra cost is in dev time (build, check). I feel it on slow laptops. On my M2, it’s fine. Security-wise, I’ve broken down what actually bites you (and what doesn’t) in my candid take on whether Node.js is safe.

I once helped a friend prototype a geolocation-based dating microservice specifically for users in Rennes. We started with a quick Node.js script to prove the concept, then hardened it with TypeScript once the data shapes (age ranges, coordinates, match preferences) became critical. If you’re curious to see how that kind of product looks from the user’s perspective, take a peek at this live Rennes dating page where you can explore real profiles and understand the features people expect—filters, instant chat, and location-aware matches—all of which influence how you might structure the underlying API. Likewise, when your target market shifts from France to central Texas, examining how escort and meetup platforms structure their data is instructive—sites like AdultLook Waco lay out profiles, pricing details, and real-time availability, giving you a concrete checklist of the fields and filters your own code will need to support.

When I pick each path

I pick plain Node (JavaScript) when:

  • It’s a small script or CLI
  • I need to try an idea in 15 minutes
  • I won’t touch it again after today

I pick Node + TypeScript when:

  • It’s an API, worker, bot, or service
  • More than one person will edit it
  • I want fewer late-night bugs
  • Data shapes matter (IDs, money, dates)

Plenty of other teams have reached the same conclusion; there’s an exploration of TypeScript’s strengths in large-scale JavaScript projects that mirrors these points.

A tiny gotcha list (that caught me)

  • ESM vs CJS in Node can be messy. I stick to one and move on.
  • Type-only imports. If I import just types, I mark them as type. It avoids weird runtime errors.
  • Some libraries need @types packages. Not hard, but easy to forget.
  • Path aliases are great. But set baseUrl and paths right, or your tests sulk.
  • The ! non-null assertion can save or sink you; I wrote a short rant about it here.

A quick “what I use” setup

On fresh TypeScript Node projects, I keep it light:

  • tsx for running dev code without heavy build steps
  • eslint + prettier for clean code
  • vitest or jest with ts support
  • npm scripts: “dev”, “build”, “start”

It’s simple. It works. It keeps my brain calm.

One more real example: the Slack bot that lied

I wrote a Slack bot in plain JS that tagged the wrong users. Fun times. The bug was a string vs number userId mix. TypeScript would’ve flagged it right away. I rebuilt it in TS. I added a UserId type alias. No more mix-ups. And no more “Why did you ping my boss?” messages.

My take, no fluff

  • For quick hacks: plain Node.
  • For real apps: Node with TypeScript.
  • For everything in between: start in JS, move to TS when the code grows roots.

I like both. I use both. If your code will live for a while, TypeScript saves you from future you. If it’s a one-off, keep it simple and ship.

Now if you’ll excuse me, I’ve got a tiny Node script to fix. It’s named “really-final.js.” Don’t judge.