Skip to content

Instantly share code, notes, and snippets.

@mkuchak
Last active February 12, 2024 19:29
Show Gist options
  • Save mkuchak/959ab4ee0de5e43f78289b32f8a1656c to your computer and use it in GitHub Desktop.
Save mkuchak/959ab4ee0de5e43f78289b32f8a1656c to your computer and use it in GitHub Desktop.
TypeScript alias alternative to Static Factory Method: avoid static create and restore methods on DDD, just simplify
export class Email {
constructor(private _value: string) {
if (!this.isValid()) {
throw new Error("Invalid email");
}
}
private isValid(): boolean {
/**
* Must have a valid username and domain.
* Allows letters, digits, hyphens, and underscores in the username.
* Allows letters and hyphens in the domain.
* Must have a valid top-level domain (TLD).
* Forbids the use of alias with '+' in the username.
*/
const regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return regex.test(this._value);
}
get value(): string {
return this._value.trim();
}
}
declare global {
type ExcludeMethods<T> = Pick<
T,
{
[K in keyof T]: T[K] extends Function ? never : K;
}[keyof T]
>;
type IfEquals<X, Y, A = X, B = never> = (<T>() => T extends X
? 1
: 2) extends <T>() => T extends Y ? 1 : 2
? A
: B;
type WritableKeys<T> = {
[P in keyof T]-?: IfEquals<
{ [Q in P]: T[P] },
{ -readonly [Q in P]: T[P] },
P
>;
}[keyof T];
type ExtractClassProps<T> = ExcludeMethods<Pick<T, WritableKeys<T>>>;
// type ClassProps<T, U = ExtractClassProps<T>> = Omit<
// ExtractClassProps<T>,
// keyof U
// > &
// U;
type ClassProps<
T,
U = ExtractClassProps<T>,
V extends keyof T = never
> = Omit<ExtractClassProps<T>, keyof U | V> & U;
}
export {};
import { User } from "./User";
const main = async () => {
const newUser = new User({ // naturally replaces the static create method
email: "johndoe@gmail.com",
password: "123456789",
surname: "Doe",
givenName: "John",
middleName: "Smith",
});
console.log(newUser.id); // get the new generated uuid
await newUser.password.hash(); // hash the password
console.log(newUser.password.value); // get the hashed password as string
console.log(newUser.fullName); // `John Smith Doe`
const oldUser = new User({ // naturally replaces the static restore method
id: "bf54c5a8-3ec4-4c5c-9577-4ec638dbf00f",
email: "janedoe@gmail.com",
password: "$argon2...", // hashed password
surname: "Doe",
givenName: "Jane",
});
const oldUserPassword = "123456789";
console.log(oldUser.id); // get the old uuid `bf54c5a8-3ec4-4c5c-9577-4ec638dbf00f`
console.log(oldUser.email.value); // get the email as string from Email value object `janedoe@gmail.com`
console.log(await oldUser.password.verify(oldUserPassword)); // verify the password (returns a boolean promise)
};
main();
import argon2 from "argon2";
export class Password {
constructor(private _value: string) {
if (argon2.needsRehash(this._value) && !this.isValid()) {
throw new Error("Invalid password");
}
}
private isValid(): boolean {
/**
* At least 8 characters.
* At least one lowercase letter.
* At least one uppercase letter.
* At least one number.
* At least one special character.
*/
const regex =
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
return regex.test(this._value);
}
async hash(): Promise<void> {
if (!argon2.needsRehash(this._value)) return;
this._value = await argon2.hash(this._value);
}
async verify(password: string): Promise<boolean> {
return await argon2.verify(this._value, password);
}
get value(): string {
return this._value;
}
}
import crypto from "node:crypto";
import { Email } from "./Email";
import { Password } from "./Password";
export type UserProps = ClassProps<
User,
{ // this second type is optional and will replace the original User class props
email: string;
password: string;
}
>;
/**
* UserProps will be:
* {
* id?: string;
* email: string;
* password: string;
* surname: string;
* givenName: string;
* middleName?: string;
* }
*/
export class User { // entity
id?: string = crypto.randomUUID();
email: Email;
password: Password;
surname: string;
givenName: string;
middleName?: string;
constructor(props: UserProps) { // props allows you to use a single object instead of stacking multiple arguments in the constructor
Object.assign(this, props);
this.email = new Email(props.email); // value object
this.password = new Password(props.password); // value object
}
get fullName(): string {
return `${this.givenName} ${this.middleName ? `${this.middleName} ` : ""}${this.surname}`;
}
// more behavior...
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment