Right, let’s talk about one of the most common ways we, as library authors, accidentally create a hostile environment for our users: overly narrow types. You’ve poured your soul into crafting a beautiful, robust API, and then you ruin it by telling the user, “No, you can’t do it that way,” when their way is perfectly valid. It’s the library equivalent of being a pedantic dinner guest who corrects everyone’s grammar. Don’t be that person.

The goal isn’t to model every possible state of your program with 100% mathematical purity—that way lies madness and a 500-line type definition for a single function. The goal is to model the contract: what you need, and what you promise to give back. Everything else is an implementation detail the user shouldn’t be forced to care about.

The Sin of Literal Overreach

The most frequent offender is the overzealous use of string literals. You think you’re being helpful by providing a specific, autocompletable list of options. Sometimes you are. Often, you’re just building a wall.

// ❌ The "My Way or the Highway" approach
function setTheme(theme: 'light' | 'dark' | 'blue'): void {
  // ... implementation
}

// What happens when a user has their theme in a config object?
const userConfig = { theme: 'dark' };
setTheme(userConfig.theme); // Error! Type 'string' is not assignable to type '"light" | "dark" | "blue"'

You just broke your user’s code. They have a string that is provably 'dark', but because it came from a wider type (string), TypeScript can’t prove it to itself at compile time. The user is now forced to do a pointless type assertion (as 'dark'), which is just them telling the compiler to shut up. You made them do that. You created the friction.

The fix? Accept the wider type and narrow it yourself internally.

// ✅ The "I Got This, You Do You" approach
function setTheme(theme: string): void {
  // Narrow it at the implementation boundary
  const validTheme = theme === 'light' || theme === 'dark' || theme === 'blue' ? theme : 'light';
  // ... proceed with validTheme
}

But wait, you say, now I’ve lost my autocompletion! No, you haven’t. You can have your cake and eat it too by providing the literal union as a default or as a suggestion, without forcing it.

// Provide the allowed values as a const for autocomplete, but don't force them.
const THEMES = {
  LIGHT: 'light',
  DARK: 'dark',
  BLUE: 'blue',
} as const;

// The function still accepts any string, but users can use THEMES for safety and discovery.
setTheme(THEMES.DARK); // Perfectly clear and safe
setTheme('custom-theme'); // Also works, because you're not a tyrant.

The ReadonlyArray Trap

This one is a classic. You write a function that shouldn’t mutate its input array. Your virtuous instinct is to declare that fact in the type system with ReadonlyArray<T>. This is good! But then you call another function, maybe from a third-party library, that only accepts a mutable T[].

// In your library:
function processData(data: ReadonlyArray<number>): number {
  // ... some logic
  return externalLibrarySumFunction(data); // Error! Argument of type 'readonly number[]' is not assignable to 'number[]'
}

The irony is thick enough to cut with a knife. You did the right thing, and you’re being punished for it. The externalLibrarySumFunction is the one with the overly narrow type here—it’s declaring that it might mutate the array, even if it actually doesn’t.

Your escape hatch is the dreaded but sometimes necessary type assertion. Since you know you’re not mutating the array, and you know a ReadonlyArray<number> is structurally identical to a number[], it’s safe to bend the rules.

return externalLibrarySumFunction(data as number[]);

It feels dirty, but it’s the correct response to someone else’s bad type design. The lesson for your library? Make your function parameters as wide as your implementation allows. If you’re not going to mutate an array, accept ReadonlyArray<T> so your users don’t have to face this same dilemma.

The Overly Specific Event Object

You’re designing a custom event system. You think, “I need to know exactly what’s going to be on that event object.” So you create a super-specific type.

interface MyCustomEvent {
  readonly type: 'click';
  readonly target: HTMLButtonElement;
  readonly customProperty: string;
}

What happens when a user tries to add a standard MouseEvent to your system? A world of pain. Instead, model what you actually need. Use generic constraints.

// ✅ Model the contract, not the specific object.
interface MyCustomEventDetails {
  customProperty: string;
}

function emitEvent<E extends MyCustomEventDetails>(event: E): void {
  // I know event has 'customProperty'. I don't care what else is on it.
  console.log(event.customProperty);
}

// Now users can pass anything that has your required property.
const standardEvent = new MouseEvent('click');
emitEvent({ ...standardEvent, customProperty: 'hello' }); // Works perfectly.

You’ve now made your system interoperable with standard DOM events, Node.js events, or anything else the user dreams up. You’ve provided flexibility without sacrificing the integrity of your required property. That’s the sweet spot. Remember, your job isn’t to prevent users from doing things. It’s to prevent them from doing things wrong, while getting out of their way when they’re doing things right.