liberal-accept-strict-return

Use when designing function signatures. Use when creating APIs. Use when parameters have optional fields but return types feel too broad.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "liberal-accept-strict-return" with this command: npx skills add marius-townhouse/effective-typescript-skills/marius-townhouse-effective-typescript-skills-liberal-accept-strict-return

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)

ExcuseReality
"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

AspectInput (Parameters)Output (Returns)
Optional fieldsOKAvoid
Union typesOKUse sparingly
Multiple formatsOKSingle canonical format
Undefined valuesOKAvoid

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.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

tsdoc-comments

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

code-gen-independent

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

tsconfig-options

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

module-by-module-migration

No summary provided by upstream source.

Repository SourceNeeds Review