Skip to main content
react

The Open Source Habit: Read the API Before the README

README files describe intent. TypeScript types describe contract. When they disagree, types win — and that gap reveals everything about the library's quality.


5 min read

Before reading a library's README, open its index.d.ts. The README describes what the authors wanted to build. The types describe what they actually built. When those two things align, you have found a well-maintained library. When they diverge — when the docs promise a clean API and the types reveal three overloads, a union of string | undefined | null, and a generic with four constraints — you have found something to be cautious about.


What Types Reveal That Docs Don't

Documentation is curated. Types are honest.

A README shows you the happy path. It shows the simplest possible usage, the most common configuration, the output everyone expects. It omits the edge cases because edge cases make documentation feel complicated.

Types cannot omit edge cases. If a function can return null, the return type is T | null. If a parameter is optional, the type shows ?:. If there are three different call signatures, there are three overloads in the type definition.

Here is a concrete example. A library's README might show:

const { data } = useFetch("/api/user");
// data: User

But the actual type signature:

function useFetch<T = unknown>(
  url: string,
  options?: RequestInit
): {
  data: T | undefined;
  error: Error | null;
  isLoading: boolean;
  mutate: (value?: T | ((prev: T | undefined) => T)) => void;
};

The README told you data: User. The types tell you data: T | undefined. That undefined matters. You will write data.name in a component and get a runtime error the first time the request is in flight. The type signature told you this would happen. The docs did not.

How to Read an index.d.ts

Most published npm packages include a type declaration file, either authored (*.d.ts) or generated from TypeScript source. You can find it in node_modules/[package]/ after installing, or navigate to it directly on npm via the TypeScript playground link or unpkg.

What to look for:

Nullable fields in return types. Any field typed as T | null | undefined is a field the library cannot guarantee will be there. Plan for it in consuming code.

Overloaded function signatures. Multiple export function foo(...) declarations mean the function behaves differently under different inputs. Read all the overloads, not just the first one that matches your usage.

Generic constraints. <T extends Record<string, unknown>> is stricter than <T>. Constraints tell you what the library assumes about the data you pass it. Violating those constraints silently produces bugs.

What is not exported. If a type used internally in the API is not exported, you cannot compose with it. You will end up writing typeof library.doThing extends (...args: infer A) => infer R ? R : never just to extract a type the library should have published. Libraries that export their internal types are easier to work with at scale.

Reading a Hook Type vs Reading the Docs

Take useFormContext from React Hook Form. The docs show a simple example: call the hook, destructure register, use it on inputs. Straightforward.

The type signature:

export declare function useFormContext<
  TFieldValues extends FieldValues = FieldValues,
  TContext = any,
  TTransformedValues extends FieldValues | undefined = undefined
>(): UseFormReturn<TFieldValues, TContext, TTransformedValues>;

Three generic parameters. The docs example uses none of them because FieldValues defaults to Record<string, any>. That default is what makes the docs example work. It is also the source of the type safety you lose if you do not pass your form's schema type:

// No type safety — data is Record<string, any>
const { register } = useFormContext();

// Full type safety — data matches your schema
type LoginForm = { email: string; password: string };
const { register } = useFormContext<LoginForm>();

The types told you this was possible. The docs for a quick-start example had no reason to show it.

What to Look for in the Source

TypeScript types tell you the contract. Source code tells you what actually happens inside it.

Three things worth checking in the source code of any library you are considering adopting:

How errors are thrown. Does the library throw synchronously or reject a promise? Does it throw standard Error instances or custom error classes? If custom, are those classes exported so you can catch them specifically? instanceof checks on non-exported error classes do not work.

What side effects happen. Does the library modify globals? Set window.__library_state? Attach event listeners it does not clean up? This is hard to see from the README and immediately visible in the source. A quick search for window. and document. in the source is worth 30 seconds.

Whether it tree-shakes. If the library uses a single export default object with all functions attached, bundlers cannot tree-shake it. You will import the entire library even if you use one function. Named exports from separate modules shake correctly. Check the package's exports field in package.json for subpath exports ("./utils": "./dist/utils.js") as a signal of tree-shaking intent.

The Disagreement That Tells You Everything

When README and types disagree, the types win at runtime. But the disagreement itself is diagnostic.

If the docs say a function takes options: Config and the types say options: Partial<Config>, all those fields are optional. Either the docs are showing best-practice usage (pass everything) or the docs are out of date. Worth checking the changelog.

If the docs say a function returns User and the types say User | null, the docs are optimistic. Either the library handles the null case internally in some contexts (check when and why), or the docs have not been updated since a refactor.

If the types have // @ts-ignore comments in the declaration file — not rare — something was too hard to type correctly and was suppressed. That is a flag, not a disqualifier. But it tells you the library has rough edges in that area that you will eventually encounter.

The Habit in Practice

The workflow I use when evaluating a new library:

  1. npm pack [package] or browse unpkg.com/[package] to see the published files
  2. Open index.d.ts — read the exports, note what is and is not exported
  3. Find the types for the two or three functions I plan to use — read all overloads, note nullability
  4. src/ or the compiled source — search for error throwing patterns, global mutations
  5. package.json — check exports, sideEffects, and peerDependencies

Then read the README for the getting-started example. By then I already know the shape of the API. The README fills in the why — the docs describe what they intended to build, which is useful context once I know what they actually built.

Libraries where types and docs match closely — where the README example typechecks exactly as written and the types have no suppressed errors and the returns are what they say they are — these are the libraries built by teams who wrote the types first and the docs after. They are rarer than they should be. When you find one, trust it.