28.7 Typing Third-Party Hooks
Alright, let’s talk about the real world. You’re not just writing your own beautiful, perfectly typed hooks; you’re also going to be using other people’s. And let’s be honest, some of those people might have been having a very long day when they wrote the type definitions. Our job is to build a sturdy, type-safe scaffold around their code, even when it feels like they’re actively working against us.
The golden rule here is simple: Never use any to shut the compiler up. That’s like fixing a gas leak by removing the smoke alarm. We’re better than that. We’re going to understand what we’re dealing with and apply the correct level of type safety.
The Dream: A Perfectly Typed Hook
This is what we hope for. A well-maintained library where the hooks come with their own first-class TypeScript types. It’s a beautiful thing.
import { useForm } from 'react-hook-form';
// The library's authors exported these types. Bless them.
import { SubmitHandler } from 'react-hook-form';
interface IFormInput {
firstName: string;
age: number;
}
function MyComponent() {
// `useForm` is generically typed with our form's shape.
// This means `register`, `watch`, and `handleSubmit` all know about `IFormInput`.
const { register, handleSubmit } = useForm<IFormInput>();
const onSubmit: SubmitHandler<IFormInput> = (data) => {
// `data` is automatically inferred as `{ firstName: string; age: number }`
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('firstName')} /> {/* TS knows 'firstName' is valid */}
<input {...register('age')} />
<input type="submit" />
</form>
);
}
See how that works? We provide the type once at the hook call site (useForm<IFormInput>()), and everything else flows from there. The library’s types do the heavy lifting. This is the ideal. Cherish it when you find it.
The Reality: Dealing with any or Unknowns
Now, let’s get our hands dirty. Sometimes you’ll pull in a hook, and its return value is just a big bag of any. Or maybe it’s a hook that fetches data and returns something like { data: unknown, loading: boolean }. Using unknown is actually more honest than any—it’s the type system’s way of saying, “I have no idea what this is, so you have to prove to me it’s safe before you use it.”
Your tool here is type assertion. But use it wisely, like a surgeon’s scalpel, not a lumberjack’s axe.
// Hypothetical poorly-typed hook from 'some-npm-package'
import { usePoorlyTypedHook } from 'some-npm-package';
interface MyActualData {
id: string;
name: string;
}
function MyComponent() {
// The hook returns `any`. We know it's wrong.
const result = usePoorlyTypedHook();
// ❌ Bad: This "works" but is utterly unsafe. `result` could be `null` or a completely different shape.
// console.log(result.name);
// ✅ Better: Assert the type at the point of use, but only if you're *sure*.
// This is a promise you're making to the type checker. Don't break it.
const typedResult = result as MyActualData;
// ⚠️ Best: Use a type guard to prove it's safe at runtime. This is the safest way.
function isMyActualData(data: unknown): data is MyActualData {
return (
typeof data === 'object' &&
data !== null &&
'id' in data &&
'name' in data &&
typeof (data as MyActualData).id === 'string' &&
typeof (data as MyActualData).name === 'string'
);
}
if (isMyActualData(result)) {
// Now TypeScript knows `result` is of type `MyActualData` inside this block.
console.log(result.name); // safe
} else {
// Handle the error case. The data wasn't what you promised.
}
}
The type guard (isMyActualData) is extra work, but it’s the only way to get true type safety with external data. For data from your own backend that you control, a simple type assertion might be an acceptable risk. For data from a third-party API, write the guard.
When the Hook Itself is Generic
Some advanced hooks are designed to be generic. They’re like a function that asks, “What kind of data are you working with? I’ll adapt to you.” useState is the classic example. You have to tell it what type it’s holding.
The pattern is the same for many third-party hooks.
import { useCustomHook } from 'another-library';
interface User {
id: number;
username: string;
}
function MyComponent() {
// The hook is defined as `useCustomHook<T = unknown>()`
// We need to provide the `T` explicitly.
const { value, setValue } = useCustomHook<User>(); // Now `value` is `User | null` and `setValue` expects a `User`
// If the initial state is complex, you might need to help TS with the initial value.
const { value: otherValue } = useCustomHook<User[]>({ initialValue: [] }); // Now `otherValue` is `User[]`
}
The key is to look at the hook’s type definition. If it’s useHook<T>(), then you can—and usually should—provide that T.
The Nuclear Option: Augmenting Module Types
Sometimes a library’s types are just wrong. Not missing, but incorrect. They’ve typed a property as string when it’s clearly a number, or they’ve forgotten to mark something as optional. Shouting into the GitHub issue void might work eventually, but you have a project to ship now.
Enter module augmentation. This is TypeScript’s superpower for fixing other people’s mistakes without forking their entire project.
// Lets say `some-library` has a hook `useConfig` that returns an object.
// Their type definition says it returns `{ settingA: string }`, but you know for a fact
// it also returns a `settingB: number` that they forgot to add.
// 1. Create a type declaration file (e.g., `types/some-library.d.ts`)
// 2. Import the original module's types and 'augment' them.
import 'some-library';
declare module 'some-library' {
// We're extending the interface that the hook's return value conforms to
interface ConfigReturn {
settingA: string;
settingB: number; // 🎉 We're adding the missing property
}
}
// Back in your component file...
import { useConfig } from 'some-library';
function MyComponent() {
const config = useConfig();
// Now TypeScript knows `config` has both `settingA` and `settingB`!
const value = config.settingB * 10; // This now works perfectly.
}
This feels like black magic, but it’s completely legitimate. You’re patching the incomplete type definitions in your own project. It’s a bit of work, but it’s the most elegant way to handle truly broken types without losing your mind.