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.
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).
/* _____________ 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>>,
EGet access to all 200+ challenges, including every medium, hard, and extreme one.
Monthly subscription.
Cancel anytime + 30-day money-back guarantee.
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}`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.
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:
Head alone, taking nothing from the restSubsequences<Rest, Sep>, skipping Head entirely`${Head}${Sep}${Subsequences<Rest, Sep>}`, keeping Head and prefixing it onto everything the rest producesEvaluated 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.
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.
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.
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.
'YYYY-DD HH:mm:ss' skips the month. Subsequences keep the original order but not every part, so a gap in the middle is expected rather than special-cased.'YYYY-MM-DD HH:ss' skips the minutes on the clock side, the mirror image of the previous case.'HH:mm:ss' and 'YYYY-MM-DD' arrive without a counterpart, which is why the clock-only and date-only members are listed separately in the final union.Has<TimeUnion, string> must be false. Plain string is only assignable to TimeUnion if TimeUnion is string or any, so this case is what rules out the lazy answer and forces the enumeration.This challenge is originally from here.
Track your progress through 200+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges