#Typing Issue

61 messages · Page 1 of 1 (latest)

digital osprey
#

Why is this.name underlined with the following error?

Type 'string | null' is not assignable to type 'If<Ready, string, null>'.
  Type 'null' is not assignable to type 'If<Ready, string, null>'.ts(2322)

Code:

type If<T extends boolean, A, B = null> = T extends true ? A : T extends false ? B : A | B;

class User<Ready extends boolean> {
    public name: If<Ready, string, null>;

    public constructor(name?: string) {
        this.name = name ?? null;
    }
}
dark mason
#
const readyUser = new User<true>();
console.log(readyUser.name); // should be a string because `Ready` is `true`, but is instead `null`
digital osprey
#

Nah, that's all working fine. The only issue is the intellisense.

dark mason
#

no, it isn't

dark mason
fallow mortar
#

if you don't provide any parameters to to contrusctor, name will always be null, no matter what you pass a generic

digital osprey
#
const user1 = new User("User 1");
user1.name // string | null

const user2 = new User<false>("User 2");
user2.name // null

const user3 = new User<true>("User 3");
user3.name // string
dark mason
#

user2.name will be "User 2".

fallow mortar
#

just try new User<true>().length;

digital osprey
dark mason
#

those are the inferred types

#

they break at runtime

digital osprey
#

Hmm

#

Well is there any way to realize that logic I want to apply?

dark mason
#

try running this in the playground

vital grailBOT
#
that_guy977#0

Preview:```ts
type If<
T extends boolean,
A,
B = null

= T extends true ? A : T extends false ? B : A | B

class User<Ready extends boolean> {
public name: If<Ready, string, null>

public constructor(name?: string) {
// @ts-expect-error
this.name = name ?? nu
...```

dark mason
digital osprey
#

Yeah kinda 😂 I like their way they are doing it with the Client class.

#

It's usefull to not make extra type as on a specific type the variables are defined.

desert mesa
#

We had a conversation the other day about how Discord.js's design of using a boolean generic to indicate online/offline is terrible.

digital osprey
#

Why? What would be a better solution?

dark mason
#

ts' type system is static, using types to denote mutable state isn't a great way to use them.

#

djs is written in js, so the interface works fine, but it wouldn't work with writing ts.

digital osprey
#

Yeah makes sense

desert mesa
#

I honestly have no idea why Discord.js designed it like that, it probably stems from the fact that Discord.js was written in JS and didn't have static typing in mind.

#

The fact that a client can magically transition itself between online/offline status, makes the code really hard to reason about in general.

digital osprey
#

Can you recommend me something to implement such thing in a better way?

desert mesa
#

Typically you would have two classes, an OfflineClient and an OnlineClient; OfflineClient#login method returns an instance of OnlineClient, and smilarly OnlineClient#logout returns an instance of OfflineClient

digital osprey
#

Yeah but so I would have a BaseClient class and both would inhert from the base class but with the depending definitons right?

dark mason
#

i suppose you could do away with the unready state and use a builder to get the ready state directly

although i guess that is kinda an unready state

desert mesa
#

You only need a base client class if you have things you want to reuse between online/offline clients, that's regular OOP.

digital osprey
#

Could you show me a proper implementation of what I want, so I don't use the TS typing system in a wrong way? Like in the simple User example I provided above?

dark mason
#

i mean, what exactly are you trying to make?

digital osprey
#

Well just that I can check if it is Ready, so I don't have to do an extra type check and that all optional props aren't anymore. Basically this but in the correct way. ```ts
type If<T extends boolean, A, B = null> = T extends true ? A : T extends false ? B : A | B;

class User<Ready extends boolean> {
public name: If<Ready, string>;

public constructor(name?: string) {
    this.name = name ?? null;
}

public isReady(): this is User<true> {
    return this.name !== null
}

}

const user = new User("Walkaisa");

if (user.isReady()) {
user.name.toUpperCase()
}

dark mason
#

no, i mean what is User supposed to be

desert mesa
#

Eh, not sure what the original example is about, but maybe something like:

class OfflineUser {
    login() {
        return new OnlineUser('foo')
    }
}

class OnlineUser {
    name: string

    constructor(name: string) {
        this.name = name
    }

    logout() {
        return new OfflineUser()
    }
}

Example usage:

const offlineUser = new OfflineUser()
// this is not allowed, it's not online yet
offlineUser.name

const onlineUser = offlineUser.login()
// now it's allowed
onlineUser.name
digital osprey
#

It was just a simple example to explain my problem

#

Thanks 👍

#

I'll then use your way.

dark mason
#

im asking what you're making... but ok

digital osprey
#

I want to use that in general

#

For example checking if the database is connected

desert mesa
#

That's the point with seperating offline/online into two classes: you don't have to check isReady() anymore.

#

Only the online class has .name/.database/etc.

digital osprey
#

Okay, makes sense.

#

But that's kinda messy having like two classes for that. You know what I mean?

desert mesa
#

Which is one of the issues I have with Discord.js's design, the fact that the same instance of class can magically transition between online/offline, means that you are forced to check .isReady() before you can use anything.

dark mason
#
class ConnectionBuilder {
  connect(): Connection {}
}

const connection = new ConnectionBuilder({...}).connect();
```![thonk](https://cdn.discordapp.com/emojis/961423622147309618.webp?size=128 "thonk")
desert mesa
digital osprey
#

Yeah but for example the Client is set to true automatically for events, so you don't have to make the checks if it is ready. That's why I think it's useful.

#

For example the interactionCreate event. Logically that can only be triggered if the Client is online, that's why Client<true> is enforced.

desert mesa
#

isReady() being a this is ... type guard really does not work well

#

Type guard can only narrow, never widen

#

So you can transition the variable from User<boolean> -> User<true>/User<false> -> User<never>

#

It's impossible to transition between User<false> <-> User<true>.

digital osprey
#

Hmm

#

That's why there is the If type.

desert mesa
#

The If type doesn't matter

#

Type guards can only narrow.