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;
}
}