Why are named functions treated differently than arrow functions in circular dependencies?

Viewed 143

I'm trying to roll my own dependency injection mechanism in TypeScript, and I've come across an interesting problem. When declaring my dependencies, I have to use named functions instead of arrow functions (which are my default typically), otherwise I get circular declaration errors.

Below are the set of types that make up my dependency resolver:

type CollectionTemplate = {
    [key: string]: (...args: any) => any
}

type RegistryTemplate = {
    [key: string]: CollectionTemplate
}

type RegistryCollection<R extends RegistryTemplate> = keyof R

type CollectionEntry<C extends CollectionTemplate> = keyof C

type BaseRegistry<R extends RegistryTemplate> = R

type BaseContext<R extends RegistryTemplate> = {
    [C in RegistryCollection<R>]: {
        [E in CollectionEntry<R[C]>]: ReturnType<R[C][E]>
    }
}

type BaseUseDep<R extends RegistryTemplate> =
    <C extends RegistryCollection<R>, E extends CollectionEntry<R[C]>>
        (collection: C, entry: E) => BaseContext<R>[C][E]

Now, this all works fine when I use named functions:

type DepRegistry = BaseRegistry<typeof registry>
type DepContext = BaseContext<DepRegistry>
type UseDep = BaseUseDep<DepRegistry>

function greet(deps: UseDep)
{
    return (name: string) =>
    {
        const console = deps('system', 'console')
        console.log(name)
    }
}

function printGreeting(deps: UseDep)
{
    return () =>
    {
        const greet = deps('social', 'greet')
        greet('John')
    }
}

const registry = {
    system: {
        console: () => console,
    },
    social: {
        greet,
        printGreeting,
    }
}

However, if I declare these functions with arrow functions instead of named functions, I get the following errors:

Type alias 'DepRegistry' circularly references itself. ts(2456)
Type alias 'UseDep' circularly references itself. ts(2456)

Why is this? Is it simply because I'm assigning a function to a variable instead of declaring an explicit function?

EDIT: Here are playgrounds with named functions and with arrow functions.

1 Answers

following answer partially explains the issue; it meant to be a comment

I think, the reason behind the discrepancy between named and arrow functions might be the lazy vs eager resolution of type signatures.

Anders Hejlsberg compared interface vs type resolution in this GitHub comment:

The trick is to make the recursive back references within interface types. This works because resolution of interface base types and interface members is deferred, whereas resolution of type aliases is performed eagerly.

A signature of a function can be specified with interface { (...params: Params): Return } and therefore the lazy resolution strategy is used by Typescript compiler and recursive definitions are allowed.

I do not know why arrow functions are treated differently but it seems their type signature is evaluated eagerly.


I often follow a similar pattern of dependencies register / injection and encountered the issue before. Never thought of replacing arrows with regular functions though. Good to know it is a possibility. You could as well consider avoiding circular dependencies in the first place.

The issue itself is highly-counterintuitive and very interesting at the same time.

Related