Created
April 18, 2021 16:19
-
-
Save JSuder-xx/66e7a2c362eab785c11dae41c9b2a124 to your computer and use it in GitHub Desktop.
A quick thought experiment on how the article [Dealing with complex dependency injection in F#](https://bartoszsypytkowski.com/dealing-with-complex-dependency-injection-in-f/) might translate to TypeScript.
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
/** | |
* Referenced article: [Dealing with complex dependency injection in F#](https://bartoszsypytkowski.com/dealing-with-complex-dependency-injection-in-f/) | |
* | |
* Good | |
* - Placing all services (stateless implementations) in a single object reduces the noise. | |
* - TypeScript intersection helps with composability (functions can specify exactly what they require). | |
* | |
* Not as Good | |
* - TypeScript inference is a bit weaker than F# so I believe authors must explicitly declare all service | |
* requirements (i.e. type constraints) rather than having the constraints inferred from the body. | |
*/ | |
module ReadMe {} | |
//---------------------------------------------------------------------- | |
// Low Level Services - Injected Abstractions | |
//---------------------------------------------------------------------- | |
type Logger = { | |
log(message: string, kind: "debug" | "error" | "info"): Promise<void>; | |
} | |
type HaveLogger = Record<"logger", Logger> | |
type Database = { | |
query<Result>(query: string, convert: (data: any) => Result): Promise<Result>; | |
execute(query: string): Promise<void>; | |
} | |
type HaveDatabase = Record<"database", Database> | |
type RandomValues = { | |
next(): number; | |
} | |
type HaveRandomValues = Record<"randomValues", RandomValues>; | |
//---------------------------------------------------------------------- | |
// Utilities depending on abstractions | |
//---------------------------------------------------------------------- | |
module Random { | |
export const bytes = <Env extends HaveRandomValues>(_env: Env, _val: number) => | |
// ⚠️Not implemented | |
0; | |
} | |
const bcrypt = (_salt: number, _value: string): number => | |
// ⚠️Not implemented | |
0; | |
//---------------------------------------------------------------------- | |
// Domain | |
//---------------------------------------------------------------------- | |
type User = { | |
kind: "User", | |
userId: number; | |
salt: number; | |
hash: number; | |
} | |
module Db { | |
export const fetchUser = <Env extends HaveDatabase>(env: Env, userId: number) => | |
// ⚠️Not implemented | |
env.database.query<User>("...someSQL...", (_) => ({ kind: "User", userId, salt: 0, hash: 0 })) | |
export const updateUser = <Env extends HaveDatabase>(env: Env, _user: User) => | |
// ⚠️Not implemented | |
env.database.execute("...someSQL...") | |
} | |
//---------------------------------------------------------------------- | |
// Example of Top Level Algorithm Definition | |
// Observe that the environment is explicitly passed five times in the body of changePassword. | |
//---------------------------------------------------------------------- | |
const changePassword = <Env extends HaveDatabase & HaveLogger & HaveRandomValues>(env: Env) => | |
async (request: { | |
userId: number; | |
oldPass: string; | |
newPass: string | |
}): Promise<void> => { | |
const user = await Db.fetchUser(env, request.userId); | |
if (user.hash = bcrypt(user.salt, request.oldPass)) { | |
const salt = Random.bytes(env, 32); | |
await Db.updateUser(env, { ...user, salt, hash: bcrypt(salt, request.newPass) }) | |
env.logger.log(`Changed password for user ${user.userId}`, "info") | |
} | |
else { | |
env.logger.log(`Password changed unauthorized: user ${user.userId}`, "error"); | |
throw new Error("Old password is invalid"); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment