Optique는 TypeScript를 위한 타입 안전 조합형 CLI 파서 라이브러리로, “Parse, don't validate” 원칙에 따라 런타임 유효성 검사 대신 파서 구조 자체로 제약을 표현합니다. 이 발표에서는 의존적 옵션, 상호 배타적 옵션, 태그된 공용체라는 세 가지 핵심 패턴을 통해 Optique의 접근 방식을 소개합니다.
unknown = . ( ); if (typeof !== "object" || === null) { throw new (); } else if (!("name" in ) || typeof . !== "string") { throw new (); } else if (!("age" in ) || typeof . !== "number") { throw new (); } else if ( . < 0) { throw new (); } // … data JSON parse response data data Error data data name Error data data age Error data age Error const = . ({ : . (), : . (). () }); const = . ( . ( )); userSchema z object name z string age z number nonnegative data userSchema parse JSON parse response
실제 제약을 반영하지 못함 런타임에만 오류 발견 Optique 방식 --port 옵션은 --server 와 함께만 사용 가능한 경우 const opts = parseArgs(process.argv); if (!opts.server && opts.port) { throw new Error("--port는 --server가 필요합니다"); } if (opts.server && !opts.port) { opts.port = 3000; } const = ( ({ : ("--server"), : ("--port", ()), : ("--workers", ()) }), { : false } as ); type = <typeof >; config withDefault object server flag port option integer workers option integer server const Config InferValue config
if ( . ) { // server가 true일 때만 port 접근 가능 . (`Server on port ${ . }`); } else { // server가 false일 때 port 접근 시도 . ( .port); // ❌ 컴파일 오류! } result Config run config result server console log result port console log result Property 'port' does not exist on type '{ readonly server: false; }'.
을 하나의 선택으로 변환 실제로는 정확히 하나만 true 여야 하는데 타입이 이를 표현 못함 Optique 방식 --json / --yaml / --xml 옵션 중 하나만 선택 가능한 경우 if ((opts.json ? 1 : 0) + (opts.yaml ? 1 : 0) + (opts.xml ? 1 : 0) !== 1) { throw new Error("출력 형식을 하나만 선택하세요."); } let format: "json" | "yaml" | "xml"; if (opts.json) format = "json"; else if (opts.yaml) format = "yaml"; else if (opts.xml) format = "xml"; else throw new Error("실제로는 여기로 오면 안 됨."); const = ( ( ("--json"), () => "json" as ), ( ("--yaml"), () => "yaml" as ), ( ("--xml"), () => "xml" as ), ); type = <typeof >; format or map flag const map flag const map flag const Format InferValue format
const = ( ( ("--json"), () => "json" as ), ( ("--yaml"), () => "yaml" as ), ( ("--xml"), () => "xml" as ), ); const : "json" | "yaml" | "xml" = ( ); // switch문으로 처리 switch ( ) { case "json": return . ( ); case "yaml": return ( ); case "xml": return ( ); // 모든 경우를 처리하지 않으면 타입 오류 } format or map flag const map flag const map flag const result run format result JSON stringify data toYaml data toXml data
워커에서도 사용 가능 파서 컴비네이터, 값 파서, 메시지 시스템 @optique/run 프로세스 통합 ( process.argv , process.exit() ) 터미널 색상/너비 자동 감지 path() 값 파서 @optique/temporal TC39 Temporal API 값 파서 instant() , duration() , plainDate() 등 @optique/zod · @optique/valibot Zod/Valibot 스키마를 값 파서로 변환 기존 검증 로직 재사용 zod(z.string().email()) , valibot(v.pipe(v.string(), v.email()))
>): < > ( : ): < > // TypeScript 타입 추론을 위한 팬텀 타입들 readonly : readonly [] readonly : readonly [] } 두 개의 타입 파라미터 TValue : 최종 결과 타입 (사용자가 받는 값) TState : 파싱 중 상태 타입 (내부 구현) 중간 상태를 추적하여 "시도했지만 실패"와 "시도 안 함" 구분 Parser TValue TState parse context ParserContext TState ParserResult TState complete state TState ValueParserResult TValue $valueType TValue $stateType TState
예시 왜 배열을 사용할까? TypeScript의 타입 추론 한계 우회 런타임 값은 빈 배열 [] 이지만, 타입만 중요 $valueType[number] 로 타입 추출 Haskell의 Data.Proxy.Proxy 타입과 유사한 역할 type < extends <unknown, unknown>> = ["$valueType"][number]; InferValue T Parser T const = ("--port", ()); type = <typeof >; portParser option integer PortType InferValue portParser
서브커맨드 최우선 flag() 10 옵션이 인자보다 우선 option() 10 옵션이 인자보다 우선 argument() 5 위치 인자는 나중에 constant() 0 입력 소비 안 함 필요한 이유 ( ( ()), ("--file", ()), ) 우선 순위 없으면 argument() 가 --file 을 먼저 소비! or argument string option string
("--port")}는 \ ${ ("NUMBER")} 값이 필요합니다.`; MessageTerm 타입들 타입 스타일 예시 optionName 이탤릭 --verbose metavar 볼드 NUMBER value 녹색 "42" envVar 볼드+밑줄 PATH 왜 구조화된 메시지인가? 내용과 표현의 분리 의미론적 구성 요소 (옵션 이름, 값, 메타 변수 등) 터미널 환경에 맞는 자동 렌더링 에러 메시지와 도움말의 일관성 보장 msg message optionName metavar
( ("--port", "-p", ({ : "PORT" }), { : `서버 포트 번호`, }), 8080, ), : ("--host", ({ : "HOST" }), { : `서버 호스트 주소`, }), }); const = ( ); const = ("myapp", !, { : true, : true, }); . ( ); 출력 예시 파서 구조에서 자동 추출 기본값 표시 그룹화 지원 중간 표현인 DocPage 제공 ( man 페이지 등 다른 형식으로 변환 가능) ← 실제 사용시에는 도움말 출력을 직접 안 해도 됨! parser object port withDefault option integer metavar description message host option string metavar description message doc getDocPage parser rendered formatDocPage doc colors showDefault console log rendered Usage: myapp [--port/-p PORT] --host HOST --port, -p PORT 서버 포트 번호 [8080] --host HOST 서버 호스트 주소