Skip to content

Instantly share code, notes, and snippets.

@JSuder-xx
Created April 18, 2021 16:19
Show Gist options
  • Save JSuder-xx/66e7a2c362eab785c11dae41c9b2a124 to your computer and use it in GitHub Desktop.
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.
/**
* 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