#13015•Medium

TimeUnion

Build a union of every valid date and time format string, like YYYY-MM-DD HH:mm:ss, by generating the ordered subsequences of the format parts.

A date picker whose format prop is typed as string happily accepts 'banana'.

TimeUnion is the type that fixes that. It is a union of every format string a time picker should accept, so the compiler can reject typos before the component ever renders. The upstream question sketches the shape:

[object Object]

Typing out all of those by hand is possible but dull, and it is easy to miss a combination. The point of the challenge is to generate them.

Challenge Instructions: TimeUnion

Medium

Generator a union that can suport time format type

For example, the function component named 'timePicker' with time params like value、onChange. we need a union type like 'YYYY-MM-DD HH:mm:ss'|'YYYY-MM-DD'|'HH:mm:ss'|'YYYY-MM HH:ss'|... not string, but can be ""

View on GitHub: https://tsch.js.org/13015

Change the following code to make the test cases pass (no type check errors).

ChallengeSolution
/* _____________ Your Code Here _____________ */

type TimeUnion = any

/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '../helpers'

type Has<T extends string, U extends string> =  U extends T ? true : false

type cases = [
  Expect<Equal<Has<TimeUnion, 'YYYY-MM-DD HH:mm:ss'>, true>>,
  Expect<Equal<Has<TimeUnion, 'YYYY-MM-DD'>, true>>,
  Expect<Equal<Has<TimeUnion, 'HH:mm:ss'>, true>>,
  Expect<Equal<Has<TimeUnion, 'YYYY-DD HH:mm:ss'>, true>>,
  Expect<Equal<Has<TimeUnion, 'YYYY-MM HH:mm:ss'>, true>>,
  Expect<Equal<Has<TimeUnion, 'YYYY HH:mm:ss'>, true>>,
  E

Pro Challenge

Get access to all 200+ challenges, including every medium, hard, and extreme one.

Monthly subscription.
Cancel anytime + 30-day money-back guarantee.

Detailed Explanation

The solution in full:

type Subsequences<Parts extends string[], Sep extends string> = Parts extends [
  infer Head extends string,
  ...infer Rest extends string[],
]
  ? Head | Subsequences<Rest, Sep> | `${Head}${Sep}${Subsequences<Rest, Sep>}`
  : never
 
type DatePart = Subsequences<['YYYY', 'MM', 'DD'], '-'>
type ClockPart = Subsequences<['HH', 'mm', 'ss'], ':'>
 
type TimeUnion = '' | DatePart | ClockPart | `${DatePart} ${ClockPart}`

The pattern behind the formats

Look at the accepted date formats: 'YYYY', 'YYYY-MM', 'YYYY-MM-DD', 'YYYY-DD', and a few more. Every one of them picks some of the parts 'YYYY', 'MM', 'DD', keeps them in that order, and glues them together with -. That is the definition of a subsequence. The clock side is the same idea with 'HH', 'mm', 'ss' and :, which is why 'HH:ss' is valid: you take the hour and the second and skip the minute.

So one generic can produce both sides if it takes the parts and the separator as arguments.

Walking a tuple with infer

Parts extends [infer Head extends string, ...infer Rest extends string[]] splits the tuple into its first element and the rest. The inline extends string on each infer is a constraint, not a check: it tells TypeScript that Head is a string so you may drop it into a template literal, and that Rest is still a tuple of strings so the recursive call type-checks. Without those constraints both would widen to unknown and the template literal below would be an error.

For each Head there are exactly three kinds of result, which is where the union in the true branch comes from:

Evaluated from the inside out:

type One = Subsequences<['DD'], '-'> // 'DD'
type Two = Subsequences<['MM', 'DD'], '-'> // 'MM' | 'DD' | 'MM-DD'
type Three = Subsequences<['YYYY', 'MM', 'DD'], '-'>
// 'YYYY' | 'MM' | 'DD' | 'MM-DD' | 'YYYY-MM' | 'YYYY-DD' | 'YYYY-MM-DD'

Seven date formats from three parts, and the same seven-member shape on the clock side.

Why the empty tuple returns never

The base case returns never, which looks like it should poison the third branch. It does the opposite, and that is the quiet trick here.

The true branch for Subsequences<['DD'], '-'> is 'DD', then never from the skip branch, then a template literal built on that same never. A union swallows never without a trace, and a template literal type whose slot is never collapses to never too, because there are no members left to substitute:

[object Object]

So the single-element case yields 'DD' and nothing else. If the base case had returned '' instead, you would get a stray 'DD-' with a dangling separator.

Combining the two halves

Template literal types distribute over unions in every slot. With seven date formats and seven clock formats, `${DatePart} ${ClockPart}` produces all 7 x 7 = 49 combined strings, among them 'YYYY-MM-DD HH:mm:ss' and 'YYYY HH:mm:ss'. Adding the date-only and clock-only variants and the empty string gives 64 members in total.

The '' member comes straight from the question text, which says the type may be the empty string. A picker with no value selected needs something to hold.

What the tests actually assert

The test file defines its own helper:

[object Object]

Has<TimeUnion, 'YYYY-MM-DD'> is true when 'YYYY-MM-DD' is assignable to TimeUnion, so each case is a membership check against one specific format.

Edge cases the tests cover

This challenge is originally from here.

Share this challenge

Related Challenges

Learn the Concepts

Become a TypeScript Pro

Track your progress through 200+ hands-on challenges. Free, sign in with GitHub.

Or start solving right away: explore all TypeScript challenges