Don't Trust TypeScript

I've never done front-end work professionally, but I am not the kind of guy who would pass up an opportunity like this. So I promised to know everything worth knowing about Angular by January. The first surprise came when the project started in December.

I had a very interesting week at work.

About mid-November, the client account manager approached me and offered me to participate in a short-term project after the New Year, where we would be implementing some new functionality in a guest-facing Angular application and the Java backend service that powers it. He wanted to know that I could do TypeScript and Angular because even though I would work mainly in the back end, I'd still need to help the front-end guy. And if we do well, "there might be more."

I've never done front-end work professionally, and my previous experience with TypeScript adds up to approximately half an hour; but I am not the kind of guy who would pass up an opportunity to do something completely different for a few sprints. So I slightly exaggerated my TS experience, and promised to know everything worth knowing about Angular by January.

The first surprise came when the project started in December.

The second surprise was when the UI guy went on vacation half way through the project. So, I wasn't helping him, gaining valuable knowledge as an apprentice to an experience craftsman. I was going to replace him, and I was going to be on my own.

The work itself was fine. On the back end, just passing through a couple of new values based on some configuration settings. On the front end it was a little more complicated, because the user experience had to be updated for the new feature with updated elements and interactions. We were ahead of schedule and very pleased with ourselves. I was even a little disappointed, because it looked like my colleague was going to be so far ahead of schedule that I won't have anything to do with Angular after all, and there goes my new resume item!

Well, I shouldn't have worried. The third surprise came the day before my colleague left for India. QA came back with a bug in the app, that could only be reproduced in iOS.

Now this is an Angular app, but the user doesn't access it in a web browser. There's a native app for Android and iOS that pulls it up and runs it in an embedded browser, which to my understanding is a totally normal thing to do. Anyway, it wasn't working in the iOS version, and the thing is that I don't have a Mac. I've been asking for a Mac—it's the closest thing I can get to Linux at work, and every single day I have to fight this Windows machine I've been given is a reminder why I stopped using Microsoft products around 2009.

Xcode and Simulator are OSX only. Apple no longer maintains Safari for Windows. And I wasn't going to run a VM, even if I could get IT to let me. I feel like if I started a VM on this machine I might not be able to turn it, or anything else, off again. Or use any other application on this toaster... There wasn't time for me to get a Mac, either. So, lacking any better choices, this is the process we landed on:

First, I check out a new branch locally, and add a whole bunch of debug logs. Then,

  1. I deploy this branch to the lower environment.
  2. I call my colleague in Mexico who has a Mac, he runs the app in Safari, saves the logs and sends them to me via Slack.
  3. I review the logs, make some changes.
  4. Iterate.

Now my colleague who has a Mac does native development. He has Xcode, not Node. We tried to set up Node on his machine so that he could run this thing locally instead of having to deploy, but (believe it or not) he had his own work to do instead of raising access tickets to the company Node repository! And when it turned out in our quick, abortive attempt we even managed to mess up his environment, we decided that this is not the way forward.

After I spent Monday day working with my colleague to get authentication working in Safari without the ModHeader Chrome extension, and Tuesday and Wednesday getting CI/CD to reveal to me how to deploy my temporary branch, I was finally able to start working. After committing my code, I could see the results of my changes in about 2 hours. The exact time depended on if the CI/CD pipeline was having a good day, if the upstream services were down, how the token service was doing, and how busy my incredibly talented and helpful colleague, without whom this bug would never have been fixed before the weekend, was.

So, I was about to find out if my bluff was going to pay off.

After the first iteration, it became clear that the issue was related to the use of the native JS Date object, which is notoriously inconsistent across platforms.

generateTimes(date: Date) {
  console.log(date) // 2025-1-25
 
  const otherDate = new Date(date);
  console.log(JSON.stringify(otherDate)) // Invalid Date Object
}

Hmm. The documentation says that this is fine, but who knows what nonsense lurks in the Safari implementation? Elsewhere in the code we mostly use the Moment.js library. Let's just use that:

generateTimes(date: Date) {
  console.log(date) // 2025-1-25
  
  const otherDate = moment(date);
  console.log(JSON.stringify(otherDate)) // Now it's null
}

The docs say that this too should work fine. And remember, this whole time it's working in Firefox and Chrome! Also, proving that a mysterious clue only adds to the detective's confusion, I got this: Deprecation warning: value provided is not in a recognized RFC2822 or ISO format. moment construction falls back to js Date(), which is not reliable across all browsers and versions. Non RFC2822/ISO date formates are discouraged. Please refer to https://momentjs.com/guides/#/warnings/js-date/ for more info.

Deprecation warning? Format? I'm just passing a Date object! What does this mean?!

Honestly, if I had more experience I should have probably figured it out already... The answer, of course is obvious! But I couldn't see it, so I had to use another expensive iteration.

What's next? I definitely don't want to mess with converting back and forth to strings. This way lie mysterious bugs and output that looks fine but is actually subtly incorrect, and I don't have time for that. At least right now I know it's not working because I'm getting invalid and null values! Instead of date strings, why don't we try to get the epoch time... That should work, right?

generateTimes(date: Date) {
  console.log(date) // 2025-1-25
  
  const otherDate = moment(date.getValue());
  console.log(JSON.stringify(otherDate)) // Nope, still null
}

Actually, nothing changed. Still null, still the warning. I even checked that the right build version was deployed.

This warning was driving me nuts, as the pirate said about the steering wheel stuck in his pants. What format? We're just passing a number! I tried to use getTime() instead of getValue() and for the first time, it broke in Chrome, when I was testing locally before making my commit. I got this: e.getTime is not a function. What?! The Date object definitely has a getTime() function!

It was getting late on Thursday and I really, really needed to get this done by Friday if I don't want this to carry over to the next sprint. And it's winter, Shabbos starts early, we have babies, and my wife will not consent to me sitting hunched in front of my keyboard until the last minute. I need to figure this out, and I need to have the fix out by lunch tomorrow, and my colleague is not going to be staying up with me tonight going back and forth until the wee hours.

So I walked away, played with the kids, made dinner, helped with bedtime. And then, when the house was quiet and the snow fell slowly in the street lamp light outside the frosted windows, I came back to my desk. And then I understood.

generateTimes(date: Date) {
  console.log(date) // 2025-1-25
  
  const otherDate = moment(date, "YYYY-M-DD");
  console.log(JSON.stringify(otherDate)) // Spoiler: it worked!
}

I'm so used to Java, where type safety is pretty, for want of a better want, safe to rely upon. But TypeScript just gets transpiled to JavaScript, which is dynamically typed.

This is a new codebase for me, and I didn't choose to dig in to the upstream logic, but I think that it's pretty clear that somehow the date parameter is not a Date object, and it isn't being caught by the transpiler. The reason that date.getValue() was giving the same results as date, and date.getTime was not a function, is because date is a string.

So, I deployed the fix in the evening, and in the morning we saw that it had worked. The ticket is now in the QA queue with a lot of confidence that it'll pass. And I have gained something very valuable, exactly what I wanted from this adventure—something that's important to know, but that people might not tell you up front:

Don't trust TypeScript types at runtime.

And that's all.

By the way, if you're a TypeScript expert and know that I'm wrong about this, and my fix worked for a different reason (or that I'm due a nasty surprise in a little bit), please let me know.