Reputation: 1779
I think this is possible but I'm having trouble searching for the right keywords. I have a method that is used to set multiple properties of an object and I'd like it type safe.
interface ICase {
caseDetails: ICaseDetails;
observations: IObservations;
}
interface ICaseDetails {
a: string;
}
interface IObservations {
b: number;
}
const updateCasePart = (state: ICase, payload: any, propName: keyof ICase) => {
state[propName] = payload
}
Is there a way I can type payload so that a calling function would be limited to the correct payload types? I know I could do payload: ICaseDetails | IObservations
but I was hoping for something like payload: typeof keyof ICase
Upvotes: 2
Views: 155
Reputation: 330061
As your title suggests, you should make the function generic as follows:
const updateCasePart = <K extends keyof ICase>(
state: ICase, payload: ICase[K], propName: K
) => { state[propName] = payload }
Instead of annotating the propName
parameter as being of type keyof ICase
(equivalent to the union type "caseDetails" | "observations"
), we annotate it as being of the generic type K
which has been constrained to keyof ICase
. This allows propName
to be more specific than keyof ICase
. For example, if you pass in "caseDetails"
as propName
, the compiler will infer that K
is the string literal type "caseDetails"
.
And note that the type of payload
is the indexed access type ICase[K]
, meaning the type of the property of ICase
at a key of type K
. So if K
is "caseDetails"
, then ICase[K]
is ICaseDetails
. This (mostly) ensures that the value you pass in for payload
will be appropriate.
The assignment state[propName] = payload
type checks because the compiler sees both sides of the assignment as being of the identical generic type ICase[K]
.
Let's test it out:
declare const caseDetails: ICaseDetails;
declare const observations: IObservations;
declare const state: ICase;
updateCasePart(state, caseDetails, "caseDetails"); // okay
updateCasePart(state, caseDetails, "observations"); // error!
// -----------------> ~~~~~~~~~~~
// Argument of type 'ICaseDetails' is not assignable to parameter of type 'IObservations'.
updateCasePart(state, observations, "observations"); // okay
Looks good. The compiler allows valid calls and (mostly) disallows invalid calls.
Okay I keep saying "mostly". There's a type safety hole with generics involving unions; if for some strange reason you pass in a propName
argument whose type is a union of key types, then K
will be inferred as that union, and ICase[K]
will also be a union, meaning it's possible for bad calls to be allowed:
updateCasePart(state, caseDetails,
Math.random() < 0.99 ? "observations" : "caseDetails"
); // okay?!
There's no error in the above, but there's a 99% chance you've passed in "observations"
as propName
with an ICaseDetails
as payload
.
If it really matters, it's possible to start rewriting updateCasePart
to make such things less likely... but since TypeScript isn't fully type safe, you can't really prevent every possible unsafe operation, and there are diminishing returns with added efforts.
For most purposes, a generic function like the above provides adequate safety and usability, and so I won't digress here further by detailing possible excursions down the endless and increasingly treacherous endless road toward pure soundness.
Upvotes: 3