Use of decorator function causes circular reference type errors

Viewed 134

My code was working just fine until I tried to wrap a function definition with a decorator (by "decorator" I just mean a higher-order function that takes one function as a param and return another function, I'm not talking about the upcoming decorator syntax). When I use this decorator, I get the error Type alias 'Thing' circularly references itself. Why is this happening?

Below is the broken code sample (This is an extremely simplified version that tries to simply showcase the problem I'm running into).

const decorate = (fn: () => boolean) => fn

const thing = {
    fn: decorate(() => {
        type Thing = typeof thing
        const thing_: Thing = thing
        return 'fn' in thing_
    })
}

Compare that to the following code snippet, which works just fine. I'm just removing the call to decorate (which is just an identity function in this simplified example).

const decorate = (fn: () => boolean) => fn

const thing = {
    fn: () => {
        type Thing = typeof thing
        const thing_: Thing = thing
        return 'fn' in thing_
    }
}

I can even take my broken example and replace the decorate function with the following generic function, and it'll magically start working, even though the two definitions should be functionally equivalent in the specific way they're being used.

const decorate = <T>(fn: T): T => fn

Update: For anyone who's interested, here's how I ended up fixing my issue

I had to explicitly tell TypeScript what the type of my function was, so I put it in an IFEE and assigned it to a variable with a declared type, like this:

const decorate = (fn: () => boolean) => fn

const thing = {
    fn: (() => {
        type MyFnType = () => boolean
        const fn: MyFnType = decorate(() => {
            type Thing = typeof thing
            const thing_: Thing = thing
            return 'fn' in thing_
        })
        return fn
    })()
}

It's not pretty, but it works around this issue.

(Note that this isn't an answer to my own question, as my question is asking why I have to do a workaround like this, I'm not necessarily asking how to do it).

2 Answers

Let's write in this way

const decorate = (fn: () => boolean) => fn

const t = () => {
        type Thing = typeof thing
        const thing_: Thing = thing
        return 'fn' in thing_
    };

Now if you write

const thing = {
    fn: decorate(t)
}

As this involve invoking decorate with t as argument, the compiler requires t be assignable to ()=>boolean before it can proceed to infer the type of thing. So it must type check t before the type of thing is inferred, causing circular references. i.e. in this case, the type of thing matters to t and the type of t also matters to thing.

In contrast when you write

const thing = {
    fn: t
}

There is no requirement on what type t should be. The compiler can proceed to infer the type of thing, just infer the type of fn to whatever t happens to be, without caring what it actually is. Therefore no need to type check t beforehand. So in this case, the type of thing matters to t but the type of t does NOT matter to thing.

I ended up asking about this on the TypeScript repository (feel free to take a peek over here. It sounds like the reason TypeScript is unable to automatically determine the types in my example code snippet simply boils down to the fact that doing so would require having to completely rewrite a lot of the TypeScript compiler, and add a lot of complexity to it, for something they feel isn't needed enough to warrant this sort of rewrite and added complexity, which is certainly understandable.

Feel free to look at the link to see the direct conversation.

Related