Implement ValidDate<T>, which checks whether an MMDD string is a real calendar date. Every legal month and day gets enumerated as a union of string literals.
The compiler can learn the calendar: this type rejects February 29th and the 32nd of January before a single line of code runs.
ValidDate takes an input type T and returns whether T is a valid date. Dates arrive as four-character MMDD strings, and different months allow different day ranges. So you'll be splitting string types apart with template literal inference and modelling the valid days of each month as unions of string literals. It's a compact showcase of how far string validation can go in the type system.
Leap year is not considered
ValidDate<'0102'> // true
ValidDate<'0131'> // true
ValidDate<'1231'> // true
ValidDate<'0229'> // false
ValidDate<'0100'> // false
ValidDate<'0132'> // false
ValidDate<'1301'> // falseImplement a type ValidDate, which takes an input type T and returns whether T is a valid date.
Leap year is not considered*
Good Luck!
ValidDate<'0102'> // true
ValidDate<'0131'> // true
ValidDate<'1231'> // true
ValidDate<'0229'> // false
ValidDate<'0100'> // false
ValidDate<'0132'> // false
ValidDate<'1301'> // falseView on GitHub: https://tsch.js.org/9155
Change the following code to make the test cases pass (no type check errors).
π Lifetime-License is leaving on August 10, 2026
Get it now for $29
One-time payment. Lifetime access to all pro challenges.
The full code:
type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
type OneToNine = Exclude<Digit, '0'>
type Month = `0${OneToNine}` | '10' | '11' | '12'
type MonthWith30Days = '04' | '06' | '09' | '11'
type DayIn31 = `0${OneToNine}` | `1${Digit}` | `2${Digit}` | '30' | '31'
type DayIn30 = Exclude<DayIn31, '31'>
type DayIn28 = Exclude<DayIn31, '29' | '30' | '31'>
type ValidDate<T extends string> = T extends `${infer M1}${infer M2}${infer Day}`
? `${M1}${M2}` extends '02'
? Day extends DayIn28
? true
: false
: `${M1}${M2}` extends MonthWith30Days
? Day extends DayIn30
? true
: false
: `${M1}${M2}` extends Month
? Day extends DayIn31
? true
: false
: false
: falseThe strategy: instead of parsing numbers and comparing them (hard in the type system), we enumerate every legal value as a union of string literals and let extends do a set-membership test. Calendars are small enough that enumeration is the idiomatic move.
MMDD with template literal inference[object Object]When several infer placeholders appear back to back in a template literal, each one except the last matches exactly one character, and the last takes everything that remains. So for '0131':
// M1 = '0'
// M2 = '1'
// Day = '31' (the rest)That's exactly the shape we want: the first two characters form the month (reassembled as `${M1}${M2}`), and Day holds the remainder. Two edge cases fall out for free:
ValidDate<''>: a string with fewer than two characters can't match the pattern at all, so the conditional short-circuits to false.ValidDate<'01234'>: the pattern does match, but Day becomes '234', and no three-character string is a member of any day union, so the check fails naturally. No explicit length validation needed.A template literal over a finite union expands to a union of every combination:
[object Object]That one rule generates all the enumerations:
Month is `0${OneToNine}` ('01'β'09') plus '10' | '11' | '12'. Note OneToNine, not Digit; that's what rejects '00' as a month.DayIn31 covers '01'β'09', '10'β'19', '20'β'29', '30', '31'. Again the first block uses OneToNine, which is why ValidDate<'0100'> is false: day '00' is never generated.The shorter day ranges are then derived by subtraction rather than re-enumeration:
type DayIn30 = Exclude<DayIn31, '31'>
type DayIn28 = Exclude<DayIn31, '29' | '30' | '31'>Exclude distributes over the expanded union and filters members out. Deriving DayIn30 and DayIn28 from DayIn31 keeps a single source of truth: if the base enumeration is right, the derived ones are too.
The conditional chain checks the most restrictive months first:
'02'? February gets DayIn28 (leap years are explicitly out of scope, so '0229' is false).MonthWith30Days ('04' | '06' | '09' | '11')? Then DayIn30.Month? Then the full DayIn31.'13', '00', 'AB'β¦) is not a month at all β false.Order matters here: '02' and '04' are also members of Month, so if the general check came first, February would happily accept 31 days. Arrange cascades from special case to general case.
One more mechanic worth noting: Day extends DayIn28 ? true : false is a membership test against a union. Since Day is always a single concrete literal (never a union itself), there's no distribution to worry about. The check reads exactly like daySet.has(day).
'0229' β false: February capped at 28.'0430' β true but '0431' would be false: the 30-day month list is honored.'0100' and '1301' β false: zero-day and month-13 are excluded by construction of the unions, not by extra checks.'' and '01234' β false: wrong lengths are rejected by the inference pattern and the union membership respectively.Enumerate-and-match is a pattern you'll reuse whenever a string type has a small, closed set of valid values: hex colors, semver segments, time strings, and of course dates.
This challenge is originally from here.