#Need some suggestions for TypeScript generics (?)

1 messages · Page 1 of 1 (latest)

peak gazelle
#

I have a SnackBarService in which I expose a function:

public openSnackBar(message: string, state: SnackBarState, data?: any): void {
  message = this.setMessageText(message, data);

  this._snackBar.openFromComponent(SnackBarComponent, {
    panelClass: state,
    duration: 3500,
    data: {
      message: message,
      state: state
    } as SnackBarData
  })
}

private setMessageText(message: string, data?: any): string {
  switch (message) {
    case 'U_03':
      message = `Welcome back ${data.username}`
      break;
  }

  return message
}

The data could be the return value of this Apollo mutation:

    return this.apollo.mutate<LoginUser>({
      mutation: LOGIN_USER,
      variables: {
        email: formData.email,
        password: formData.password
      }
    })

The returned value from Apollo will always (at least I think) be a object containing data and a message:

{
  data: User
  message: String
}
{
  data: Movie
  message: String
}

So now I'm using data: any since the returned object can be of many types. I'm pretty sure I need to use TypeScript generics for this, but I would love a pointer in the right direction

analog kelp
#

Why would u want generics here ?

#

As far as I can see, all u care about is the fact that it has a username property.

#

However, I can see u have both User and Movie, can u show how setMessageText looks like when its a movie ?

#

Personally, I would decouple the snackbar from things like Movie and User, but use something like this:

#
private setMessageText(message: string, data?: { message: string }): string {
  switch (message) {
    case 'U_03':
      message = `Welcome back ${data?.message}`
      break;
  }

  return message
}
#

But that gives u two messages, while I think the first one is more a message code. But I also think u dont need that anymore if u decouple it from Users and Movies.

agile lotus
#

IMHO, the snack bar service shouldn't be the one responsible to format a message based on some unknown data. The caller should do that instead.

analog kelp
#

Exactly, thats what I was going to as well. Just have the snackbar accept a message, and build the message externally based on Movies / Users.

#

So long story short, remove setMessageText entirly to outside of the service.

#

You could still have another service that is responsible for building the message, then opening the snackbar.

#

But that would still not need generics.

#

I would use union types I think.

peak gazelle
#

I agree the setMessage should be maybe a util function and shouldn't be in the service but that's more of a code quality concept than a functional one.

I don't want each component that sends a message to a snackbar to create their own message. I want a function that based on the data type (movie, user, notification) returns a message.

It might not have been clear (and now I have to explain it I see why). But message is the actual "code" returned from the back-end. For example U_03 or M_01 (when a movie has been added). The data (the returned movie object) has the movie data, title, release date etc.

I considered using union types, but isn't that a lot of code?

type SnackbarData = {
  data: Movie;
  message: 'movie';
} | {
  data: User;
  message: 'user';
} | {
  data: Profile;
  message: 'profile';
};

Then you have to declare each type right?

analog kelp
#

Not sure how generics would help.

#

So are u saying the message code determines what property you are going to access?

peak gazelle
#
export interface Movie {
  title: string
}

export interface User {
  username: string
}

export interface SnackBarDataType<T> {
  data: T
}

type SnackBarDataTypes = User & Movie;

@Injectable({
  providedIn: 'root'
})
export class SnackbBarService<T extends SnackBarDataTypes> {
  public openSnackBar(message: string, state: SnackBarState, data?: SnackBarDataType<T>): void {
    message = this.setMessageText(message, data);
  }

  private setMessageText(message: string, data?: SnackBarDataType<T>): string {
    switch (message) {
      case 'U_03':
        message = `Welcome back ${data?.data.username}`
        break;

      case 'M_01':
        message = `Movie ${data?.data.title} has been added!` 
        break;
    }

    return message
  }
}

This is working for me (of course I would abstract the interfaces etc)

analog kelp
#

Why does it need to be generic?

#

Why cant data: T just be data: SnackBarDataTypes ?

#

I think you are focussing on generics, but there are other things you can do imho.

#

E.g. u could have overloads that ensure whenever you pass U_03 as the code, TypeScript knows its a User, or when u pass M_01, it knows its a movie.

#

Also type SnackBarDataTypes = User & Movie is a bit weird.

#

Its never a user and a movie, it's always a user or a movie.

#

(making it a user and a movie may look simpler, but it's definetly now what u want)

peak gazelle
#

Intersection Types
Intersection types are closely related to union types, but they are used very differently. An intersection type combines multiple types into one

#

Looks like you're right about that

analog kelp
#

Yeah u use it for other things, like:

type Admin = User & Employee

peak gazelle
#

Ah yea that makes sense

analog kelp
#

Give me 5 min

#

Here u can see, if I pass 'U_01', I can not even pass a movie.

#

And by using if ('title' in data), TypeScript knows its a movie inside that if, and a User outside.

#

So I would make a single function that is responsible for building the message.

#

Like shown in the stackblitz.

#

No need for generics at all

#

Yet full type safety an no any.

peak gazelle
#

Thanks Frederik, that's pretty cool!

analog kelp
#

Always focus on what you want to achieve, not how. Generics was, imho, the wrong focus here.