Created
October 16, 2024 17:16
-
-
Save jasuperior/a005dbc80b79421418a0ec6f97172294 to your computer and use it in GitHub Desktop.
Rust Like Result Types in Typescript (with a twist)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Result, ResultType } from "./adt"; | |
import { list } from "./primitives"; | |
type Method<T extends any[] = any[], U = any> = U extends any[] | |
? (...arg: T) => Readonly<U> | |
: (...arg: T) => U; | |
type Chained<T extends readonly any[]> = T extends readonly [ | |
infer First extends Method, | |
infer Second extends Method, | |
...infer Rest | |
] | |
? [ChainPair<[First, Second]>, ...Chained<[Second, ...Rest]>] | |
: T extends [infer First extends Method, infer Second extends Method] | |
? [ChainPair<[First, Second]>, Second] | |
: T; | |
type ChainPair<T extends [Method, Method]> = ReturnType< | |
T[0] | |
> extends Parameters<T[1]> | |
? T[0] | |
: (...args: Parameters<T[0]>) => ParameterType<T[1]>; | |
type ParameterType<T extends Method> = | |
| Parameter<T> | |
| Promise<Parameter<T>> | |
| Result<ParameterType<T>>; | |
type LastElement<T extends any[]> = T extends [...infer First, infer Rest] | |
? Rest | |
: T[0]; | |
type Parameter<T extends Method> = Parameters<T>["length"] extends 1 | 0 | |
? Parameters<T>[0] | |
: Parameters<T>; | |
type HasPromise<T extends any[]> = T extends [infer First, ...infer Rest] | |
? First extends Promise<any> | ((...args: any[]) => Promise<any>) | |
? HasNever<First> extends true | |
? HasPromise<Rest> | |
: true | |
: HasPromise<Rest> | |
: false; | |
type HasNever<T> = T extends (...args: any[]) => never ? true : false; | |
type ComposedResult<T extends Method[]> = HasPromise<T> extends false | |
? Result<ReturnType<LastElement<T>>, Error, Exclude<ResultType, "pending">> | |
: Result<ReturnType<LastElement<T>>, Error, "pending">; | |
const compose = <const T extends Method[]>( | |
fns: Chained<T> | |
): ((...args: Parameters<T[0]>) => ComposedResult<T>) => { | |
return (...arg: Parameters<T[0]>) => { | |
//@ts-expect-error | |
return fns.reduce((result: Result, fn) => { | |
return result.map(fn); | |
}, Result.from(arg)) as ComposedResult<T>; | |
}; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export const list = <const T extends any[]>(...args: T) => args; | |
//... other primitives are supposed to go here, but lists are most important | |
//in order for the type checker to know what arguments to pass in a chain, since | |
// lists get flattened over arguments, it needs to know the constant type of returned arrays. | |
//... BASICALLY, arrays are equivilant to returning multiple return values. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Result } from "result"; | |
import {list} from "primitives"; | |
import {compose} from "compose"; | |
let process = compose([ | |
(x: number) => x * 2, // Multiply the input by 2 | |
(x: number) => `${x}px`, // Add some CSS flavor | |
async (x: string) => x.split("").reverse().join(""), // Flip it around, async-style | |
(x: string) => list(x, "auto"), // multiple return values | |
(x: string, y: string ) => Result.Ok({ x, y }) // Wrap it up in an Ok, like a cozy blanket... but you dont have to. | |
]); | |
let result = process(21); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export type ResultType = "ok" | "notok" | "pending"; | |
export type Defered<T> = T extends PromiseLike<infer U> ? Defered<U> : T; | |
export type ResultParams<T> = T extends any[] | readonly any[] ? T : [T]; | |
export class Result<T = any, E = Error, S extends ResultType = ResultType> { | |
private constructor( | |
private _value: T | E | Promise<T | Result<T, E>>, | |
private _type: S | |
) {} | |
get type(): ResultType { | |
return this._type; | |
} | |
isOk(): boolean { | |
return this._type === "ok"; | |
} | |
isNotOk(): boolean { | |
return this._type === "notok"; | |
} | |
isPending(): boolean { | |
return this._type === "pending"; | |
} | |
map<P extends ResultParams<T>, U>( | |
fn: (...args: P) => U | Promise<U> | |
): Result<U, E> { | |
if (this.isNotOk()) { | |
return Result.NotOk(this._value as E); | |
} | |
if (this.isPending()) { | |
return Result.Pending( | |
this.toPromise().then((result) => result.map(fn)) | |
); | |
} | |
try { | |
let args = Array.isArray(this._value) ? this._value : [this._value]; | |
//@ts-expect-error | |
const mappedValue = fn(...args); //not really sure why it hates this. | |
return Result.from(mappedValue); | |
} catch (error) { | |
return Result.NotOk(error as E); | |
} | |
} | |
get(): S extends "pending" ? Promise<T> : T { | |
if (this.isNotOk()) { | |
throw this._value; | |
} | |
if (this.isPending()) { | |
return this.toPromise().then((result) => | |
result.get() | |
) as S extends "pending" ? Promise<T> : T; | |
} | |
return this._value as S extends "pending" ? Promise<T> : T; | |
} | |
getOr(defaultValue: T): S extends "pending" ? Promise<T> : T { | |
if (this.isNotOk()) { | |
return defaultValue as S extends "pending" ? Promise<T> : T; | |
} | |
if (this.isPending()) { | |
return this.toPromise().then((result) => | |
result.getOr(defaultValue) | |
) as S extends "pending" ? Promise<T> : T; | |
} | |
return this._value as S extends "pending" ? Promise<T> : T; | |
} | |
toPromise(): Promise<Result<T, E>> { | |
if (this.isPending()) { | |
return (this._value as Promise<T | Result<T, E>>).then( | |
(value) => (value instanceof Result ? value : Result.Ok(value)), | |
(error) => Result.NotOk(error) | |
); | |
} | |
return Promise.resolve(this); | |
} | |
static of<Args extends any[], U, E>( | |
cb: (...args: Args) => U | |
): (...args: Args) => Result<U, E> { | |
return (...args: Args) => { | |
try { | |
return Result.from(cb(...args)); | |
} catch (e) { | |
return Result.NotOk(e as E); | |
} | |
}; | |
} | |
static from<T, E = Error>( | |
value: T | Result<T, E> | Promise<T | Result<T, E>> | |
): Result<T, E> { | |
if (value instanceof Promise) { | |
return Result.Pending(value); | |
} | |
return value instanceof Result ? value : Result.Ok(value); | |
} | |
static Ok<const T, E = Error>(value: T): Result<T, E, "ok"> { | |
return new Result<T, E, "ok">(value, "ok"); | |
} | |
static NotOk<const T, E = Error>(error: E): Result<T, E, "notok"> { | |
return new Result<T, E, "notok">(error, "notok"); | |
} | |
static Pending<const T, E = Error>( | |
promise: Promise<T | Result<T, E>> | |
): Result<T, E, "pending"> { | |
return new Result<T, E, "pending">(promise, "pending"); | |
} | |
} | |
export type Translation<T, U, E = never> = (value: T) => Result<U, E>; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment