Why is "Date" in TypeScript a Trap?
\nImagine: you're writing an application, everything works perfectly. But one fine day, a bug report comes in: "A user from Tokyo sees the date one day earlier." You open the code and see the classic: let date: Date = "2024-12-01". Or even worse — any. Sound familiar?
The question "How do I express a date type in TypeScript?" on Stack Overflow has garnered tens of thousands of views. And for good reason. It seems the answer is simple: use Date. But in practice, it's more complicated. TypeScript is a superset of JavaScript, and Date in JS is an object that behaves... peculiarly.
In this article, we'll break down not just the syntax, but proper date typing that will save your project from bugs, improve performance, and make the code understandable for your team. You'll learn how to work with real dates in TypeScript without wasting time on debugging.
\n\n1. Basic Typing: Date, string, or number?
\nThe most obvious option is the built-in Date type:
let birthday: Date = new Date('1990-05-15');\nDoes it work? Yes. But there's a catch: TypeScript does not check if the date is valid. You can write new Date('abc') — there will be no compilation error, but at runtime you'll get Invalid Date. And trying to call .getTime() will return NaN.
A stricter option is to use ISO 8601 formatted strings:
\ntype ISODateString = string;\nlet eventDate: ISODateString = '2024-12-01T10:00:00Z';\nPros: simple type, easily serializable to JSON. Cons: no validation — you can assign 'hello', and TS will stay silent.
The third option is timestamp (number):
\ntype Timestamp = number;\nlet ms: Timestamp = Date.now();\nIdeal for mathematical operations, but unreadable for humans. And again — any number will pass the check.
\n\nSummary of Basic Types
\nIf you need quick and dirty typing — use Date. If you're working with an API — use string. If it's only calculations — use number. But for serious projects, this is not enough. Let's dive deeper.
2. Custom Types and Branded Types
\nTo prevent TypeScript from confusing a date-string with an ordinary string, branded types are used. It's like a sticker saying "I'm a date, verified!":
\ntype DateString = string & { __brand: 'DateString' };\n\nfunction toDateString(date: Date): DateString {\n return date.toISOString() as DateString;\n}\n\nfunction processDate(d: DateString) {\n // here d is definitely an ISO string\n}\n\nconst iso = toDateString(new Date());\nprocessDate(iso); // OK\n// processDate('hello'); // ❌ Error!\nThis adds compile-time safety. The cost is a bit more code. But for large projects (especially if you have microservices or REST APIs), this is the gold standard.
\n\n3. Libraries for Working with Dates in TypeScript
\nRaw Date is evil. It's mutable, parses strings inconsistently, and timezones are a separate pain. Therefore, real projects use libraries. Here are three favorites:
3.1. date-fns (Lightweight and Modular)
\nimport { format, parseISO } from 'date-fns';\n\nconst date: Date = parseISO('2024-12-01T10:00:00Z');\nconst formatted: string = format(date, 'dd.MM.yyyy');\nPros: tree-shaking, immutable (returns new dates), excellent typing.
\n\n3.2. Day.js (Minimalist)
\nimport dayjs, { Dayjs } from 'dayjs';\n\nconst now: Dayjs = dayjs();\nconst formatted: string = now.format('DD.MM.YYYY');\nDay.js is like Moment.js, but without the heavy bundle. The Dayjs type is a wrapper that is easy to pass between functions.