Branded git path types
Skill kjuhwa/skills-hub/skills/typescript/branded-git-path-types
Self-correcting knowledge corpus for Claude Code — 9 stable shape clusters, bias-correction pipeline baked into contribution flow. 47 papers, 45 techniques, 1.1k skills.
npx -y skills add kjuhwa/skills-hub --skill branded-git-path-typesAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use branded string types (`RepoPath`, `WorktreePath`, `BranchName`) with constructor helpers to prevent accidentally passing one string primitive where another is expected.
SKILL.md
5.1 KB, ~1.1k tokens by cl100k_base, as published. Nobody here has run it
Branded String Types for Git Paths and Branch Names
When to use
- Your codebase has several kinds of string primitives that look the same but are semantically different:
RepoPath(canonical git repo root),WorktreePath(git worktree directory),BranchName,CodebaseId,ConversationId, etc. - You keep hitting bugs where someone passes a branch name where a path is expected (or worse, a path where a branch name is expected — the shell swallows it silently).
- You want the compiler to enforce the distinction with minimal runtime cost.
Steps
-
Declare private brand symbols and intersect with
string:declare const REPO_PATH_BRAND: unique symbol; declare const BRANCH_NAME_BRAND: unique symbol; declare const WORKTREE_PATH_BRAND: unique symbol; export type RepoPath = string & { readonly [REPO_PATH_BRAND]: true }; export type BranchName = string & { readonly [BRANCH_NAME_BRAND]: true }; export type WorktreePath = string & { readonly [WORKTREE_PATH_BRAND]: true };The brands never exist at runtime (phantom types). The
string &intersection means you can pass aRepoPathanywhere astringis expected (great for library APIs) but cannot pass a plainstringwhereRepoPathis required. -
Provide one cast helper per brand, which validates and asserts the type:
export function toRepoPath(path: string): RepoPath { if (!path) throw new Error('RepoPath cannot be empty'); return path as RepoPath; } export function toBranchName(name: string): BranchName { if (!name) throw new Error('BranchName cannot be empty'); return name as BranchName; }Runtime validation inside the helper (empty-string rejection, maybe format regex) makes the cast safer than raw
as RepoPath. The empty-string check alone has caught real bugs. -
Design function signatures to consume the branded types, forcing callers to go through the helpers:
async function isBranchMerged( repoPath: RepoPath, branchName: BranchName, mainBranch: BranchName, ): Promise<boolean> { … }Now
isBranchMerged('my-branch', repoPath, main)is a compile error — you physically cannot get the argument order wrong. -
Union helpers for functions that accept multiple kinds:
async function hasUncommittedChanges( workingPath: RepoPath | WorktreePath ): Promise<boolean> { … }|-union the brands when a function works on either. -
Pair with discriminated-union error results:
export type GitResult<T> = { ok: true; value: T } | { ok: false; error: GitError }; export type GitError = | { code: 'not_a_repo'; path: string } | { code: 'permission_denied'; path: string } | { code: 'branch_not_found'; branch: string } | { code: 'no_space'; path: string } | { code: 'unknown'; message: string };At the package boundary, return a
GitResult<T>instead of throwing raw strings. Callers pattern-match oncodeand get typedpath/branchfields.
Counter / Caveats
- Brands do not survive JSON round-trips. Deserializing from the DB gives you a
string; you must go back throughtoRepoPath(row.path)to rebrand. - Brands are not runtime-distinguishable.
typeof repoPath === 'string'is true. If you need runtime distinction (e.g. structured logging), carry the kind explicitly (e.g.{ kind: 'repo', path }). - Too many brands becomes noise. Good candidates: identifiers from external systems (ids, handles, paths), and values where accidental swap is catastrophic. Bad candidates: every internal function parameter.
- Helpers should throw on invalid input rather than returning
Option<T>— at the boundary you either have a valid value or you don't want to continue. - If you're in strict mode, the
unique symboldeclaration trick requiresskipLibCheck: falseto stay robust. Consider a named const symbol if you hit library-type conflicts.
Evidence
packages/git/src/types.ts:1-26: full declaration ofREPO_PATH_BRAND,BRANCH_NAME_BRAND,WORKTREE_PATH_BRANDplus constructorstoRepoPath,toBranchName,toWorktreePathwith empty-string guards.- Discriminated-union
GitResult<T>+GitErrorattypes.ts:29-37. - Consumers across the codebase:
packages/git/src/branch.ts(every signature usesRepoPath/BranchName),packages/isolation/src/resolver.ts,packages/core/src/services/cleanup-service.ts. - Example union at
packages/git/src/branch.ts:118:hasUncommittedChanges(workingPath: RepoPath | WorktreePath). - Commit SHA: d89bc767d291f52687beea91c9fcf155459be0d9.
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.