Typesafe functions with dynamic parameter types
Thursday, February 29, 2024 - 3 min read
Imagine a function signIn that takes two parameters and returns nothing, for example:
provider
: A string representing the authentication method (for example, "credentials" or "phone")values
: An object containing relevant information for the chosen provider
We want to ensure that values align with the chosen provider. For example:
- If
provider
is "credentials",values
should haveemail
(string) andpassword
(string). - If
provider
is "phone",values
should havemobile
(number) andotp
(number)
Let's Try Union
We might initially write:
This approach has a significant limitation: It doesn't prevent mismatched values. You can pass phone
values to the credentials
provider or vice versa, without TypeScript raising any errors like this:
Better Union: Discriminated Union
We can restructure the function signature using a discriminated union like this:
This approach defines a single type SignInTypes
that includes both possibilities.
This ensures consistency between provider and values.
However, it requires modifying the function signature, which might not always be possible in existing libraries or frameworks.
Generics For The Rescue!
This is where generics come in handy. Generics allow us to define a function template that can work with various types. Here's how we can implement it
1. Define possible types.
This defines SignInTypes
as key-value pairs where the key is the provider type and the value is the type of the provider's values.
2. Create a Generic Function Type.
This defines a generic function type SignInFunction. It takes two parameters:
type
: A generic type parameterT
that extends thekeyof SignInTypes
(ensuring it's one of the provider types).values
: The second parameter leveragesSignInTypes
and the type of providerT
to get the type of the values. This ensures values match the structure expected for the chosen provider.
3. Use the type
Now it's working perfectly without changing the function signature.
Here is the final code
We can override the third-party package types, but that's a topic for another post.
Conclusion
By leveraging generics and TypeScript’s powerful type system, we’ve successfully ensured the type safety of our signIn
function without altering the function’s signature.