Preface:
In one of my recent projects, I had to deal with multiple custom date string notation, e.g.YYYY-MM-DD
andYYYYMMDD
. Since these dates are string variables, TypeScript will infer by default tostring
type. 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 astring
type.
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 goaltypescript
Features, 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, telltypescript
How 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 includesYYYYMMDD
The 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 will19990231
Mark as a valid date, even if it is incorrect, just because it matches the type of the template.
In addition, when checking the above variablesdate
、dateInvalid
、dateWrong
When 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 ourDateString
Types 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-ids
、user-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!