Reputation: 45657
Let's say I have an interface like this:
interface Cat {
weight: number;
name: string;
adoptable: boolean;
}
I want to define the following interface that can be used to set values on Cat
:
interface CatUpdate {
field: keyof Cat;
value: ???
}
and I want TypeScript to enforce that value is the appropriate type based on field. E.g., this should be invalid:
const x: CatUpdate = {
field: "name",
value: 12
}
How do I define the type of CatUpdate.value
?
Upvotes: 10
Views: 5066
Reputation: 327754
My inclination would be to make CatUpdate
a union type and not an interface, like this:
interface Cat {
weight: number;
name: string;
adoptable: boolean;
}
type PropUpdate<T extends object> = {
[K in keyof T]: { field: K; value: T[K] }
}[keyof T];
type CatUpdate = PropUpdate<Cat>;
/*
type CatUpdate = {
field: "weight";
value: number;
} | {
field: "name";
value: string;
} | {
field: "adoptable";
value: boolean;
}
*/
That should work well enough on your mentioned use case:
const x: CatUpdate = {
field: "name",
value: 12
} // error!
And you should be able to compose those (CatUpdate | DogUpdate
) if you need to, although without a particular use case it's hard to know if such composition is appropriate (Can Cat
and Dog
be discriminated from each other? That is, is Cat & Dog
impossible? If Cat & Dog
is possible, then CatUpdate | DogUpdate
can only be used to update a Cat & Dog
. If it's impossible, then CatUpdate | DogUpdate
should probably be replaced with a discriminated union instead like (CatUpdate & {kind: "CatUpdate"}) | (DogUpdate & {kind: "DogUpdate"})
.)
Upvotes: 13
Reputation: 4163
Unfortunately, TypeScript does not have what you are looking for, but you can hardcode the types:
type CatUpdate
= { field: 'weight', value: number }
| { field: 'name', value: string }
| { field: 'adoptable', value: boolean }
Upvotes: 1
Reputation: 1175
One way is to use a generic type:
interface CatUpdate<T extends keyof Cat> {
field: T;
value: Cat[T];
}
It works, but it means you'll need to be a little bit more verbose when creating the object.
// Error
const x: CatUpdate<"name"> = {
field: "name",
value: 12
}
// No error
const y: CatUpdate<"name"> = {
field: "name",
value: "Dave"
}
One solution would be to use a function for instantiation which can infer the type based on the parameters:
function getCatUpdate<T extends keyof Cat>(field: T, value: Cat[T]): CatUpdate<T> {
return {
field,
value
}
}
Which you could call like:
// Error
const x = getCatUpdate("name", 12);
// No error
const y = getCatUpdate("name", "Dave");
Upvotes: 3