@@ -125,22 +125,39 @@ export type OrgListConfig<TEntity, TWithOrg> = ListCommandMeta & {
125125// Mode handler types
126126// ---------------------------------------------------------------------------
127127
128- /** A single dispatch handler — a zero-argument async function. */
129- export type ModeHandler = ( ) => Promise < void > ;
128+ /** Extract a specific variant from the {@link ParsedOrgProject} union by its `type` discriminant. */
129+ export type ParsedVariant < T extends ParsedOrgProject [ "type" ] > = Extract <
130+ ParsedOrgProject ,
131+ { type : T }
132+ > ;
133+
134+ /**
135+ * A dispatch handler that receives the correctly-narrowed parsed variant.
136+ * The dispatcher guarantees `parsed.type` matches the handler key, so
137+ * callers can safely access variant-specific fields (e.g. `.org`, `.projectSlug`)
138+ * without runtime checks or manual casts.
139+ */
140+ export type ModeHandler <
141+ T extends ParsedOrgProject [ "type" ] = ParsedOrgProject [ "type" ] ,
142+ > = ( parsed : ParsedVariant < T > ) => Promise < void > ;
130143
131144/**
132145 * Complete handler map — one handler per parsed target type.
133- * Keys match `ParsedOrgProject["type"]` .
146+ * Each handler receives the corresponding { @link ParsedVariant} .
134147 */
135- export type ModeHandlerMap = Record < ParsedOrgProject [ "type" ] , ModeHandler > ;
148+ export type ModeHandlerMap = {
149+ [ K in ParsedOrgProject [ "type" ] ] : ModeHandler < K > ;
150+ } ;
136151
137152/**
138153 * Partial handler map for overriding specific dispatch modes.
139154 *
140155 * Provide only the modes you need to customise; the rest will use
141156 * the default handlers from {@link buildDefaultHandlers}.
142157 */
143- export type ModeOverrides = Partial < ModeHandlerMap > ;
158+ export type ModeOverrides = {
159+ [ K in ParsedOrgProject [ "type" ] ] ?: ModeHandler < K > ;
160+ } ;
144161
145162// ---------------------------------------------------------------------------
146163// Type guard
@@ -580,30 +597,34 @@ type DefaultHandlerOptions<TEntity, TWithOrg> = {
580597 stdout : Writer ;
581598 cwd : string ;
582599 flags : BaseListFlags ;
583- parsed : ParsedOrgProject ;
584600} ;
585601
586602/**
587603 * Build the default `ModeHandlerMap` for the given config and request context.
588604 *
605+ * Each handler receives the correctly-narrowed {@link ParsedVariant} for its mode,
606+ * so it can access variant-specific fields (`.org`, `.projectSlug`) without casts.
607+ *
589608 * If `config` is only {@link ListCommandMeta} (not a full {@link OrgListConfig}),
590609 * each default handler throws when invoked — this only happens if a mode is not
591610 * covered by the caller's overrides, which would be a programming error.
592611 */
593612function buildDefaultHandlers < TEntity , TWithOrg > (
594613 options : DefaultHandlerOptions < TEntity , TWithOrg >
595614) : ModeHandlerMap {
596- const { config, stdout, cwd, flags, parsed } = options ;
615+ const { config, stdout, cwd, flags } = options ;
597616
598- const notSupported =
599- ( mode : string ) : ModeHandler =>
600- ( ) =>
617+ function notSupported < T extends ParsedOrgProject [ "type" ] > (
618+ mode : string
619+ ) : ModeHandler < T > {
620+ return ( ) =>
601621 Promise . reject (
602622 new Error (
603623 `No handler for '${ mode } ' mode in '${ config . commandPrefix } '. ` +
604624 "Provide a full OrgListConfig or an override for this mode."
605625 )
606626 ) ;
627+ }
607628
608629 if ( ! isOrgListConfig ( config ) ) {
609630 // Metadata-only config — all modes must be overridden by the caller
@@ -615,49 +636,47 @@ function buildDefaultHandlers<TEntity, TWithOrg>(
615636 } ;
616637 }
617638
618- const contextKey = buildOrgContextKey (
619- parsed . type === "org-all" ? parsed . org : ""
620- ) ;
621-
622639 return {
623640 "auto-detect" : ( ) => handleAutoDetect ( config , stdout , cwd , flags ) ,
624641
625- explicit : ( ) => {
642+ explicit : ( parsed ) => {
626643 if ( config . listForProject ) {
627644 return handleExplicitProject ( {
628645 config,
629646 stdout,
630- org : parsed . type === "explicit" ? parsed . org : "" ,
631- project : parsed . type === "explicit" ? parsed . project : "" ,
647+ org : parsed . org ,
648+ project : parsed . project ,
632649 flags,
633650 } ) ;
634651 }
635652 // No project-scoped API — fall back to org listing with a note
636653 return handleExplicitOrg ( {
637654 config,
638655 stdout,
639- org : parsed . type === "explicit" ? parsed . org : "" ,
656+ org : parsed . org ,
640657 flags,
641658 noteOrgScoped : true ,
642659 } ) ;
643660 } ,
644661
645- "project-search" : ( ) =>
646- handleProjectSearch (
647- config ,
648- stdout ,
649- parsed . type === "project-search" ? parsed . projectSlug : "" ,
650- flags
651- ) ,
662+ "project-search" : ( parsed ) =>
663+ handleProjectSearch ( config , stdout , parsed . projectSlug , flags ) ,
652664
653- "org-all" : ( ) => {
654- const org = parsed . type === " org-all" ? parsed . org : "" ;
665+ "org-all" : ( parsed ) => {
666+ const contextKey = buildOrgContextKey ( parsed . org ) ;
655667 const cursor = resolveOrgCursor (
656668 flags . cursor ,
657669 config . paginationKey ,
658670 contextKey
659671 ) ;
660- return handleOrgAll ( { config, stdout, org, flags, contextKey, cursor } ) ;
672+ return handleOrgAll ( {
673+ config,
674+ stdout,
675+ org : parsed . org ,
676+ flags,
677+ contextKey,
678+ cursor,
679+ } ) ;
661680 } ,
662681 } ;
663682}
@@ -683,10 +702,13 @@ export type DispatchOptions<TEntity = unknown, TWithOrg = unknown> = {
683702} ;
684703
685704/**
686- * Validate the cursor flag and dispatch to the correct handler.
705+ * Validate the cursor flag and dispatch to the correct mode handler.
687706 *
688707 * Merges default handlers with caller-provided overrides using
689- * `{ ...defaults, ...overrides }`, then invokes `handlers[parsed.type]()`.
708+ * `{ ...defaults, ...overrides }`, then invokes `handlers[parsed.type](parsed)`.
709+ * Each handler receives the correctly-narrowed {@link ParsedVariant} for its mode,
710+ * eliminating the need for `Extract<>` casts at call sites.
711+ *
690712 * This is the single entry point for all org-scoped list commands.
691713 */
692714export async function dispatchOrgScopedList < TEntity , TWithOrg > (
@@ -704,8 +726,12 @@ export async function dispatchOrgScopedList<TEntity, TWithOrg>(
704726 ) ;
705727 }
706728
707- const defaults = buildDefaultHandlers ( { config, stdout, cwd, flags, parsed } ) ;
729+ const defaults = buildDefaultHandlers ( { config, stdout, cwd, flags } ) ;
708730 const handlers : ModeHandlerMap = { ...defaults , ...overrides } ;
709731
710- await handlers [ parsed . type ] ( ) ;
732+ // TypeScript cannot prove that `parsed` narrows to `ParsedVariant<typeof parsed.type>`
733+ // through the indexed access `handlers[parsed.type]`, but the handler map guarantees
734+ // each key maps to a handler expecting exactly that variant.
735+ // biome-ignore lint/suspicious/noExplicitAny: safe — dispatch guarantees type match
736+ await ( handlers [ parsed . type ] as ModeHandler < any > ) ( parsed ) ;
711737}
0 commit comments