SoFunction
Updated on 2025-04-06

How to handle date strings in TypeScript

Preface:

In one of my recent projects, I had to deal with multiple custom date string notation, e.g.YYYY-MM-DDandYYYYMMDD. Since these dates are string variables, TypeScript will infer by default tostringtype. Although this is not wrong with technical implementation, using such type definitions in work is broad, making it difficult to effectively handle these date strings. For example,let dog = 'alfie'It is also inferred as astringtype.

In this post, I will present my workaround to you to improve the developer experience and reduce potential errors by typing these date strings.

Before entering coding, let's briefly review what needs to be used to achieve the goaltypescriptFeatures, namely template literal type and narrowing the scope by type predicates.

1. Template literal type

Introduced in typescript 4.1, the template literal type and JavaScript's template string syntax are the same, but are used as types. The template literal type resolves to a union of all string combinations of a given template. This may sound a bit abstract,

Look directly at the code:

type Person = 'Jeff' | 'Maria'
type Greeting = `hi ${Person}!` // Template literal type
const validGreeting: Greeting = `hi Jeff!` // 
// note that the type of `validGreeting` is the union `"hi Jeff!" | "hi Maria!`
const invalidGreeting: Greeting = `bye Jeff!` // 
// Type '"bye Jeff!"' is not assignable to type '"hi Jeff!" | "hi Maria!"

Template literal types are very powerful, allowing you to perform general type operations on these types. For example, capitalization.

type Person = 'Jeff' | 'Maria'
type Greeting = `hi ${Person}!`
type LoudGreeting = Uppercase<Greeting> // Capitalization of template literal type
const validGreeting: LoudGreeting = `HI JEFF!` // 
const invalidGreeting: LoudGreeting = `hi jeff!` // 
// Type '"hi Jeff!"' is not assignable to type '"HI JEFF!" | "HI MARIA!"

2. Reduce the scope of type predicates

Typescript performs very well in narrowing the scope of types. You can see the following example:

let age: string | number = getAge();
// `age` is of type `string` | `number`
if (typeof age === 'number') {
  // `age` is narrowed to type `number`
} else {
  // `age` is narrowed to type `string`
}

That is, when processing custom types, telltypescriptHow the compiler does type reduction is helpful. For example, when we want to narrow down to a type after performing runtime verification, in this case, the narrowing of the type predicate, or the user-defined type guard, can come in handy.

In the following example, the isDog type guard helps narrow down the types of animal variables by checking type properties:

type Dog = { type: 'dog' };
type Horse = { type: 'horse' };
//  custom type guard, `pet is Dog` is the type predicate
function isDog(pet: Dog | Horse): pet is Dog {
  return  === 'dog';
}
let animal: Dog | Horse = getAnimal();
// `animal` is of type `Dog` | `Horse`
if (isDog(animal)) {
  // `animal` is narrowed to type `Dog`
} else {
  // `animal` is narrowed to type `Horse`
}

3. Define date string

For brevity, this example only includesYYYYMMDDThe code of the date string.

First, we need to define the template literal type to represent the union type of all date-like strings

type oneToNine = 1|2|3|4|5|6|7|8|9
type zeroToNine = 0|1|2|3|4|5|6|7|8|9
/**
 * Years
 */
type YYYY = `19${zeroToNine}${zeroToNine}` | `20${zeroToNine}${zeroToNine}`
/**
 * Months
 */
type MM = `0${oneToNine}` | `1${0|1|2}`
/**
 * Days
 */
type DD = `${0}${oneToNine}` | `${1|2}${zeroToNine}` | `3${0|1}`
/**
 * YYYYMMDD
 */
type RawDateString = `${YYYY}${MM}${DD}`;
const date: RawDateString = '19990223' // 
const dateInvalid: RawDateString = '19990231' //31st of February is not a valid date, but the template literal doesnt know!
const dateWrong: RawDateString = '19990299'//  Type error, 99 is not a valid day

As you can see from the example above, the template literal type helps specify the format of the date string, but there is no actual verification of these dates. Therefore, the compiler will19990231Mark as a valid date, even if it is incorrect, just because it matches the type of the template.

In addition, when checking the above variablesdatedateInvaliddateWrongWhen you find that the editor will display a union of all valid characters for these template literals. Although useful, I prefer to set the nominal type so that the type of the valid date string isDateString, not"19000101" | "19000102" | "19000103" | .... Nominal types also come in handy when adding user-defined type protection.

type Brand<K, T> = K & { __brand: T };
type DateString = Brand<RawDateString, 'DateString'>;
const aDate: DateString = '19990101'; // 
// Type 'string' is not assignable to type 'DateString'

To ensure ourDateStringTypes also represent valid dates, we will set a user-defined type protection to verify dates and narrow down types

/**
 * Use `moment`, `luxon` or other date library
 */
const isValidDate = (str: string): boolean => {
  // ...
};
//User-defined type guard
function isValidDateString(str: string): str is DateString {
  return (/^\d{4}\d{2}\d{2}$/) !== null && isValidDate(str);
}

Now, let's look at the date string type in a few examples. In the following code snippet, user-defined type protection is applied to type reduction, allowing the TypeScript compiler to refine types to more specific types than declared. Type protection is then applied in a factory function to create a valid date string from an unstandardized input string.

/**
 *   Usage in type narrowing
 */
// valid string format, valid date
const date: string = '19990223';
if (isValidDateString(date)) {
  // evaluates to true, `date` is narrowed to type `DateString` 
}
//  valid string format, invalid date (February doenst have 31 days)
const dateWrong: string = '19990231';
if (isValidDateString(dateWrong)) {
  // evaluates to false, `dateWrong` is not a valid date, even if its shape is YYYYMMDD 
}
/**
 *   Usage in factory function
 */
function toDateString(str: RawDateString): DateString {
  if (isValidDateString(str)) return str;
  throw new Error(`Invalid date string: ${str}`);
}
//  valid string format, valid date
const date1 = toDateString('19990211');
// `date1`, is of type `DateString`
//  invalid string format
const date2 = toDateString('asdf');
//  Type error: Argument of type '"asdf"' is not assignable to parameter of type '"19000101" | ...
//  valid string format, invalid date (February doenst have 31 days)
const date3 = toDateString('19990231');
//  Throws Error: Invalid date string: 19990231

Summarize:

I hope this post gives us an idea of ​​TypeScript's ability to enter custom strings. Remember that this approach also works with other custom strings like custom stringsuser-idsuser-xxxx, and other date strings, likeYYYY-MM-DD

The possibilities are endless when combining user-defined type protection, template literal strings, and nominal types.

This is the end of this article about how to process date strings in TypeScript. For more related contents of TypeScript processing strings, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!