RustのResultおよびOption型をTypeScriptに移植しました
原題: I ported Rust's Result and Option types to TypeScript
分析結果
- カテゴリ
- AI
- 重要度
- 59
- トレンドスコア
- 21
- 要約
- RustのResult型とOption型をTypeScriptに移植したことについての報告です。これにより、TypeScriptでもRustのようなエラーハンドリングやオプショナルな値の管理が可能になります。移植の過程や実装の詳細、TypeScriptでの利点についても触れています。
- キーワード
If you've used Rust, you know how nice it is to have Result<T, E> and Option<T> types that make failure and absence explicit instead of something that blows up at runtime. I wanted that in TypeScript, so I built results-ts . It's not a novel idea. There are a lot of similar libraries out there. But I wanted something that felt close to the real Rust API, worked with async code without being awkward, and didn't cut corners on type safety. Installation npm install results-ts # or bun add results-ts # or pnpm add results-ts # or deno add results-ts # or yarn add results-ts Result Result<T, E> is either Ok(value) or Err(error) . You get a concrete type for both sides, which means TypeScript can actually help you when you handle them. import { Ok , Err } from ' results-ts ' ; const parseUserId = ( id : string ) => { const parsed = parseInt ( id , 10 ); if ( isNaN ( parsed )) return Err ({ code : ' INVALID_INPUT ' , message : ' ID must be a valid number ' } as const ); if ( parsed <= 0 ) return Err ({ code : ' INVALID_ID ' , message : ' ID must be positive ' } as const ); return Ok ( parsed ); }; From there you can chain operations. .map() transforms the Ok value, .andThen() lets you sequence two fallible operations, and .match() handles both branches: const fetchUser = ( id : number ) => { if ( id === 13 ) return Err ({ code : ' NOT_FOUND ' , message : ' User not found ' } as const ); return Ok ({ id , name : ' Alice ' , role : ' admin ' }); }; const message = parseUserId ( ' 10 ' ) . map (( id ) => id + 3 ) . andThen ( fetchUser ) . match ({ Ok : ( user ) => `Welcome, ${ user . role } ${ user . name } !` , Err : ( error ) => { if ( error . code === ' NOT_FOUND ' ) return `Database Error: ${ error . message } ` ; return `Validation Error: ${ error . message } ` ; } }); Because as const is used on the error objects, TypeScript knows every possible code value and will complain if you miss one in .match() . Option Option<T> is Some(value) or None() . It's basically T | null | undefined but with methods on it, so you don't have to break out of the chain to check for emptiness. import { Some , None } from ' results-ts ' ; const parseNickname = ( nickname ?: string ) => { if ( ! nickname ) return None (); const trimmed = nickname . trim (); return trimmed . length > 0 ? Some ( trimmed ) : None (); }; const displayName = parseNickname ( ' Ada ' ) . map (( name ) => name . toUpperCase ()) . match ({ Some : ( name ) => name , None : () => ' ANONYMOUS ' }); console . log ( displayName ); // "ADA" Wrapping code that throws You can't always rewrite everything. catchUnwind wraps a throwing function so it returns a Result instead: import { catchUnwind } from ' results-ts ' ; const safeParse = catchUnwind ( JSON . parse , ( thrown ) => thrown instanceof Error ? thrown . message : ' parse error ' ); safeParse ( ' {"a":1} ' ); // Ok({ a: 1 }) safeParse ( ' {bad ' ); // Err('Unexpected token ...') The second argument maps the thrown value to an error type of your choice. Leave it out and the error type becomes unknown , since JS lets you throw anything, that's the honest type. For async functions there's catchUnwindAsync , which catches both sync throws and rejected promises. Async AsyncResult<T, E> and AsyncOption<T> are promise wrappers that keep the same chainable API. No need to await in the middle of a chain just to call .map() . import { catchUnwindAsync } from ' results-ts ' ; const safeFetch = catchUnwindAsync ( async ( url : string ) => { const res = await fetch ( url ); if ( ! res . ok ) throw new Error ( `HTTP ${ res . status } ` ); return res . json (); }, ( thrown ) => ( thrown instanceof Error ? thrown . message : ' request failed ' ) ); const result = await safeFetch ( ' https://api.example.com ' ); // ^? AsyncResult<unknown, string> On .unwrap() and panics Methods like .unwrap() and .expect() deliberately "panic" (throw), same as Rust. The idea is they're for cases where you're certain something is Ok , and if it isn't, you want a loud failure rather than a silent wrong value. They're not for normal error handling. If a non-panic error comes out of the library, that's a bug on the call site (garbage data, type system bypass, etc.), not something to catch. Performance Overhead is minimal, full numbers in BENCHMARKS.md . Browser / no bundler It's an ES module, so you can import it straight from a CDN: <script type= "module" > import { Ok } from ' https://unpkg.com/results-ts/dist/index.js ' ; console . log ( Ok ( 1 ). map (( x ) => x + 1 ). unwrap ()); // 2 </script> That's about it. If you try it out, feedback is welcome, issues and PRs are open on GitHub . If you've used Rust, you know how nice it is to have Result<T, E> and Option<T> types that make failure and absence explicit instead of something that blows up at runtime. I wanted that in TypeScript, so I built results-ts . It's not a novel idea. There are a lot of similar libraries out there. But I wanted something that felt close to the real Rust API, worked with async code without being awkward, and didn't cut corners on type safety. Installation npm install results-ts # or bun add results-ts # or pnpm add results-ts # or deno add results-ts # or yarn add results-ts Result Result<T, E> is either Ok(value) or Err(error) . You get a concrete type for both sides, which means TypeScript can actually help you when you handle them. import { Ok , Err } from ' results-ts ' ; const parseUserId = ( id : string ) => { const parsed = parseInt ( id , 10 ); if ( isNaN ( parsed )) return Err ({ code : ' INVALID_INPUT ' , message : ' ID must be a valid number ' } as const ); if ( parsed <= 0 ) return Err ({ code : ' INVALID_ID ' , message : ' ID must be positive ' } as const ); return Ok ( parsed ); }; From there you can chain operations. .map() transforms the Ok value, .andThen() lets you sequence two fallible operations, and .match() handles both branches: const fetchUser = ( id : number ) => { if ( id === 13 ) return Err ({ code : ' NOT_FOUND ' , message : ' User not found ' } as const ); return Ok ({ id , name : ' Alice ' , role : ' admin ' }); }; const message = parseUserId ( ' 10 ' ) . map (( id ) => id + 3 ) . andThen ( fetchUser ) . match ({ Ok : ( user ) => `Welcome, ${ user . role } ${ user . name } !` , Err : ( error ) => { if ( error . code === ' NOT_FOUND ' ) return `Database Error: ${ error . message } ` ; return `Validation Error: ${ error . message } ` ; } }); Because as const is used on the error objects, TypeScript knows every possible code value and will complain if you miss one in .match() . Option Option<T> is Some(value) or None() . It's basically T | null | undefined but with methods on it, so you don't have to break out of the chain to check for emptiness. import { Some , None } from ' results-ts ' ; const parseNickname = ( nickname ?: string ) => { if ( ! nickname ) return None (); const trimmed = nickname . trim (); return trimmed . length > 0 ? Some ( trimmed ) : None (); }; const displayName = parseNickname ( ' Ada ' ) . map (( name ) => name . toUpperCase ()) . match ({ Some : ( name ) => name , None : () => ' ANONYMOUS ' }); console . log ( displayName ); // "ADA" Wrapping code that throws You can't always rewrite everything. catchUnwind wraps a throwing function so it returns a Result instead: import { catchUnwind } from ' results-ts ' ; const safeParse = catchUnwind ( JSON . parse , ( thrown ) => thrown instanceof Error ? thrown . message : ' parse error ' ); safeParse ( ' {"a":1} ' ); // Ok({ a: 1 }) safeParse ( ' {bad ' ); // Err('Unexpected token ...') The second argument maps the thrown value to an error type of your choice. Leave it out and the error type becomes unknown , since JS lets you throw anything, that's the honest type. For async functions there's catchUnwindAsync , which catches both sync throws and rejected promises. Async AsyncResult<T, E> and AsyncOption<T> are promise wrappers that keep the same chainable API. No need to await in the middle of a chain just to call .map() . import { catchUnwindAsync } from ' results-ts ' ; const safeFetch = catchUnwindAsync ( async ( url : string ) => { const res = await fetch ( url ); if ( ! res . ok ) throw new Error ( `HTTP ${ res . status } ` ); return res . json (); }, ( thrown ) => ( thrown instanceof Error ? thrown . message : ' request failed ' ) ); const result = await safeFetch ( ' https://api.example.com ' ); // ^? AsyncResult<unknown, string> On .unwrap() and panics Methods like .unwrap() and .expect() deliberately "panic" (throw), same as Rust. The idea is they're for cases where you're certain something is Ok , and if it isn't, you want a loud failure rather than a silent wrong value. They're not for normal error handling. If a non-panic error comes out of the library, that's a bug on the call site (garbage data, type system bypass, etc.), not something to catch. Performance Overhead is minimal, full numbers in BENCHMARKS.md . Browser / no bundler It's an ES module, so you can import it straight from a CDN: <script type= "module" > import { Ok } from ' https://unpkg.com/results-ts/dist/index.js ' ; console . log ( Ok ( 1 ). map (( x ) => x + 1 ). unwrap ()); // 2 </script> That's about it. If you try it out, feedback is welcome, issues and PRs are open on GitHub .