Last active
April 11, 2021 20:20
-
-
Save arcdev1/52df01fa2a30725e6e6a563d9ee104b8 to your computer and use it in GitHub Desktop.
FP-Value Object in TypeScript
This file contains hidden or 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
|
This file contains hidden or 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
interface ValueObject<TName extends string, TValue> { | |
type: TName; | |
equals(other: ValueObject<TName, TValue>): boolean; | |
valueOf(): TValue; | |
toJSON(): TValue; | |
} | |
interface ValueObjectType<TName extends string, TValue> { | |
is(value: ValueObject<TName, TValue>): boolean; | |
of(value: TValue): Readonly<ValueObject<TName, TValue>>; | |
} | |
const makeValueObjectType = <TName extends string, TValue>({ | |
name, | |
validate, | |
}: { | |
name: TName; | |
validate: (value: unknown) => TValue; | |
}): ValueObjectType<TName, TValue> => | |
Object.freeze({ | |
of: (value: TValue): ValueObject<TName, TValue> => { | |
validate(value); | |
return Object.freeze({ | |
type: name, | |
equals: (other: ValueObject<TName, TValue>) => | |
other.valueOf() === value, | |
valueOf: () => value, | |
toJSON: () => value, | |
}); | |
}, | |
validate | |
}); | |
const isNullOrUndefined = (value:unknown): value is null | undefined => value == null; | |
const isString = (value: unknown): value is string => typeof value === "string"; | |
const isEmpty = (value: {length: number}) => !value?.length | |
const isShorterThan = (length: number) => (value: {length: number}) => value.length < length | |
const notNullOrUndefined = (label: string) => (value: unknown) => { | |
if (isNullOrUndefined(value)) { | |
throw new TypeError(`${label} cannot be null or undefined.`) | |
}; | |
return value; | |
}; | |
const nonEmptyString = (label: string) => (value: unknown) => { | |
if (!isString(value)) { | |
throw new TypeError(`${label} must be a string.`); | |
} | |
if (isEmpty(value.trim())) { | |
throw new TypeError(`${label} cannot be empty.`); | |
} | |
return value; | |
}; | |
const minLengthString = (label: string) => (minLength: number) => ( | |
value: string | |
) => { | |
if (isShorterThan(minLength)(value.trim())) | |
throw new TypeError( | |
`${label} cannot have fewer than ${minLength} characters.` | |
); | |
return value; | |
}; | |
const validateFirstName = (value: unknown) => { | |
const label = "firstName"; | |
const minLength = 2; | |
notNullOrUndefined(label)(value); | |
nonEmptyString(label)(value); | |
return minLengthString(label)(minLength)(value as string); | |
}; | |
type FirstName = ValueObject<"FirstName", string>; | |
const FirstName = makeValueObjectType<"FirstName", string>({ | |
name: "FirstName", | |
validate: validateFirstName, | |
}); | |
// FirstName.of(null as unknown as string) // TypeError: FirstName cannot be null or undefined. | |
// FirstName.of(" ") // TypeError: FirstName cannot be empty. | |
// FirstName.of("J") // TypeError: FirstName cannot have fewer than 2 characters. | |
const jada = FirstName.of("Jada"); | |
console.log(FirstName.is(jada)) // true | |
sayMyName(jada) // "Jada" | |
type LastName = ValueObject<"LastName", string>; | |
const LastName = makeValueObjectType<"LastName", string>({ | |
name: "LastName", | |
validate: nonEmptyString("LastName"), | |
}); | |
function sayMyName(firstName: FirstName) { | |
// We are confident that firstName is a valid first name | |
// so, we can just use it. | |
// No defensive programming necessary. | |
console.log(firstName) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment