diff --git a/package.json b/package.json index a09f207..6eca479 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,12 @@ "name": "Chris Feijoo", "url": "https://github.com/kube" }, + "contributors": [ + { + "name": "Cezary Drożak", + "url": "https://github.com/nawordar" + } + ], "license": "MIT", "bugs": { "url": "https://github.com/kube/when-switch/issues" diff --git a/src/__tests__/true.spec.ts b/src/__tests__/true.spec.ts new file mode 100644 index 0000000..5a32c05 --- /dev/null +++ b/src/__tests__/true.spec.ts @@ -0,0 +1,86 @@ + + /*#######. + ########",#: + #########',##". + ##'##'## .##',##. + ## ## ## # ##",#. + ## ## ## ## ##' + ## ## ## :## + ## ## ##*/ + +import when from '../when' +import { StaticCheck, IsType, IsSubtype } from './helpers'; + +describe("'when.true' syntax with a simple return type", () => { + const getDrinkPrice = (drink: 'Pepsi' | 'Coke' | 'Orangina'): number => + when + .true(() => drink === 'Coke', 1.5) + .true(() => drink === 'Pepsi', 1.8) + .else(2.0) + + StaticCheck>>() + + it('returns value if assertion is true', () => { + expect(getDrinkPrice('Coke')).toEqual(1.5) + expect(getDrinkPrice('Pepsi')).toEqual(1.8) + }) + + it('returns default value if any of assertions is true', () => { + expect(getDrinkPrice('Orangina')).toEqual(2.0) + }) +}) + +describe("'when.true' syntax with a union return-type", () => { + const getDrinkPrice = (drink: 'Pepsi' | 'Coke' | 'Orangina'): number | string | boolean => + when + .true(drink === 'Coke', 1.5) + .true(drink === 'Pepsi', true) + .else('Free') + + StaticCheck>>() + + it('returns value if matches an expression', () => { + expect(getDrinkPrice('Coke')).toEqual(1.5) + expect(getDrinkPrice('Pepsi')).toEqual(true) + }) + + it('returns default value if no match', () => { + expect(getDrinkPrice('Orangina')).toEqual('Free') + }) +}) + +describe("'when.true' syntax with a function as `is` return value", () => { + type Action = { type: string } + + const apply = (action: Action) => + when + .true(action.type === 'INCREMENT', () => 2) + .true(action.type === 'DECREMENT', () => true) + .else(() => null) + + StaticCheck>>() + + it('returns value if matches an expression', () => { + expect(apply({ type: 'INCREMENT' })).toEqual(2) + expect(apply({ type: 'DECREMENT' })).toEqual(true) + }) + + it('returns default value if no match', () => { + expect(apply({ type: 'Hello' })).toBeNull() + expect(apply({ type: 'World' })).toBeNull() + }) +}) + +describe("'when.true' syntax with assertion wrapped in thunk", () => { + const isBuffer = (obj: { isBuffer: () => boolean }) => + when + .true(obj.isBuffer, 1) + .else(2) + + StaticCheck>>() + + it('unwraps the thunk', () => { + expect(isBuffer({isBuffer: () => true})).toBe(1) + expect(isBuffer({isBuffer: () => false})).toBe(2) + }) +}) diff --git a/src/when.ts b/src/when.ts index a5c30f7..9715d95 100644 --- a/src/when.ts +++ b/src/when.ts @@ -26,6 +26,18 @@ export type When = { else: (returnValue: ((inputValue: T) => W) | W) => V | W } +export interface WhenSwitch { + (expr: T): When + + /** Tests assertion and returns _value_ if assertion is true. */ + true: (assertion: (() => boolean) | boolean, value: (() => T) | T) => True +} + +export type True = { + true: (assertion: (() => boolean) | boolean, value: (() => U) | U) => True, + else: (returnValue: (() => U) | U) => T | U +} + /** * Exposes same API as `when`, but just propagates a resolved value, * without doing any further test. @@ -39,19 +51,39 @@ const resolve = (resolvedValue: any): When => ({ /** * Tests an object against multiple expressions. */ -export const when = (expr: T): When => ({ +export const when = ((expr: T): When => ({ is: (constExpr, value) => expr === constExpr - ? resolve(typeof value === 'function' ? value(constExpr) : value) + ? resolve(typeof value === 'function' ? (value as (x: any) => any)(constExpr) : value) : when(expr), match: (matcher, value) => matcher.test(expr) - ? resolve(typeof value === 'function' ? value(expr) : value) + ? resolve(typeof value === 'function' ? (value as (x: any) => any)(expr) : value) : when(expr), else: defaultValue => - typeof defaultValue === 'function' ? defaultValue(expr) : defaultValue + typeof defaultValue === 'function' ? (defaultValue as (x: any) => any)(expr) : defaultValue +})) as WhenSwitch + +/** + * Exposes same API as `true`, but just propagates a resolved value, + * without doing any further test. + */ +const resolveAssertion = (resolvedValue: any): True => ({ + true: () => resolveAssertion(resolvedValue), + else: () => resolvedValue }) +when.true = (assertion: (() => boolean) | boolean, value: (() => T) | T): True => + (typeof assertion === 'function' ? assertion() : assertion) + ? resolveAssertion(typeof value === 'function' ? (value as (() => any))() : value) + : ({ + true: when.true, + else: defaultValue => + typeof defaultValue === 'function' + ? (defaultValue as (() => any))() + : defaultValue + }) + export default when