forked from microsoft/fluentui
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Result: Add either to help with creation of codemods (microsoft#14154)
#### Pull request checklist - [ ] Addresses an existing issue: Fixes #0000 - [ ] Include a change request file using `$ yarn change` #### Description of changes Either makes it easy to handle things like failure states since you can return the error and handle it later after preforming several computations. #### Focus areas to test (optional)
- Loading branch information
Showing
6 changed files
with
261 additions
and
11 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"type": "patch", | ||
"comment": "Result: add in an result helper to assit in the creation of codemods", | ||
"packageName": "@fluentui/codemods", | ||
"email": "joschect@microsoft.com", | ||
"dependentChangeType": "patch", | ||
"date": "2020-07-22T00:13:11.108Z" | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,26 @@ | ||
import { Mappable } from './mappable'; | ||
|
||
export type Flattened<T, V> = T extends Chainable<unknown> ? T : V; | ||
export interface Chainable<T> extends Mappable<T> { | ||
export interface Chainable<T> { | ||
/** | ||
* If this Chainables's T is an instance of Chainable, | ||
* then it will flatten it so now Chainable now contains | ||
* a value of whatever type T held | ||
* Chainable<Chainable<T>> => Chainable<T> | ||
*/ | ||
flatten: () => Flattened<T, Chainable<T>>; | ||
|
||
/** | ||
* This takes in a lambda which maps from the current chainable | ||
* type to the same type of chainable with a new type R. The entire function then returns | ||
* a new Chainable with type R | ||
* @param fn: A lambda that maps from a type T and returns a new Chainable that has type R. | ||
*/ | ||
chain: <R>(fn: (v: T) => Chainable<R>) => Chainable<R>; | ||
|
||
/** | ||
* A combination of chain, map, and flatten. Then can return either type R or | ||
* Chainable<R> and this value will get flattened appropriately so the | ||
* returned Chainable is not nested. | ||
* @param fn: A lambda that maps from a type T and returns a new type R. | ||
*/ | ||
then: <R>(fn: (v: T) => R) => Chainable<R>; | ||
} |
This file was deleted.
Oops, something went wrong.
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import { Chainable, Flattened } from './chainable'; | ||
|
||
export interface Ok<R, E> extends ResultInternal<R, E> { | ||
ok: true; | ||
value: R; | ||
} | ||
|
||
export interface Err<R, E> extends ResultInternal<R, E> { | ||
ok: false; | ||
value: E; | ||
} | ||
|
||
class ResultInternal<R, E> implements Chainable<R> { | ||
public ok: boolean; | ||
public value: R | E; | ||
|
||
public constructor(options: { ok: boolean; value: R | E }) { | ||
this.ok = options.ok; | ||
this.value = options.value; | ||
} | ||
|
||
public chain<T>(this: Result<R, E>, fn: (v: R) => Result<T, E>): Result<T, E> { | ||
if (this.ok) { | ||
return fn(this.value); | ||
} | ||
return Err(this.value); | ||
} | ||
|
||
/** | ||
* Works just like chain, but is only called if this Result is an error. | ||
* This returns a new Result with type Result<R, NewType> | ||
*/ | ||
public errChain<T>(this: Result<R, E>, fn: (v: E) => Result<R, T>): Result<R, T> { | ||
if (!this.ok) { | ||
return fn(this.value); | ||
} | ||
return Ok(this.value); | ||
} | ||
|
||
public flatten(): Flattened<R, Result<R, E>> { | ||
if (this.value && this.value instanceof ResultInternal) { | ||
return this.value as Flattened<R, Result<R, E>>; | ||
} | ||
return (this as unknown) as Flattened<R, Result<R, E>>; | ||
} | ||
|
||
/** | ||
* This allows users to opperate on a presumed result without needing to know whether or not | ||
* the result was successful or not. | ||
* At each call if it is a result type of ok, it will call the supplied function, otherwise it | ||
* will return the current value with a new Err. | ||
* | ||
* @param fnOk Function that takes in an ok value of type R and returns either F or Result<F,E> | ||
*/ | ||
public then<F>(this: Result<R, E>, fnOk: (v: R) => F | Result<F, E>): Result<F, E> { | ||
if (this.ok) { | ||
return Ok(fnOk(this.value)).flatten() as Result<F, E>; | ||
} | ||
|
||
return Err(this.value); | ||
} | ||
|
||
/** | ||
* Works just like then, but is only called if this Result is an error. | ||
* This returns a new Result with type Result<R, NewType> | ||
*/ | ||
public errThen<F>(this: Result<R, E>, fnErr: (v: E) => F | Result<R, F>): Result<R, F> { | ||
if (!this.ok) { | ||
return Err(fnErr(this.value)).flatten() as Result<R, F>; | ||
} | ||
|
||
return Ok(this.value); | ||
} | ||
|
||
public okOrElse(this: Result<R, E>, okElse: R): R { | ||
if (this.ok) { | ||
return this.value; | ||
} | ||
return okElse; | ||
} | ||
|
||
public errOrElse(this: Result<R, E>, errElse: E): E { | ||
if (!this.ok) { | ||
return this.value; | ||
} | ||
return errElse; | ||
} | ||
|
||
/** | ||
* Takes in two functions, one which takes type R and the other which takes type E and both return | ||
* type T. This lets you handle either case and potentially return a unifying type. You might use this | ||
* to create a message for users. | ||
* | ||
* @param fnOk Function that maps from Ok value(R) to new type T | ||
* @param fnErr Function that maps from Err value(E) to new type T | ||
*/ | ||
public resolve<T>(this: Result<R, E>, fnOk: (v: R) => T, fnErr: (v: E) => T): T { | ||
if (this.ok) { | ||
return fnOk(this.value); | ||
} | ||
return fnErr(this.value); | ||
} | ||
} | ||
|
||
export const Ok = <R, E>(value: R): Ok<R, E> => { | ||
return new ResultInternal<R, E>({ value: value, ok: true }) as Ok<R, E>; | ||
}; | ||
|
||
export const Err = <R, E>(value: E): Err<R, E> => { | ||
return new ResultInternal<R, E>({ value: value, ok: false }) as Err<R, E>; | ||
}; | ||
|
||
/** | ||
* Result is a useful type for when you might want to handle errors down the line without swallowing them | ||
* while still preforming potentially several operations that could result in an error. A simple example could | ||
* be a dividing function that returns a Result. Instead of throwing an error that you cannot divide by zero, | ||
* it would be returned in an Err. This allows the rest of the program to execute cleanly. | ||
*/ | ||
export type Result<R, E> = Err<R, E> | Ok<R, E>; |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import { Err, Ok, Result } from '../result'; | ||
|
||
const getOk = <T, Z>(ok: T, err: Z): Result<T, Z> => { | ||
return Ok<T, Z>(ok!); | ||
}; | ||
|
||
const getErr = <T, Z>(okay: T, err: Z): Result<T, Z> => { | ||
return Err<T, Z>(err!); | ||
}; | ||
|
||
describe('Result', () => { | ||
it('chained Okay value is evaluated correctly', () => { | ||
expect( | ||
getOk(3, '4') | ||
.chain(v => Ok(v + 3)) | ||
.okOrElse(100), | ||
).toBe(6); | ||
}); | ||
|
||
it('errChained Err value is evaluated correctly', () => { | ||
expect( | ||
getErr(3, '4') | ||
.errChain(v => Err(7)) | ||
.errOrElse(100), | ||
).toBe(7); | ||
}); | ||
|
||
it('chained Err value is evaluated correctly', () => { | ||
expect( | ||
getErr(3, '4') | ||
.chain(v => Ok(1)) | ||
.okOrElse(100), | ||
).toBe(100); | ||
}); | ||
|
||
it('chained Err value is evaluated correctly', () => { | ||
expect( | ||
getErr(3, '4') | ||
.errChain(v => Ok(1)) | ||
.okOrElse(100), | ||
).toBe(1); | ||
}); | ||
|
||
it('chain returning a Err returns a Err correctly', () => { | ||
expect(getOk(3, '4').chain(v => Err('Error')).ok).toBe(false); | ||
}); | ||
|
||
it('errChain returning an Ok returns an Ok correctly', () => { | ||
expect(getOk(3, '4').chain(v => Ok('Error')).ok).toBe(true); | ||
}); | ||
|
||
it('Thens correctly on Ok', () => { | ||
expect( | ||
getOk(3, '4') | ||
.then(v => 30) | ||
.then(v => v.toString()) | ||
.okOrElse('Bad'), | ||
).toBe('30'); | ||
}); | ||
|
||
it('Thens correctly on Err', () => { | ||
expect( | ||
getErr(3, '4') | ||
.then(v => 30) | ||
.then(v => v.toString()) | ||
.okOrElse('Bad'), | ||
).toBe('Bad'); | ||
}); | ||
|
||
it('orThens correctly on Err', () => { | ||
expect( | ||
getErr(3, '4') | ||
.errThen(v => 30) | ||
.errThen(v => v.toString()) | ||
.errOrElse('Bad'), | ||
).toBe('30'); | ||
}); | ||
|
||
it('orThens correctly on Ok', () => { | ||
expect( | ||
getOk(3, '4') | ||
.errThen(v => 30) | ||
.errThen(v => v.toString()) | ||
.errOrElse('Bad'), | ||
).toBe('Bad'); | ||
}); | ||
|
||
it('resolve calls Ok function with Ok object value', () => { | ||
const spyLeft = jest.fn(); | ||
const spyRight = jest.fn(); | ||
getOk(3, '4').resolve(spyRight, spyLeft); | ||
expect(spyRight).toHaveBeenCalled(); | ||
expect(spyRight).toHaveBeenCalledWith(3); | ||
}); | ||
|
||
it('resolve calls Err function with Err object value', () => { | ||
const spyLeft = jest.fn(); | ||
const spyRight = jest.fn(); | ||
getErr(3, '4').resolve(spyRight, spyLeft); | ||
expect(spyLeft).toHaveBeenCalled(); | ||
expect(spyLeft).toHaveBeenCalledWith('4'); | ||
}); | ||
|
||
it('resolve calls Err function with Err object value after then', () => { | ||
const spyLeft = jest.fn(); | ||
const spyRight = jest.fn(); | ||
getErr(3, '4') | ||
.then(v => 10) | ||
.resolve(spyRight, spyLeft); | ||
expect(spyLeft).toHaveBeenCalled(); | ||
expect(spyLeft).toHaveBeenCalledWith('4'); | ||
}); | ||
}); |