1

I was wondering if something like this is possible.

I am using a NestJS and TypeORM for my project, and want to use transactions. I am trying to replace this usage:

getConnection().transaction(async (transactionManager) => {
    const repositoryOne = transactionManager.getCustomRepository(RepostiroyOne) // obviusly types are inherited here
    const repositoryTwo = transactionManager.getCustomRepository(RepostiroyTwo)

   // code in transaction
})

with something like this:

const transaction = async(callback, repositories) => {
    const getTransactionRepositories = (manager: EntityManager) => {
        return repositories.map((repository) => manager.getCustomRepository(repository));
    };
    return getManager().transaction(async (transactionManager) =>
        callback(...getTransactionRepositories(transactionManager)),
    );
};

then in the code to use it:

transaction(async (repoOne, repoTwo) => {
   // code to run inside transaction
   // but types here are not inherited
   // if i say repoOne: RepositoryOne it is working fine, but I tried to minimize repetition and
   // would like if repoOne is a type of RepositoryOne without manually adding it
}, [RepositoryOne, RepositoryTwo])

So as I mentioned in the first example what I am trying to exclude:

const repositoryOne = transactionManager.getCustomRepository(RepositoryOne)

then this repostiryOne becomes:

repositoryOne: RepositoryOne

So in my function, 1st param, e.g. repoOne would have a type of RepositoryOne provided as a first item in the array and so on.

note: Talking strictly about TypeORM tranasaction decorators, I won't use them since there are some issues with nest testing module and typeorm decorators.

Any help is appreciated

1 Answer 1

1

I believe the best way to write this is to break the function up into a nested functions (currying), use rest parameters, and use mapped types:

import { EntityManager, getManager } from "typeorm";

type MapToInstanceTypes<T> = {
    [key in keyof T]: T[key] extends new() => infer U ? U : never
};

function transaction<T extends (new () => any)[]> (
    ...repositories: T
) {
    return async function(
        callback: (...repos: MapToInstanceTypes<T>) => Promise<void>,
    ) {
        const getTransactionRepositories = (manager: EntityManager) => {
            return repositories.map((repository) => manager.getCustomRepository(repository));
        };
        return getManager().transaction(async (transactionManager) =>
            callback(...getTransactionRepositories(transactionManager) as any),
        );
    }
};

This can be used like so:

class X {
    public x: number =  5;
}
class Y {
    public y: number =  5;
}

transaction(X, Y)(async (a, b) => {
    a.x;
    b.y;
});

The nested functions are required to force typescript to infer one argument first (the repository types, T), and then make the callback typed based on that (otherwise it becomes too complex for typescript to infer correctly).

The rest parameter is required to prevent arrays from degenerating to union types. For example, [0, ""] degenerates to (string | number)[], where a better type is actually: [number, string].

The mapped type allows you to convert from a class type to a class instance. Usually you can do this with InstanceType<T>, but I ran into some issues with that due to the argument being an array instead of an object.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.