From d0c9466170c4338d9793aa22bcbe2c1f68105fbe Mon Sep 17 00:00:00 2001 From: Oli Sturm Date: Fri, 24 Apr 2026 18:15:22 +0100 Subject: [PATCH] separate types --- js-fp3/application/accountApp.d.ts | 11 +++++ js-fp3/application/accountApp.js | 24 --------- js-fp3/domain/account.d.ts | 53 ++++++++++++++++++++ js-fp3/domain/account.js | 38 --------------- js-fp3/domain/money.d.ts | 7 +++ js-fp3/domain/money.js | 13 ----- js-fp3/index.js | 10 ---- js-fp3/infrastructure/repository.d.ts | 31 ++++++++++++ js-fp3/infrastructure/repository.js | 31 ------------ js-fp3/library/logging.d.ts | 19 ++++++++ js-fp3/library/logging.js | 35 -------------- js-fp3/library/result.d.ts | 57 ++++++++++++++++++++++ js-fp3/library/result.js | 70 --------------------------- 13 files changed, 178 insertions(+), 221 deletions(-) create mode 100644 js-fp3/application/accountApp.d.ts create mode 100644 js-fp3/domain/account.d.ts create mode 100644 js-fp3/domain/money.d.ts create mode 100644 js-fp3/infrastructure/repository.d.ts create mode 100644 js-fp3/library/logging.d.ts create mode 100644 js-fp3/library/result.d.ts diff --git a/js-fp3/application/accountApp.d.ts b/js-fp3/application/accountApp.d.ts new file mode 100644 index 0000000..8f0e694 --- /dev/null +++ b/js-fp3/application/accountApp.d.ts @@ -0,0 +1,11 @@ +import { Result } from '../library/result.js'; +import { Repository, AppError } from '../infrastructure/repository.js'; +import { Account, AccountError } from '../domain/account.js'; + +export type AccountResult = Result; + +export type WithdrawMoneyFn = (accountId: string, amount: number) => AccountResult; + +export type CreateWithdrawMoneyResult = Result; + +export declare const createWithdrawMoney: (repo: Repository) => CreateWithdrawMoneyResult; diff --git a/js-fp3/application/accountApp.js b/js-fp3/application/accountApp.js index 9d29091..ffe5090 100644 --- a/js-fp3/application/accountApp.js +++ b/js-fp3/application/accountApp.js @@ -3,36 +3,12 @@ import { output, outputError } from '../library/logging.js'; import { withdraw } from '../domain/account.js'; import { createMoney } from '../domain/money.js'; -/** - * @typedef {import('../infrastructure/repository.js').Repository} Repository - * @typedef {import('../domain/account.js').Account} Account - * @typedef {import('../domain/account.js').AccountError} AccountError - * @typedef {import('../infrastructure/repository.js').AppError} AppError - * @typedef {import('../library/result.js').Result} AccountResult - * @typedef {(accountId: string, amount: number) => AccountResult} WithdrawMoneyFn - * @typedef {import('../library/result.js').Result} CreateWithdrawMoneyResult - */ - -/** - * @param {string} src - * @param {string} msg - * @returns {(result: import('../library/result.js').Result) => import('../library/result.js').Result} - */ const log = (src, msg) => (result) => tap((x) => output(src, msg)(x))(tapError((e) => outputError(src, e))(result)); -/** - * @param {string} src - * @param {(x: T) => string} renderText - * @returns {(result: import('../library/result.js').Result) => import('../library/result.js').Result} - */ const logWith = (src, renderText) => (result) => tap((x) => output(src, renderText(x))(x))(tapError((e) => outputError(src, e))(result)); -/** - * @param {Repository} repo - * @returns {CreateWithdrawMoneyResult} - */ export const createWithdrawMoney = (repo) => ok((accountId, amount) => pipe( diff --git a/js-fp3/domain/account.d.ts b/js-fp3/domain/account.d.ts new file mode 100644 index 0000000..39a822f --- /dev/null +++ b/js-fp3/domain/account.d.ts @@ -0,0 +1,53 @@ +import { Result } from '../library/result.js'; +import { Money } from './money.js'; + +export type Account = { + id: string; + balance: Money; +}; + +export type OpeningBalanceMustBeNonNegative = { + type: 'OpeningBalanceMustBeNonNegative'; +}; + +export type AmountMustBePositive = { + type: 'AmountMustBePositive'; +}; + +export type InsufficientBalance = { + type: 'InsufficientBalance'; + balance: Money; + amount: Money; +}; + +export type AccountNotFound = { + type: 'AccountNotFound'; + accountId: string; +}; + +export type AccountError = + | OpeningBalanceMustBeNonNegative + | AmountMustBePositive + | InsufficientBalance + | AccountNotFound; + +export declare const openingBalanceMustBeNonNegative: OpeningBalanceMustBeNonNegative; + +export declare const amountMustBePositive: AmountMustBePositive; + +export declare const insufficientBalance: ( + balance: Money, + amount: Money +) => InsufficientBalance; + +export declare const accountNotFound: (accountId: string) => AccountNotFound; + +export declare const openAccount: ( + id: string, + openingBalance: Money +) => Result; + +export declare const withdraw: ( + account: Account, + amount: Money +) => Result; diff --git a/js-fp3/domain/account.js b/js-fp3/domain/account.js index 0590bd8..f55073e 100644 --- a/js-fp3/domain/account.js +++ b/js-fp3/domain/account.js @@ -1,63 +1,25 @@ import { ok, fail } from '../library/result.js'; -/** - * @typedef {Object} Account - * @property {string} id - * @property {import('./money.js').Money} balance - */ - -/** @typedef {{ type: 'OpeningBalanceMustBeNonNegative' }} OpeningBalanceMustBeNonNegative */ -/** @typedef {{ type: 'AmountMustBePositive' }} AmountMustBePositive */ -/** @typedef {{ type: 'InsufficientBalance', balance: import('./money.js').Money, amount: import('./money.js').Money }} InsufficientBalance */ -/** @typedef {{ type: 'AccountNotFound', accountId: string }} AccountNotFound */ - -/** @typedef {OpeningBalanceMustBeNonNegative | AmountMustBePositive | InsufficientBalance | AccountNotFound} AccountError */ - -/** - * @type {OpeningBalanceMustBeNonNegative} - */ export const openingBalanceMustBeNonNegative = { type: 'OpeningBalanceMustBeNonNegative' }; -/** - * @type {AmountMustBePositive} - */ export const amountMustBePositive = { type: 'AmountMustBePositive' }; -/** - * @param {import('./money.js').Money} balance - * @param {import('./money.js').Money} amount - * @returns {InsufficientBalance} - */ export const insufficientBalance = (balance, amount) => ({ type: 'InsufficientBalance', balance, amount, }); -/** - * @param {string} accountId - * @returns {AccountNotFound} - */ export const accountNotFound = (accountId) => ({ type: 'AccountNotFound', accountId, }); -/** - * @param {string} id - * @param {import('./money.js').Money} openingBalance - * @returns {import('../library/result.js').Result} - */ export const openAccount = (id, openingBalance) => openingBalance.amount < 0 ? fail(openingBalanceMustBeNonNegative) : ok({ id, balance: openingBalance }); -/** - * @param {Account} account - * @param {import('./money.js').Money} amount - * @returns {import('../library/result.js').Result} - */ export const withdraw = (account, amount) => { if (amount.amount <= 0) { return fail(amountMustBePositive); diff --git a/js-fp3/domain/money.d.ts b/js-fp3/domain/money.d.ts new file mode 100644 index 0000000..21ffced --- /dev/null +++ b/js-fp3/domain/money.d.ts @@ -0,0 +1,7 @@ +export type Money = { + amount: number; +}; + +export declare const createMoney: (amount: number) => Money; + +export declare const formatMoney: (money: Money) => string; diff --git a/js-fp3/domain/money.js b/js-fp3/domain/money.js index b83ed64..863b3b3 100644 --- a/js-fp3/domain/money.js +++ b/js-fp3/domain/money.js @@ -1,16 +1,3 @@ -/** - * @typedef {Object} Money - * @property {number} amount - */ - -/** - * @param {number} amount - * @returns {Money} - */ export const createMoney = (amount) => ({ amount }); -/** - * @param {Money} money - * @returns {string} - */ export const formatMoney = (money) => money.amount.toFixed(2); diff --git a/js-fp3/index.js b/js-fp3/index.js index fab48db..96cd4db 100644 --- a/js-fp3/index.js +++ b/js-fp3/index.js @@ -6,19 +6,9 @@ import { openAccount } from './domain/account.js'; import { createMoney } from './domain/money.js'; import { createWithdrawMoney } from './application/accountApp.js'; -/** - * @param {string} src - * @param {string} msg - * @returns {(result: import('./library/result.js').Result) => import('./library/result.js').Result} - */ const log = (src, msg) => (result) => tap((x) => output(src, msg)(x))(tapError((e) => outputError(src, e))(result)); -/** - * @param {string} src - * @param {(x: T) => string} renderText - * @returns {(result: import('./library/result.js').Result) => import('./library/result.js').Result} - */ const logWith = (src, renderText) => (result) => tap((x) => output(src, renderText(x))(x))(tapError((e) => outputError(src, e))(result)); diff --git a/js-fp3/infrastructure/repository.d.ts b/js-fp3/infrastructure/repository.d.ts new file mode 100644 index 0000000..ce8427f --- /dev/null +++ b/js-fp3/infrastructure/repository.d.ts @@ -0,0 +1,31 @@ +import { Result } from '../library/result.js'; +import { Account, AccountError } from '../domain/account.js'; + +export type Repository = { + loadAccount: (id: string) => Result; + saveAccount: (account: Account) => Result; +}; + +export type AppErrorRepositoryCreationFailed = { + type: 'RepositoryCreationFailed'; + message: string; +}; + +export type AppErrorInnerAccountError = { + type: 'InnerAccountError'; + innerError: AccountError; +}; + +export type AppError = + | AppErrorRepositoryCreationFailed + | AppErrorInnerAccountError; + +export declare const repositoryCreationFailed: ( + message: string +) => AppErrorRepositoryCreationFailed; + +export declare const innerAccountError: ( + innerError: AccountError +) => AppErrorInnerAccountError; + +export declare const createInMemoryRepository: () => Repository; diff --git a/js-fp3/infrastructure/repository.js b/js-fp3/infrastructure/repository.js index 0a26435..2332539 100644 --- a/js-fp3/infrastructure/repository.js +++ b/js-fp3/infrastructure/repository.js @@ -2,46 +2,19 @@ import { ok, fail } from '../library/result.js'; import { logReturn } from '../library/logging.js'; import { accountNotFound } from '../domain/account.js'; -/** - * @typedef {Object} Repository - * @property {(id: string) => import('../library/result.js').Result} loadAccount - * @property {(account: import('../domain/account.js').Account) => import('../library/result.js').Result} saveAccount - */ - -/** @typedef {{ type: 'RepositoryCreationFailed', message: string }} AppErrorRepositoryCreationFailed */ -/** @typedef {{ type: 'InnerAccountError', innerError: import('../domain/account.js').AccountError }} AppErrorInnerAccountError */ - -/** @typedef {AppErrorRepositoryCreationFailed | AppErrorInnerAccountError} AppError */ - -/** - * @param {string} message - * @returns {AppErrorRepositoryCreationFailed} - */ export const repositoryCreationFailed = (message) => ({ type: 'RepositoryCreationFailed', message, }); -/** - * @param {import('../domain/account.js').AccountError} innerError - * @returns {AppErrorInnerAccountError} - */ export const innerAccountError = (innerError) => ({ type: 'InnerAccountError', innerError, }); -/** - * @returns {Repository} - */ export const createInMemoryRepository = () => { - /** @type {Map} */ const store = new Map(); - /** - * @param {string} id - * @returns {import('../library/result.js').Result} - */ const getById = (id) => store.has(id) ? logReturn( @@ -53,10 +26,6 @@ export const createInMemoryRepository = () => { fail(accountNotFound(id)) ); - /** - * @param {import('../domain/account.js').Account} account - * @returns {import('../library/result.js').Result} - */ const save = (account) => { store.set(account.id, account); return logReturn( diff --git a/js-fp3/library/logging.d.ts b/js-fp3/library/logging.d.ts new file mode 100644 index 0000000..e3e9630 --- /dev/null +++ b/js-fp3/library/logging.d.ts @@ -0,0 +1,19 @@ +export declare const logReturn: (message: string, r: T) => T; + +export declare const output: (src: string, message: string) => (x: T) => void; + +export declare const outputWith: ( + src: string, + renderText: (x: T) => string +) => (x: T) => void; + +export declare const outputError: (src: string, error: E) => void; + +export declare const log: (src: string, message: string) => ( + result: import('./result.js').Result +) => import('./result.js').Result; + +export declare const logWith: ( + src: string, + renderText: (x: T) => string +) => (result: import('./result.js').Result) => import('./result.js').Result; diff --git a/js-fp3/library/logging.js b/js-fp3/library/logging.js index f54496e..2e5ab7c 100644 --- a/js-fp3/library/logging.js +++ b/js-fp3/library/logging.js @@ -1,59 +1,24 @@ import { tap, tapError } from './result.js'; -/** - * @template T - * @param {string} message - * @param {T} r - * @returns {T} - */ export const logReturn = (message, r) => { console.log(message); return r; }; -/** - * @template T - * @param {string} src - * @param {string} message - * @returns {(x: T) => void} - */ export const output = (src, message) => (x) => { const value = typeof x === 'object' ? JSON.stringify(x) : x; console.log(`[${src}] ${message} | ${value}`); }; -/** - * @template T - * @param {string} src - * @param {(x: T) => string} renderText - * @returns {(x: T) => void} - */ export const outputWith = (src, renderText) => (x) => { console.log(`[${src}] ${renderText(x)}`); }; -/** - * @template E - * @param {string} src - * @param {E} error - */ export const outputError = (src, error) => { const value = typeof error === 'object' ? JSON.stringify(error) : error; console.error(`\x1b[1;31m[${src} ERROR]\x1b[0m ${value}`); }; -/** - * @template T, E - * @param {string} src - * @param {string} message - * @returns {(result: import('./result.js').Result) => import('./result.js').Result} - */ export const log = (src, message) => tap(output(src, message)); -/** - * @template T, E - * @param {string} src - * @param {(x: T) => string} renderText - * @returns {(result: import('./result.js').Result) => import('./result.js').Result} - */ export const logWith = (src, renderText) => tap(outputWith(src, renderText)); diff --git a/js-fp3/library/result.d.ts b/js-fp3/library/result.d.ts new file mode 100644 index 0000000..5ee29b0 --- /dev/null +++ b/js-fp3/library/result.d.ts @@ -0,0 +1,57 @@ +export type Success = { + isSuccess: true; + isFailure: false; + value: T; +}; + +export type Failure = { + isSuccess: false; + isFailure: true; + error: E; +}; + +export type Result = Success | Failure; + +export declare const ok: (value: T) => Result; + +export declare const fail: (error: E) => Result; + +export declare const catchResult: ( + f: () => T, + exceptionMapper: (err: Error) => E +) => Result; + +export declare const bind: ( + binder: (value: TIn) => Result +) => (result: Result) => Result; + +export declare const map: ( + mapper: (value: TIn) => TOut +) => (result: Result) => Result; + +export declare const tap: ( + action: (value: T) => void +) => (result: Result) => Result; + +export declare const tapError: ( + action: (error: E) => void +) => (result: Result) => Result; + +export declare const mapError: ( + mapper: (error: EIn) => EOut +) => (result: Result) => Result; + +export declare const match: ( + onSuccess: (value: T) => TResult, + onFailure: (error: E) => TResult +) => (result: Result) => TResult; + +export declare const switch_: ( + onSuccess: (value: T) => void, + onFailure: (error: E) => void +) => (result: Result) => void; + +export declare const pipe: ( + result: Result, + ...fns: Array<(arg: Result) => Result> +) => Result; diff --git a/js-fp3/library/result.js b/js-fp3/library/result.js index a2b4a44..d735948 100644 --- a/js-fp3/library/result.js +++ b/js-fp3/library/result.js @@ -1,40 +1,15 @@ -/** - * @template T, E - * @typedef {Object} Result - * @property {boolean} isSuccess - * @property {boolean} isFailure - * @property {T} [value] - Present when isSuccess is true - * @property {E} [error] - Present when isFailure is true - */ - -/** - * @template T, E - * @param {T} value - * @returns {Result} - */ export const ok = (value) => ({ isSuccess: true, isFailure: false, value, }); -/** - * @template T, E - * @param {E} error - * @returns {Result} - */ export const fail = (error) => ({ isSuccess: false, isFailure: true, error, }); -/** - * @template T, E - * @param {() => T} f - * @param {(err: Error) => E} exceptionMapper - * @returns {Result} - */ export const catchResult = (f, exceptionMapper) => { try { return ok(f()); @@ -43,77 +18,32 @@ export const catchResult = (f, exceptionMapper) => { } }; -/** - * @template TIn, TOut, E - * @param {(value: TIn) => Result} binder - * @returns {(result: Result) => Result} - */ export const bind = (binder) => (result) => result.isSuccess ? binder(result.value) : fail(result.error); -/** - * @template TIn, TOut, E - * @param {(value: TIn) => TOut} mapper - * @returns {(result: Result) => Result} - */ export const map = (mapper) => (result) => result.isSuccess ? ok(mapper(result.value)) : fail(result.error); -/** - * @template T, E - * @param {(value: T) => void} action - * @returns {(result: Result) => Result} - */ export const tap = (action) => (result) => { if (result.isSuccess) action(result.value); return result; }; -/** - * @template T, E - * @param {(error: E) => void} action - * @returns {(result: Result) => Result} - */ export const tapError = (action) => (result) => { if (result.isFailure) action(result.error); return result; }; -/** - * @template T, EIn, EOut - * @param {(error: EIn) => EOut} mapper - * @returns {(result: Result) => Result} - */ export const mapError = (mapper) => (result) => result.isSuccess ? ok(result.value) : fail(mapper(result.error)); -/** - * @template T, E, TResult - * @param {(value: T) => TResult} onSuccess - * @param {(error: E) => TResult} onFailure - * @returns {(result: Result) => TResult} - */ export const match = (onSuccess, onFailure) => (result) => result.isSuccess ? onSuccess(result.value) : onFailure(result.error); -/** - * @template T, E - * @param {(value: T) => void} onSuccess - * @param {(error: E) => void} onFailure - * @returns {(result: Result) => void} - */ export const switch_ = (onSuccess, onFailure) => (result) => { if (result.isSuccess) onSuccess(result.value); else onFailure(result.error); }; -/** - * Pipe a result through a series of functions (left-to-right composition). - * This allows chaining without extending the prototype. - * @template T, E - * @param {Result} result - * @param {...Function} fns - * @returns {Result} - */ export const pipe = (result, ...fns) => fns.reduce((acc, fn) => fn(acc), result);