Is it possible to implicitly state the possible options (based on the key
) for the name
property in the example below:
type TypeMap = {
A: 1 | 2 | 3;
B: 4 | 5 | 6;
};
type InnerType<TKey extends keyof TypeMap> = {
key: TKey;
name: TypeMap[TKey];
// + other properties;
};
type OuterType = {
x: InnerType; // Should be agnostic at this point
y: InnerType; // but typescript requires a generic definition
};
const foo: OuterType = {
x: { key: "A", name: 2 },
y: { key: "B", name: 4 },
};
The closest I can come up with would be something like the following, (but I would rather not have to change the format of the respective types):
type TypeMap = {
A: 1 | 2 | 3;
B: 4 | 5 | 6;
};
type InnerType = {
data: Partial<{ [key in keyof TypeMap]: TypeMap[key] }>;
// + other properties
};
type OuterType = {
x: InnerType;
y: InnerType;
};
const foo: OuterType = {
x: { data: { A: 2 } }, // not blocking me from using `{}` or `{ A: 2, B: 4 }` on the same object
y: { data: { B: 4 } },
};
Another approach would be the following (but does not limit based on the key):
type TypeMap = {
A: 1 | 2 | 3;
B: 4 | 5 | 6;
};
type InnerType = {
key: keyof TypeMap;
name: TypeMap[keyof TypeMap];
// + other properties;
};
type OuterType = {
x: InnerType;
y: InnerType;
};
const foo: OuterType = {
x: { key: "A", name: 2 }, // allows me to use `{ key: "A", name 4 }`
y: { key: "B", name: 4 },
};
Your version of InnerType
is a single object type whose properties are unions, which allows mixing of key
and name
in a way you don't want. You really want InnerType
to itself be a union of object types, like this:
type InnerType = {
key: "A";
name: 1 | 2 | 3;
} | {
key: "B";
name: 4 | 5 | 6;
}
That gives you the behavior you want:
const foo: OuterType = {
x: { key: "A", name: 1 },
y: { key: "B", name: 5 }
};
const badFoo: OuterType = {
x: { key: "A", name: 4 }, // error!
// Type '{ key: "A"; name: 4; }' is not assignable to type 'InnerType'.
// Type '4' is not assignable to type '1 | 2 | 3'.
y: { key: "A", name: 2 }
}
You can even write InnerType
in terms of TypeMap
so that it will automatically update if TypeMap
is updated:
type InnerType = { [K in keyof TypeMap]:
{ key: K, name: TypeMap[K] }
}[keyof TypeMap]
This InnerType
is a distributive object type as coined in microsoft/TypeScript#47109. It works by mapping over the keys K
of TypeMap
, computing the desired object type for each key K
, and then indexing into the mapped type with keyof TypeMap
, resulting in the desired union.
If for some reason you want to keep InnerType
generic so that InnerType<K>
corresponds to a particular K
constrained to keyof TypeMap
, you can do it:
type InnerType<K extends keyof TypeMap = keyof TypeMap> =
{ [P in K]:
{ key: P, name: TypeMap[P] }
}[K]
type InnerTypeA = InnerType<"A">
/* type InnerTypeA = {
key: "A";
name: 1 | 2 | 3;
} */
type InnerTypeB = InnerType<"B">
/* type InnerTypeB = {
key: "B";
name: 4 | 5 | 6;
} */
And note that K
defaults to keyof TypeMap
, so that InnerType
without a generic type argument is equivalent to the full union:
type InnerTypeBoth = InnerType
/* type InnerTypeBoth = {
key: "A";
name: 1 | 2 | 3;
} | {
key: "B";
name: 4 | 5 | 6;
} */
So you can get both generic-like and union-like behavior depending on your needs.