Type Signature¶
Type-level interface declaration — instantiates Domain–Codomain Delimitation
Names a function and declares, in the type system itself, exactly what kinds of value it accepts and what kind it returns — so a compiler rejects out-of-domain calls before the code ever runs.
A Type Signature binds a function-like unit to a declaration of the types it accepts and the type it produces — divide(a: Money, b: NonZeroInt) -> Money. What makes it this mechanism and not its siblings is when and by whom the boundary is enforced: the signature is checked mechanically by a compiler or type checker at build time, so an out-of-domain call is simply not expressible code. It bounds both sides at the granularity the type system can express — coarser than a schema's field-by-field structure, but enforced with total, up-front reliability. It names the unit, its input domain, and its output codomain in one line that the toolchain, not a human reviewer, polices.
Example¶
A currency library exposes convert(amount: Decimal, from: Currency, to: Currency) -> Money. Because Currency is a closed enum type and amount must be a Decimal, a caller who writes convert(19.99, "USD", "EUR") — passing a raw float and bare strings — never compiles: the float isn't a Decimal, and "USD" isn't a Currency. The domain violation is caught at the developer's desk, not in production. Push it further: making the return type Result<Money, ConversionError> rather than Money widens the codomain honestly, declaring in the signature itself that "no rate available" is a legal, typed outcome the caller must handle — so an unhandled failure also becomes a compile error rather than a runtime surprise.
How it works¶
The signature encodes the boundary in the vocabulary of the type system: accepted parameter types define the domain, the return type defines the codomain, and richer type constructs (non-null, ranges via newtypes, sum types like Result/Optional) push more of the real boundary into something the checker can enforce. Its power and its ceiling are the same fact — it can only express what the type system can express. It is verified statically, so violations are impossible to run rather than merely detected when they happen.
Tuning parameters¶
- Type expressiveness — how much boundary you encode in types (a bare
intvs. aPositiveIntnewtype vs. a refinement type). Richer types catch more at compile time but cost modelling effort and can obscure intent. - Total vs. partial return — whether the codomain is widened to a
Result/Optionalthat names failure, or left as a bare type that hides it. Total signatures force callers to handle every declared outcome. - Nominal vs. structural typing — whether a
UserIdand anOrderIdare interchangeable because both areint, or distinct because named. Nominal typing tightens the domain against accidental mix-ups. - Nullability policy — whether
null/Noneis an allowed inhabitant of every type or an explicit, opt-in part of the domain.
When it helps, and when it misleads¶
Its strength is unmatched reliability at its chosen granularity: within what the types express, an out-of-domain call cannot be written, and the check costs nothing at runtime. Its limit is that the type is coarser than the true boundary — String says nothing about "a valid IBAN," so a well-typed argument can still be semantically out of domain. The classic failure is smuggling values past the boundary the signature implies: overly liberal parameters (Any, object, stringly-typed inputs) and escape hatches (as, unchecked casts) that accept out-of-domain values while the signature still looks strict — the robustness-principle temptation to be liberal in what you accept, which quietly enlarges the effective domain.[1] The discipline is to keep signatures as tight as the type system allows and delegate the finer, value-level checks to a runtime Input Validation Gate and a Contract Test Suite.
How it implements the components¶
functional_unit_under_scope— the signature names the exact function being bounded; the boundary is attached to this callable, not a vague area.input_domain— the parameter types are the domain, enforced by the checker: only values of those types can be passed.output_codomain— the return type is the codomain; a total return type (Result<T, E>) declares even the failure outcomes as legal, typed members.
It bounds by type, not by value-level rule (finer membership_criteria and structural output shape live in Eligibility Criteria and Output Schema), and it neither routes rejected calls nor stewards the boundary over time (Unsupported Case Triage Workflow, Scope Change Review).
Related¶
- Instantiates: Domain–Codomain Delimitation — the Type Signature is the boundary the compiler enforces.
- Sibling mechanisms: Input Validation Gate · Output Schema · Contract Test Suite · Output Validation · Eligibility Criteria · Model Applicability Card · Service Scope Statement · Clinical Indication Criteria · Scope Change Review · Unsupported Case Triage Workflow
Notes¶
A Type Signature enforces the boundary but does not validate the values inside the types: withdraw(amount: PositiveInt) guarantees a positive integer, not that the account holds it. The value-level and business-rule checks belong to the runtime Input Validation Gate; the signature's job is to make the type-level domain unbypassable, and it is at its most valuable when kept narrow enough that "well-typed" already means "in domain."
References¶
[1] Postel's Law (the robustness principle) — "be conservative in what you send, be liberal in what you accept." Useful for interoperability, but taken too far on the input side it silently widens a function's effective domain past what its signature declares, admitting values the rest of the code was never built to handle. ↩