0

I have to declare an interface for a team object:

export interface Team{
  memberUid?: {
    mail: string
    name: string
    photoURL: string
  }
  startDate: Timestamp
  endDate: Timestamp
  agenda: Array<{
    date: Date | Timestamp
    title: string
    description: string
  }>
}

There will be several members Uids. I don't know how to declare it in TypeScript.

I tried:

[memberUid: string] : {
  mail: string
  name: string
  photoURL: string
}

But then TypeScript understand that all the fields in Team should have this structure.

What would be the correct way to handle this?

Thanks

Xiiryo
  • 3,021
  • 5
  • 31
  • 48

1 Answers1

2

Let me start by saying that I hate this structure. If there's a way to rewrite your code to a more logical Team type, then do it. But can it be done? Yes, it can. Team will have to be a type rather than an interface though, because interfaces can only have known keys.

Let's start by breaking it into pieces.

TeamBase defines the properties which are always present:

interface TeamBase {
  startDate: Timestamp
  endDate: Timestamp
  agenda: Array<{
    date: Date | Timestamp
    title: string
    description: string
  }>
}

UidProperties defines the contents of the unknown key:

interface UidProperties {
    mail: string
    name: string
    photoURL: string
}

Here's a utility type which can be used to declare a property based on the type of the key by using mapped types:

type HasProperty<Key extends keyof any, Value> = {
    [K in Key]: Value;
}

If the generic Key is a string literal then the only possible value for K in Key is that exact string, so HasProperty<"myKey", string> resolves to { myKey: string; }

So now we need to define the type Team. We know that it needs to include all of the properties of TeamBase and we know that there is some string key for which the value is UidProperties. An important fact which was causing you problems before is that this key cannot be one of the keys of TeamBase. So instead of using string, we use Exclude<string, keyof TeamBase>.

You can use HasProperty<...> to say that every string key which is not a key of TeamBase has a value of UidProperties, but I think it is better to use Partial<HasProperty<...>> which says that any of these unknown string keys could be UidProperties but could also be undefined. When you access an unknown key, you probably want to double check that value anyways.

So our final type for Team is:

export type Team = TeamBase & Partial<HasProperty<Exclude<string, keyof TeamBase>, UidProperties>>

This works as expected. Because we have allowed for the possibility that uid is undefined, you cannot access properties of uid without ruling out undefined via optional chaining with ? or an if statement, etc.

function f1(team: Team) {

    const startDate: Timestamp = team.startDate;

    const uid: UidProperties | undefined = team['randomKey']

    const name = uid?.name;

    if ( uid !== undefined ) {
        const mail = uid.mail;
    }
}
Linda Paiste
  • 38,446
  • 6
  • 64
  • 102