#Typeguard in class instance not properly narrowed down

13 messages · Page 1 of 1 (latest)

cursive salmon
#

I made a very simple example here:

class Test<Initialized extends boolean = boolean> {
    property?: Initialized extends true ? string : Initialized extends false ? null : string | null

    private initialized = false

    public initialize() {
        this.initialized = true
    }

    public isInitialized(): this is Test<true> {
        return this.initialized
    }
}

const instance = new Test()

const test = (instance: Test) => {
    if (instance.isInitialized()) return

    instance.initialize()
    // ^? (parameter) instance: never
    // Why is this not const instance: Test<true>?
}

test(instance)

Could someone explain why this isn't working as expected please?

https://www.typescriptlang.org/play?#code/MYGwhgzhAEAqCmEAuAeAkgOwJZK2EWAXvACbTwAeS8GJMARgPaMjxgbQC80TLbGAPmgBvAFDQJ0AA4AnRlPgykATwD8ALmiYceAsTKVqtGEhkBXeNFXRkMrBgDm0Tdtz4ipclRp1oAM3wIS2sMMxAQZxtTeycAH2hQ8NFxSVksADcwamh7HXd9Ln9A+GTJaTN6AmAc7Dc9eAAKAEoRFLKJJAALLAgAOly6jzJuUws26ABfUtSKqpyIV10h5s0unvm4RFRR+CExdskZeCQzGQ41voGl-XGpqdFgRgxkGuR2YEtuDHgAd03kZrJR7PJDQagvbgNexvDAfTQIZAtTh7cZYPzQKEg97wfoLWrXUjNFpHE5naYSaFIbH9fH5RpNcYAekZ0AAeqomSyAOqdZQbC4JRig4EvSnY+FbFA7AQc+7gpCYmEfJpAA

bitter mossBOT
#

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

silver_lk#0

Preview:```ts
class Test<Initialized extends boolean = boolean> {
property?: Initialized extends true
? string
: Initialized extends false
? null
: string | null

private initialized = false

public initialize() {
this.initialized = true
}

public isInitialized(): this is Test<t
...```

fallen tartan
#

@cursive salmon I'm having a hard time pointing at an exact smoking gun, but when you work with generic classes, TS looks at the actual structure of the resulting type.

So TS isn't just looking at Test<boolean> - Test<true> = Test<false> it's looking at the structure of a Test<boolean> and comparing it to a Test<true> and since the only difference is a conditional type'd property and those are kind of weird as comparisons go.

#

If you actually typed initialized as Initialized then there'd be a clear structural difference that TS can work with... but you'll have a lot of compile errors trying to do that.

#

To be honest, I don't think using a generic to try to express internal state of a mutable class is a good idea.

bleak oak
#

I wonder if Discord.js has set a bad example and people are trying to follow it.

cursive salmon
fallen tartan
#

It depends on the structural details of Message.

bitter mossBOT
#
class Foo<T extends boolean> {
  constructor(private val: boolean) {}

  isInitialized(): this is Foo<true> {
    return this.val;
  }
}

class Bar<T extends boolean> {
  constructor(private val: T) {}

  isInitalized(): this is Bar<true> {
    return this.val;
  }
}

declare const foo: Foo<boolean>
if(foo.isInitialized()) {
  foo
// ^? - const foo: Foo<boolean>
}
declare const bar: Bar<boolean>;
if(bar.isInitalized()) {
  bar
// ^? - const bar: Bar<true>
}
fallen tartan
#

These behave differently because Bar actually structurally changes based on the type param, Foo does not.

#

Realistically, if you're doing initialization, I'd handle that before constructing the class:

class MyClass {
  constructor(stuffTheClassNeeds: { data: string }) {}

  static create(): Promise<MyClass> {
    // does the initialization logic then returns an instance of MyClass
  }
}
cursive salmon
#

interesting 🤔 thanks for the examples 😄
the static class method is a clever alternative