mark
mark

Reputation: 1779

How can I make a typesafe generic function

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

Answers (1)

jcalz
jcalz

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.

Playground link to code

Upvotes: 3

Related Questions