I have a good pattern in a project I made to do this.
Let's say I have this interface (this is my own model):
type ITransaction = {
network_id: string;
transaction_id: string;
commission_amount: number;
order_date?: Date;
type: 'sale' | 'lead' | 'bonus' | 'claim' | 'none';
};
I create a new type called Mapper
type Mapper = {
[K in keyof ITransaction]: ((t: any) => ITransaction[K]) | string;
}
And I create an associated mapper that will map values of the API object to my current model :
const mapper: Mapper = {
transaction_id: 'transaction_id',
commission_amount: (transaction) => Math.round(parseFloat(transaction.commission_amount) * 100),
order_date: 'order_date',
type: ({ type }) => {
if (type === 4 || type === '4') return 'lead';
if (type === 5 || type === '5') return 'sale';
return 'none';
},
};
You can compute the value of your key in your model by two ways:
- By directly calling the key name in the API (you can call nested keys too, like
sale_amount.commission_amount
for instance)
- Or by getting directly the complete object and compute it
(like with commission or type)
Then, you just have to map it like this
const rawTransaction = await getTransactionFromApi();
const transactionInMyModel: ITransaction = mapNetworkTransaction(rawTransaction, mapper);
Where mapNetworkTransaction is:
const getValue = (obj: Record<string, unknown>, path: string | string[], separator = '.') => {
const properties = Array.isArray(path) ? path : path.split(separator);
return properties.reduce((prev, curr) => prev && prev[curr], obj);
};
const mapNetworkTransaction = (
object: Record<string, string>,
mapper: Mapper,
): ITransaction => {
const txn = { };
// eslint-disable-next-line no-plusplus
for (let i = 0; i < txnFields.length; i++) {
const txnField = txnFields[i];
const accessor = mapper[txnField];
if (accessor) {
if (typeof accessor === 'string') {
txn[txnField] = getValue(object, accessor);
} else if (typeof accessor === 'function') {
txn[txnField] = accessor(object);
}
}
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return txn;
};
With this pattern, each time you have to map an object API to your model, you'll just have to create a new mapper.
And you can do it generic by typing mapper like this:
type Mapper<T> = {
[K in keyof T]: ((t: any) => T[K]) | string;
}
and the mapper function like this:
const mapNetworkTransaction = <T>(
object: Record<string, string>,
mapper: Mapper,
): T