mirror of
https://github.com/microsoft/vscode.git
synced 2026-06-01 05:04:59 +01:00
util: result: extend functionality with some common utils (#3750)
This commit is contained in:
committed by
GitHub
parent
b77056774f
commit
b86d986cbd
@@ -3,7 +3,9 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export type Result<T, K> = ResultOk<T> | ResultError<K>;
|
||||
import * as errors from './errors';
|
||||
|
||||
export type Result<T, E> = ResultOk<T> | ResultError<E>;
|
||||
|
||||
export namespace Result {
|
||||
|
||||
@@ -11,13 +13,29 @@ export namespace Result {
|
||||
return new ResultOk(value);
|
||||
}
|
||||
|
||||
export function error<K>(value: K): ResultError<K> {
|
||||
export function error<E>(value: E): ResultError<E> {
|
||||
return new ResultError(value);
|
||||
}
|
||||
|
||||
export function fromString(errorMessage: string): ResultError<Error> {
|
||||
return Result.error(new Error(errorMessage));
|
||||
}
|
||||
|
||||
export function tryWith<T>(f: () => T): Result<T, Error> {
|
||||
try {
|
||||
return Result.ok(f());
|
||||
} catch (err) {
|
||||
return Result.error(errors.fromUnknown(err));
|
||||
}
|
||||
}
|
||||
|
||||
export async function tryWithAsync<T>(f: () => Promise<T>): Promise<Result<T, Error>> {
|
||||
try {
|
||||
return Result.ok(await f());
|
||||
} catch (err) {
|
||||
return Result.error(errors.fromUnknown(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,14 +45,35 @@ export namespace Result {
|
||||
class ResultOk<T> {
|
||||
constructor(readonly val: T) { }
|
||||
|
||||
map<K>(f: (result: T) => K) {
|
||||
map<U>(f: (value: T) => U): ResultOk<U> {
|
||||
return new ResultOk(f(this.val));
|
||||
}
|
||||
|
||||
flatMap<K>(f: (result: T) => Result<K, never>) {
|
||||
mapError<E2>(_f: (error: never) => E2): ResultOk<T> {
|
||||
return this;
|
||||
}
|
||||
|
||||
flatMap<U, E2>(f: (value: T) => Result<U, E2>): Result<U, E2> {
|
||||
return f(this.val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the contained ok value.
|
||||
* @throws if this is an error (which is impossible for `ResultOk`,
|
||||
* but provided for use on the `Result<T, E>` union type).
|
||||
*/
|
||||
unwrap(): T {
|
||||
return this.val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the contained ok value, or the provided default if this
|
||||
* is an error.
|
||||
*/
|
||||
unwrapOr(_defaultValue: T): T {
|
||||
return this.val;
|
||||
}
|
||||
|
||||
isOk(): this is ResultOk<T> {
|
||||
return true;
|
||||
}
|
||||
@@ -48,24 +87,46 @@ class ResultOk<T> {
|
||||
* To instantiate a ResultOk, use `Result.ok(value)`.
|
||||
* To instantiate a ResultError, use `Result.error(value)`.
|
||||
*/
|
||||
class ResultError<K> {
|
||||
class ResultError<E> {
|
||||
constructor(
|
||||
public readonly err: K,
|
||||
public readonly err: E,
|
||||
) { }
|
||||
|
||||
map(f: unknown) {
|
||||
map<U>(_f: (value: never) => U): ResultError<E> {
|
||||
return this;
|
||||
}
|
||||
|
||||
flatMap(f: unknown) {
|
||||
mapError<E2>(f: (error: E) => E2): ResultError<E2> {
|
||||
return new ResultError(f(this.err));
|
||||
}
|
||||
|
||||
flatMap<U, E2>(_f: (value: never) => Result<U, E2>): ResultError<E> {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Always throws since this is an error result.
|
||||
* @throws The contained error value (wrapped in Error if not already one).
|
||||
*/
|
||||
unwrap(): never {
|
||||
if (this.err instanceof Error) {
|
||||
throw this.err;
|
||||
}
|
||||
throw errors.fromUnknown(this.err);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the provided default value since this is an error.
|
||||
*/
|
||||
unwrapOr<T>(defaultValue: T): T {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
isOk(): this is ResultOk<never> {
|
||||
return false;
|
||||
}
|
||||
|
||||
isError(): this is ResultError<K> {
|
||||
isError(): this is ResultError<E> {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { Result } from '../result';
|
||||
|
||||
describe('Result', () => {
|
||||
|
||||
describe('Result.ok', () => {
|
||||
it('creates an ok result', () => {
|
||||
const r = Result.ok(42);
|
||||
expect(r.isOk()).toBe(true);
|
||||
expect(r.isError()).toBe(false);
|
||||
expect(r.val).toBe(42);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Result.error', () => {
|
||||
it('creates an error result', () => {
|
||||
const r = Result.error('bad');
|
||||
expect(r.isOk()).toBe(false);
|
||||
expect(r.isError()).toBe(true);
|
||||
expect(r.err).toBe('bad');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Result.fromString', () => {
|
||||
it('creates an error result with an Error instance', () => {
|
||||
const r = Result.fromString('something failed');
|
||||
expect(r.isError()).toBe(true);
|
||||
expect(r.err).toBeInstanceOf(Error);
|
||||
expect(r.err.message).toBe('something failed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Result.tryWith', () => {
|
||||
it('returns ok when the function succeeds', () => {
|
||||
const r = Result.tryWith(() => 10 + 5);
|
||||
expect(r.isOk()).toBe(true);
|
||||
if (r.isOk()) {
|
||||
expect(r.val).toBe(15);
|
||||
}
|
||||
});
|
||||
|
||||
it('returns error when the function throws', () => {
|
||||
const r = Result.tryWith(() => { throw new Error('boom'); });
|
||||
expect(r.isError()).toBe(true);
|
||||
if (r.isError()) {
|
||||
expect(r.err.message).toBe('boom');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Result.tryWithAsync', () => {
|
||||
it('returns ok when the async function resolves', async () => {
|
||||
const r = await Result.tryWithAsync(async () => 99);
|
||||
expect(r.isOk()).toBe(true);
|
||||
if (r.isOk()) {
|
||||
expect(r.val).toBe(99);
|
||||
}
|
||||
});
|
||||
|
||||
it('returns error when the async function rejects', async () => {
|
||||
const r = await Result.tryWithAsync(async () => { throw new Error('async boom'); });
|
||||
expect(r.isError()).toBe(true);
|
||||
if (r.isError()) {
|
||||
expect(r.err.message).toBe('async boom');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('map', () => {
|
||||
it('transforms the value of an ok result', () => {
|
||||
const r = Result.ok(3).map(x => x * 2);
|
||||
expect(r.isOk()).toBe(true);
|
||||
if (r.isOk()) {
|
||||
expect(r.val).toBe(6);
|
||||
}
|
||||
});
|
||||
|
||||
it('is a no-op on an error result', () => {
|
||||
const r: Result<number, string> = Result.error('fail');
|
||||
const mapped = r.map(x => x * 2);
|
||||
expect(mapped.isError()).toBe(true);
|
||||
if (mapped.isError()) {
|
||||
expect(mapped.err).toBe('fail');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapError', () => {
|
||||
it('is a no-op on an ok result', () => {
|
||||
const r: Result<number, string> = Result.ok(5);
|
||||
const mapped = r.mapError(e => new Error(e));
|
||||
expect(mapped.isOk()).toBe(true);
|
||||
if (mapped.isOk()) {
|
||||
expect(mapped.val).toBe(5);
|
||||
}
|
||||
});
|
||||
|
||||
it('transforms the error of an error result', () => {
|
||||
const r = Result.error('oops');
|
||||
const mapped = r.mapError(e => ({ reason: e }));
|
||||
expect(mapped.isError()).toBe(true);
|
||||
if (mapped.isError()) {
|
||||
expect(mapped.err).toEqual({ reason: 'oops' });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('flatMap', () => {
|
||||
it('chains ok results', () => {
|
||||
const r = Result.ok(10).flatMap(x =>
|
||||
x > 0 ? Result.ok(x.toString()) : Result.error('negative' as const)
|
||||
);
|
||||
expect(r.isOk()).toBe(true);
|
||||
if (r.isOk()) {
|
||||
expect(r.val).toBe('10');
|
||||
}
|
||||
});
|
||||
|
||||
it('chains to an error result', () => {
|
||||
const r = Result.ok(-1).flatMap(x =>
|
||||
x > 0 ? Result.ok(x.toString()) : Result.error('negative' as const)
|
||||
);
|
||||
expect(r.isError()).toBe(true);
|
||||
if (r.isError()) {
|
||||
expect(r.err).toBe('negative');
|
||||
}
|
||||
});
|
||||
|
||||
it('is a no-op on an error result', () => {
|
||||
const r: Result<number, string> = Result.error('already bad');
|
||||
const chained = r.flatMap(x => Result.ok(x * 2));
|
||||
expect(chained.isError()).toBe(true);
|
||||
if (chained.isError()) {
|
||||
expect(chained.err).toBe('already bad');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('unwrap', () => {
|
||||
it('returns the value for ok results', () => {
|
||||
expect(Result.ok('hello').unwrap()).toBe('hello');
|
||||
});
|
||||
|
||||
it('throws for error results with an Error', () => {
|
||||
const r: Result<string, Error> = Result.error(new Error('fail'));
|
||||
expect(() => r.unwrap()).toThrow('fail');
|
||||
});
|
||||
|
||||
it('throws a wrapped error for non-Error error values', () => {
|
||||
const r: Result<string, string> = Result.error('string error');
|
||||
expect(() => r.unwrap()).toThrow('string error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('unwrapOr', () => {
|
||||
it('returns the value for ok results', () => {
|
||||
const r: Result<number, string> = Result.ok(42);
|
||||
expect(r.unwrapOr(0)).toBe(42);
|
||||
});
|
||||
|
||||
it('returns the default for error results', () => {
|
||||
const r: Result<number, string> = Result.error('nope');
|
||||
expect(r.unwrapOr(0)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('type narrowing', () => {
|
||||
it('narrows to ok after isOk check', () => {
|
||||
const r: Result<number, string> = Result.ok(1);
|
||||
if (r.isOk()) {
|
||||
// TypeScript should know r.val exists here
|
||||
const _v: number = r.val;
|
||||
expect(_v).toBe(1);
|
||||
}
|
||||
});
|
||||
|
||||
it('narrows to error after isError check', () => {
|
||||
const r: Result<number, string> = Result.error('e');
|
||||
if (r.isError()) {
|
||||
// TypeScript should know r.err exists here
|
||||
const _e: string = r.err;
|
||||
expect(_e).toBe('e');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user