Skip to content

Instantly share code, notes, and snippets.

@zallesov
Created February 17, 2024 22:19
Show Gist options
  • Save zallesov/2876917d24bbf201ca30cabbee4b3588 to your computer and use it in GitHub Desktop.
Save zallesov/2876917d24bbf201ca30cabbee4b3588 to your computer and use it in GitHub Desktop.
Code snippet for medium article
import { DateTime } from 'luxon';
import { z } from 'zod';
/**
* Replace type V1 with type V2.
*/
type Replace<T, V1, V2> = T extends V1 ? V2 : never;
/**
* Replace all fields of type V1 with fields of type V2.
*/
export type DeepReplace<T, V1, V2> = {
[P in keyof T]: T[P] extends V1
? Replace<T[P], V1, V2>
: T[P] extends object
? DeepReplace<T[P], V1, V2>
: T[P];
};
export function deepReplace<T, V1, V2>(
obj: T,
isFunc: (value: any) => value is V1,
convert: (value: V1) => V2,
): DeepReplace<T, V1, V2> {
const isObject = (value: any): value is Object =>
value !== null && typeof value === 'object' && !Array.isArray(value);
const replace = (current: any): any => {
if (Array.isArray(current)) {
// If current value is an array, recursively replace each element
return current.map((element: any) => replace(element));
} else if (isFunc(current)) {
// Replace Date with its string representation
return convert(current);
} else if (isObject(current)) {
// Recursively replace Date with string for all object properties
const result: any = {};
for (const key of Object.keys(current)) {
result[key] = replace(current[key]);
}
return result;
} else {
// Return the value unchanged if it's neither Date nor object nor array
return current;
}
};
return replace(obj);
}
export const toDTO = <T>(obj: T) =>
deepReplace<T, DateTime, string>(
obj,
(value: any): value is DateTime => value instanceof DateTime,
(value: DateTime) => value.toISO() || "",
);
export const toDAO = <T>(obj: T) =>
deepReplace<T, DateTime, Date>(
obj,
(value: any): value is DateTime => value instanceof DateTime,
(value: DateTime) => value.toJSDate(),
);
const DateTimeType = z.union([
z.string(),
z.custom<DateTime>(arg => arg instanceof DateTime),
z.date(),
z.number()
]).transform((value) => {
if (typeof value === 'string') {
return DateTime.fromISO(value);
} if (typeof value === 'number') {
return DateTime.fromMillis(value);
}else if (value instanceof Date) {
return DateTime.fromJSDate(value);
} else {
return value;
}
});
// ZOD defintions
const Currency = z.enum(['$', '€', '£']);
const Amount = z.object({
value: z.number(),
currency: Currency,
});
const Payment = z.object({
createdAt: DateTimeType,
amount: Amount,
});
// Derived model types
type Currency = z.infer<typeof Currency>;
type Amount = z.infer<typeof Amount>;
type Payment = z.infer<typeof Payment>;
// DTO and DAO types derived from Payment
type DTO<T> = DeepReplace<T, DateTime, string>;
type DAO<T> = DeepReplace<T, DateTime, Date>;
type PaymentDTO = DTO<Payment>;
// type PaymentDAO = {
// createdAt: string
// amount: {
// value: number;
// currency: Currency;
// };
// };
type PaymentDAO = DAO<Payment>;
// type PaymentDAO = {
// createdAt: Date
// amount: {
// value: number;
// currency: Currency;
// };
// };
const payment: Payment = {
createdAt: DateTime.local(),
amount: {
value: 100,
currency: '$',
},
};
// Example usage
// PaymentDTO
const paymentDto = {
createdAt: "2022-01-01T00:00:00.000Z",
amount: {
value: 100,
currency: "$",
},
};
// PaymentDAO
const paymentDao = {
createdAt: new Date(),
amount: {
value: 100,
currency: "$",
},
};
const paymentFromDto: Payment = Payment.parse(paymentDto);
const paymentFromDao: Payment = Payment.parse(paymentDao);
console.log("from DTO", paymentFromDto);
console.log("from DAO", paymentFromDao);
const dtoFromPayment: PaymentDTO = toDTO(payment);
const daoFromPayment: PaymentDAO = toDAO(payment);
console.log("DTO", dtoFromPayment);
console.log("DAO", daoFromPayment);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment