liberal-accept-strict-return

SKILL.md

Be Liberal in What You Accept and Strict in What You Produce

Overview

Accept broad input types, return narrow output types.

This is Postel's Law applied to TypeScript: functions should be flexible about what they accept but precise about what they return. This makes APIs easier to use and types more useful.

When to Use This Skill

  • Designing function parameters and return types
  • Creating reusable APIs or libraries
  • Function returns feel too broad for callers to use
  • Want to accept multiple input formats
  • Struggling with optional fields that shouldn't be optional in output

The Iron Rule

ALWAYS make input types broader than output types.

Remember:

  • Parameters: optional fields, union types, multiple formats OK
  • Return types: required fields, specific types, single format
  • Input flexibility helps callers
  • Output precision helps consumers

Detection: The "Too Broad Return" Problem

If callers have to do extra work to use your function's return value, your return type is too broad:

// ❌ Return type is as broad as input type
declare function viewportForBounds(bounds: LngLatBounds): CameraOptions;

function focusOnFeature(f: Feature) {
  const camera = viewportForBounds(calculateBoundingBox(f));
  const {center: {lat, lng}, zoom} = camera;
  //             ~~~  Property 'lat' does not exist on type 'LngLat | undefined'
  //             ~~~  Property 'lng' does not exist on type 'LngLat | undefined'
  zoom;
  // ^? const zoom: number | undefined
}

The Postel's Law Pattern

Broad Input Types

// Accept multiple formats for convenience
type LngLat =
  | { lng: number; lat: number }
  | { lon: number; lat: number }
  | [number, number];

type LngLatBounds =
  | { northeast: LngLat; southwest: LngLat }
  | [LngLat, LngLat]
  | [number, number, number, number];

// Input: Many ways to specify bounds
declare function setCamera(camera: CameraOptions): void;

Strict Output Types

// Return a single, precise format
interface Camera {
  center: { lng: number; lat: number };  // Not optional, not union
  zoom: number;                           // Not optional
  bearing: number;
  pitch: number;
}

// Output: One clear format
declare function viewportForBounds(bounds: LngLatBounds): Camera;

Complete Example

// LIBERAL INPUT: Accept many formats
interface CameraOptions {
  center?: LngLat;      // Optional
  zoom?: number;        // Optional
  bearing?: number;     // Optional  
  pitch?: number;       // Optional
}

// STRICT OUTPUT: Return precise types
interface Camera {
  center: { lng: number; lat: number };  // Required, canonical format
  zoom: number;                           // Required
  bearing: number;                        // Required
  pitch: number;                          // Required
}

declare function setCamera(camera: CameraOptions): void;      // Liberal input
declare function viewportForBounds(bounds: LngLatBounds): Camera;  // Strict output

Now callers can use the output directly:

function focusOnFeature(f: Feature) {
  const camera = viewportForBounds(calculateBoundingBox(f));
  setCamera(camera);  // Works! Camera is assignable to CameraOptions
  
  const {center: {lat, lng}, zoom} = camera;  // No errors!
  window.location.search = `?v=@${lat},${lng}z${zoom}`;
}

Why This Works

Broader types are subtypes of narrower types (see types-as-sets skill):

// Camera (all required) is a SUBTYPE of CameraOptions (all optional)
// So Camera is assignable to CameraOptions

const camera: Camera = viewportForBounds(bounds);
setCamera(camera);  // OK! Camera ⊆ CameraOptions

Applying the Pattern

For Functions

// ❌ Input and output have same optionality
function process(options: Options): Options { ... }

// ✅ Input liberal, output strict
function process(options: Options): ProcessedOptions { ... }

For Classes

class DataProcessor {
  // Liberal: accept various formats
  constructor(data: RawData | FormattedData | string) { ... }
  
  // Strict: return precise types
  getResult(): ProcessedResult { ... }
}

For APIs

interface CreateUserInput {
  email: string;
  name?: string;           // Optional input
  preferences?: UserPrefs; // Optional input
}

interface User {
  id: string;              // Always present in output
  email: string;
  name: string;            // Defaulted if not provided
  preferences: UserPrefs;  // Defaulted if not provided
  createdAt: Date;         // Added by system
}

function createUser(input: CreateUserInput): User { ... }

Separate Input/Output Types

A common pattern is to have distinct types for input and output:

// Input type (liberal)
interface CreatePostInput {
  title: string;
  body: string;
  tags?: string[];
  draft?: boolean;
}

// Output type (strict)
interface Post {
  id: string;
  title: string;
  body: string;
  tags: string[];      // Always present (defaults to [])
  draft: boolean;      // Always present (defaults to false)
  createdAt: Date;
  updatedAt: Date;
}

function createPost(input: CreatePostInput): Post { ... }

Pressure Resistance Protocol

1. "Just Use the Same Type"

Pressure: "It's simpler to have one type for input and output"

Response: It shifts complexity to every caller.

Action: Create separate input/output types when they differ.

2. "Optional Output Fields Are Fine"

Pressure: "Callers can just check for undefined"

Response: That's unnecessary work that accumulates.

Action: Make output fields required with sensible defaults.

Red Flags - STOP and Reconsider

  • Return types with many optional fields
  • Callers doing null checks on return values
  • Union return types where one type would suffice
  • Input and output types identical despite different needs

Common Rationalizations (All Invalid)

Excuse Reality
"Same type is simpler" It makes every call site more complex
"DRY means one type" Input/output types have different purposes
"Users can handle optionals" They shouldn't have to

Quick Reference

Aspect Input (Parameters) Output (Returns)
Optional fields OK Avoid
Union types OK Use sparingly
Multiple formats OK Single canonical format
Undefined values OK Avoid

The Bottom Line

Be generous in what you accept, precise in what you return.

Input types should accommodate callers. Output types should serve consumers. When in doubt, make input optional and output required. This makes your functions easier to call and their results easier to use.

Reference

Based on "Effective TypeScript" by Dan Vanderkam, Item 30: Be Liberal in What You Accept and Strict in What You Produce.

Weekly Installs
5
GitHub Stars
1
First Seen
Feb 3, 2026
Installed on
opencode5
claude-code4
mcpjam3
gemini-cli3
windsurf3
zencoder3