#combining generics without circular dependency

23 messages · Page 1 of 1 (latest)

odd jungleBOT
#

@toxic hawk Here's a shortened URL of your playground link! You can remove the full link from your message.

zaceno#7945

Preview:```ts
//I have a library and two modules A & B

// "library code" defines this type
type Foo<X> = (x:X) => X | Foo<X>

// Module A (imports BType from Module B):

type AType = {a: number, b: boolean}
const afunc :Foo<AType & BType> = (x) => {
//this part requires x to extend AType
...```

toxic hawk
#

Here's the full code snippet in case you don't want to follow the link:

//I have a library and two modules A & B

// "library code" defines this type
type Foo<X> = (x:X) => X | Foo<X>

// Module A (imports BType from Module B):

type AType = {a: number, b: boolean}
const afunc :Foo<AType & BType> = (x) => {
    //this part requires x to extend AType
    if (x.a > 3) return {...x, a: 0, b:!x.b}
    return x
}

// Module B (imports AType and afunc from module A)
type BType = {c: string, d: number}

const bfunc :Foo<AType & BType> = (x) => {
    //this part requires x to extend BType
    if (x.d > 3) x.c = x.c.toUpperCase()
    return afunc
}

// How can I break the cyclic dependency?
// I e how can I change the type definitions in module A 
// so that it does not need to import module B?
mild elbow
#

typescript is structurally-typed. the name BType is just an alias for {c: string, d: number}. so you can always write the {c: string, d: number} type inline in module A

#

but i'm curious why the cyclic dependency is a problem here? if it's just in the types it shouldn't have any impact at runtime, and the type checker shouldn't have issues with it. it doesn't look like your runtime code has a cycle (bfunc depends on afunc but not the other way around)

toxic hawk
#

I’m not blocked or anything by the cyclic dependency. But I started playing around with the idea of making module A (or the equivalent in my real example) more loosely coupled from the app, so it could potentially be reused in other apps/situations. For that same reason I’m not really satisfied with the “just express the type explicitly” solution because that “knowledge” is supposed to be encapsulated in module A.

glad jacinth
#

The idea is that Module A would export AType and module B could import it.

#

That's not 'breaking encapsulation' of what module A is supposed to contain.

toxic hawk
#

True, but also, module A should not need to import anything from module B (nor depend on knowing anything about module B) - and that’s the part I can’t figure out (if it’s even possible)

glad jacinth
#

It does seem to need to import the BType, but that's fine?

toxic hawk
#

No that’s the problem. I can’t figure out how to make it work if I don’t import BType in A

glad jacinth
#

Yeah, that's why I think you should just let both import each other.

#

You can theoretically pull one or both out to a third file that can be imported by both... but there's really no reason to do that: there isn't a reason to avoid circular type dependencies.

toxic hawk
#

Yeah it works fine with the circular dependency. I was just wondering if it was possible to break it somehow so module A could be an independent reusable module. But if it isn’t possible I guess it isn’t.

#

You’re right I could make a third module both could import from to break the cycle probably. But it wouldn’t make module A “standalone”. I think… maybe I should play with that idea a bit to make sure.

mild elbow
#

even theoretically i don't see what third alternative there could possibly be besides importing the type from elsewhere or defining it locally

toxic hawk
#

I was thinking there might be some way using generics and type inference or something. Been playing around with that a bit without luck.

mild elbow
#

ah, like not annotating the type of the x argument at all? unfortunately TS can't infer argument types without any context... in a function like x => foo(x) the type of x needs to be known to check the body of the function and make sure the foo call is valid. it doesn't go "backwards" to infer the type of x from the type signature of foo

#

the only language i can think of offhand that does that kind of inference is Haskell

toxic hawk
#

Turns out I was overthinking it probably. This solves the problem in the example (remains to be seen if I can apply it to my original problem but I think so):

//I have a library and two modules A & B

// "library code" defines this type
type Foo<X> = (x:X) => X | Foo<X>

//-----------------------------------------
// Module A (imports BType from Module B):

type AType = {a: number, b: boolean}

const afunc = <X extends AType>(x:X) => {
    //this part requires x to extend AType
    if (x.a > 3) return {...x, a: 0, b:!x.b}
    return x
}

//-----------------------------------------

//-----------------------------------------
// Module B (imports AType and afunc from module A)

type BType = {c: string, d: number}

const bfunc:Foo<BType & AType> = (x) => {
    //this part requires x to extend BType
    if (x.d > 3) x.c = x.c.toUpperCase()
    return afunc
}
//-----------------------------------------

#

Since afunc can be defined to match the Foo type I don't need to try to declare it as a Foo-type function. It just is (this is the part I was overthinking).

#

So I could use the generic function and there specify that it can be any type as long as it extends AType.

#

At first I was trying for something like:

const afunc:Foo<X extends AType> = (x:X) => {
    //this part requires x to extend AType
    if (x.a > 3) return {...x, a: 0, b:!x.b}
    return x
}

which of course doesn't even make sense.