diff --git a/README.md b/README.md index eb5f01e..db31026 100644 --- a/README.md +++ b/README.md @@ -40,3 +40,17 @@ const RegularHeader = ({ dayOfTheWeek }) => { ) } ``` + +You can also use it with unit functions, to short circuit execution of branches where the chain executes to false: +```javascript +const result = $if(false) + .thenDo(() => computeEngine.computeValue()) + .elseIf(true).thenDo(() => computeEngine.computeSomethingElse()) + .elseIf(Math.random > 0.5).then('bingo!') + .elseDo(computeEngine.getDefaultValue); +``` +In this above example, only one of the branches gets executed, so you don't need to worry about all the wasted calculation or unwanted side-effects. + +--- + +And $if also supports types through [Typescript](http://typescriptlang.org/)! diff --git a/src/index.ts b/src/index.ts index 83c2c68..a750eb3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,8 +4,9 @@ const ifExpression = (condition: boolean): IfStatement => { const makeIfStatement = (cond: boolean): IfStatement => { return { - then: (thenValue: T) => makeThenStatement(cond, thenValue) - } + then: (thenValue: T) => makeThenStatement(cond, thenValue), + thenDo: (thenArrow: Unit) => makeThenDoStatement(cond, thenArrow), + }; } const makeThenStatement = (cond: boolean, thenValue: T): ThenStatement => { @@ -19,41 +20,83 @@ const makeThenStatement = (cond: boolean, thenValue: T): ThenStatement => }, elseIf: (cond2: boolean) => { if (cond) { - return makeLoadedIfStatement(thenValue) as any; + return makeLoadedIfStatement(thenValue); } else { return makeIfStatement(cond2); } + }, + elseDo: (elseArrow: Unit) => { + if (cond) { + return thenValue; + } else { + return elseArrow(); + } } - } + }; } -const makeLoadedIfStatement = (fixedValue: T) => { +const makeThenDoStatement = (cond: boolean, thenArrow: Unit): ThenStatement => { return { - then: (ignoredThenValue: TX) => makeLoadedThenStatement(fixedValue) + else: (elseValue: V) => { + if (cond) { + return thenArrow(); + } else { + return elseValue; + } + }, + elseIf: (cond2: boolean) => { + if (cond) { + return makeLoadedIfStatement(thenArrow()) as any; + } else { + return makeIfStatement(cond2); + } + }, + elseDo: (elseArrow: Unit) => { + if (cond) { + return thenArrow(); + } else { + return elseArrow(); + } + } + }; +} + +const makeLoadedIfStatement = (fixedValue: T): IfStatement => { + return { + then: (ignoredThenValue: TX) => makeLoadedThenStatement(fixedValue), + thenDo: (ignoredThenValue: Unit) => makeLoadedThenStatement(fixedValue) } } -const makeLoadedThenStatement = (fixedValue: T) => { +const makeLoadedThenStatement = (fixedValue: T): ThenStatement => { return { else: (ignoredElseValue: VX) => { return fixedValue; }, elseIf: (ignoredCond: boolean) => { return makeLoadedIfStatement(fixedValue); + }, + elseDo: (ignoredElseArrow: Unit) => { + return fixedValue; } } } +type Unit = () => T; + type IfStatement = { then: (t: T) => ThenStatement; + thenDo: (p: Unit) => ThenStatement; } type ElseIfStatement = { then: (t2: T2) => ThenStatement; + thenDo: (p: Unit) => ThenStatement; } type ThenStatement = { else: ElseStatement; + elseDo: (p: Unit) => T | V; elseIf: (condition: boolean) => ElseIfStatement; } diff --git a/test/index.spec.ts b/test/index.spec.ts index a881a2a..66695f4 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -1,5 +1,6 @@ import {$if} from "../src"; +type Unit = () => T; describe("$if expression", () => { it("returns the .then clause's value when condition is true", () => { @@ -82,3 +83,135 @@ describe("$if with elseif clause", () => { expect(expressionValue).toBe(firstTrueIndex); }); }); + +describe("$if with delayed values", () => { + type A = 'valA'; + type B = 'valB'; + type C = 'valC'; + + let fnA: () => 'valA'; + let fnB: () => 'valB'; + let fnC: () => 'valC'; + + beforeEach(() => { + fnA = jest.fn(() => 'valA'); + fnB = jest.fn(() => 'valB'); + fnC = jest.fn(() => 'valC'); + }); + + it('works on .thenDo()', () => { + const value: A | B = $if(true).thenDo(fnA).elseDo(fnB); + + expect(value).toBe('valA'); + expect(fnA).toBeCalledTimes(1); + expect(fnB).not.toBeCalled(); + }); + + + it('works on .elseDo()', () => { + const value: A | B = $if(false).thenDo(fnA).elseDo(fnB); + + expect(value).toBe('valB'); + expect(fnA).not.toBeCalled(); + expect(fnB).toBeCalledTimes(1); + }); + + it('works on .elseIf.thenDo()', () => { + const value: A | B | C = $if(false).thenDo(fnA).elseIf(true).thenDo(fnB).elseDo(fnC); + + expect(value).toBe('valB'); + expect(fnA).not.toBeCalled(); + expect(fnB).toBeCalledTimes(1); + expect(fnC).not.toBeCalled(); + }); + + it('works on .elseIf.elseDo()', () => { + const value: A | B | C = $if(false).thenDo(fnA).elseIf(false).thenDo(fnB).elseDo(fnC); + + expect(value).toBe('valC'); + expect(fnA).not.toBeCalled(); + expect(fnB).not.toBeCalled(); + expect(fnC).toBeCalledTimes(1); + }); + + it(`doesn't bleed over to any further arrows`, () => { + const fnD: Unit<'valD'> = jest.fn(() => 'valD'); + const fnE: Unit<'valE'> = jest.fn(() => 'valE'); + + const value: A|B|C|'valD'|'valE' = $if(false).thenDo(fnA) + .elseIf(true).thenDo(fnB) + .elseIf(true).thenDo(fnC) + .elseIf(false).thenDo(fnD) + .elseDo(fnE); + + expect(value).toBe('valB'); + expect(fnA).not.toBeCalled(); + expect(fnB).toBeCalledTimes(1); + expect(fnC).not.toBeCalled(); + expect(fnD).not.toBeCalled(); + expect(fnE).not.toBeCalled(); + }); +}); + +describe("$if with mixed clauses", () => { + type A = 'valA'; + type B = 'valB'; + type C = 'valC'; + + let fnA: () => 'valA'; + let fnB: () => 'valB'; + let fnC: () => 'valC'; + + beforeEach(() => { + fnA = jest.fn(() => 'valA'); + fnB = jest.fn(() => 'valB'); + fnC = jest.fn(() => 'valC'); + }); + + it('works on .thenDo()', () => { + const value: A | B = $if(true).then(fnA()).elseDo(fnB); + + expect(value).toBe('valA'); + expect(fnA).toBeCalledTimes(1); + expect(fnB).not.toBeCalled(); + }); + + + it('works on .elseDo()', () => { + const value: A | B = $if(false).then(fnA()).elseDo(fnB); + + expect(value).toBe('valB'); + expect(fnB).toBeCalledTimes(1); + }); + + it('works on .elseIf.thenDo()', () => { + const value: A | B | C = $if(false).thenDo(fnA).elseIf(true).thenDo(fnB).else(fnC()); + + expect(value).toBe('valB'); + expect(fnA).not.toBeCalled(); + expect(fnB).toBeCalledTimes(1); + }); + + it('works on .elseIf.elseDo()', () => { + const value: A | B | C = $if(false).thenDo(fnA).elseIf(false).then(fnB()).elseDo(fnC); + + expect(value).toBe('valC'); + expect(fnA).not.toBeCalled(); + expect(fnC).toBeCalledTimes(1); + }); + + it(`doesn't bleed over`, () => { + const fnD: Unit<'valD'> = jest.fn(() => 'valD'); + const fnE: Unit<'valE'> = jest.fn(() => 'valE'); + + const value: A|B|C|'valD'|'valE' = $if(false).then(fnA()) + .elseIf(true).thenDo(fnB) + .elseIf(true).then(fnC()) + .elseIf(true).thenDo(fnD) + .else(fnE()); + + expect(value).toBe('valB'); + expect(fnB).toBeCalledTimes(1); + expect(fnD).not.toBeCalled(); + }); +});