Skip to content

Instantly share code, notes, and snippets.

@jasuperior
Created October 16, 2024 17:16
Show Gist options
  • Save jasuperior/a005dbc80b79421418a0ec6f97172294 to your computer and use it in GitHub Desktop.
Save jasuperior/a005dbc80b79421418a0ec6f97172294 to your computer and use it in GitHub Desktop.
Rust Like Result Types in Typescript (with a twist)
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>;
};
};
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.
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);
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