Skip to content

Instantly share code, notes, and snippets.

@arcdev1
Last active April 11, 2021 20:20
Show Gist options
  • Save arcdev1/52df01fa2a30725e6e6a563d9ee104b8 to your computer and use it in GitHub Desktop.
Save arcdev1/52df01fa2a30725e6e6a563d9ee104b8 to your computer and use it in GitHub Desktop.
FP-Value Object in TypeScript
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