@studnicky/type-alias-invariants
Enforces one ordered contract for type aliases and imported type identity.
A retained alias is verified schema-derived pure data. Callable, constructor, runtime, brand, unknown-bearing, and other non-schema computations are interfaces or are redesigned into named schema data plus interface contracts. A generic conditional, mapped, or indexed-access alias is a type-level function and is retained as a type alias — TypeScript interfaces cannot express these shapes.
Fixable: No · Options: No · Suggested severity: error
Declaration contract
| Declaration | Required representation |
|---|---|
| JSON-Schema-expressible data | *Entity.Type = F<typeof Schema> for a verified schema-deriving F, under the complete entity suite |
| Callable or constructor | Interface call, method, or construct signature |
| Runtime object or provider seam | Interface |
| Readonly access policy | Interface |
| Unique-symbol brand marker | Interface |
| Generic conditional, mapped, or indexed-access type-level function | Retained as a type alias; interfaces cannot express the shape |
| Non-generic conditional, mapped, indexed-access, or other non-schema computation | Interface where representable; otherwise named schema data plus a contract interface |
Primitive forwarding aliases, naked renames, generic forwarding aliases, import aliases, inline object aliases, unresolved references, contract-interface references, and non-JSON types do not establish canonical data.
A reference to a type-level function from another declaration still composes a contract portion exactly as an inline conditional, mapped, or indexed-access body would — only the type-level function's own declaration is exempt.
Schema provenance
Recognition is library-agnostic: it inspects what a schema derivation produced, not the package that produced it. A retained alias satisfies four conditions:
- Shape — the alias body applies a type-level function to a value:
F<typeof Schema>,typeof Schema.inferred, or(typeof Schema)['inferred']. - Deriving function —
Fis recognized by structure, not by name or origin package. TypeBox'sStatic, Zod'sz.infer,json-schema-to-ts'sFromSchema, and a project-local equivalent are all accepted identically, satisfied by any one of:Fis a type alias declared with type parameters;Fis declared in a.d.tsfile;F's declaration carries a/** @schemaDerivation */JSDoc tag — the one in-code extension point, for a project-local schema-to-type function whose declaration is not itself a generic type alias; orFand the builder function that producedSchema(see below) share the same package root.
- Value-first authoring —
Schemais a module-scopeconstwith no explicit type annotation (an annotation means the type came first, so the value is not the source of truth), whose initializer is either a const-asserted object literal ({ ... } as const, optionallysatisfies-wrapped) or a builder call (Type.Object(...),z.object(...), and so on). Aletbinding never qualifies. - Result plainness — the resolved type that
F<typeof Schema>produces is JSON-plain: no call or construct signatures, no class instances, no symbol, bigint,never,void,undefined,any, orunknown. Recognition stops recursing intoF's own implementation and checks only what it resolves to, which is what makes this library-agnostic.
Provenance resolution follows TypeScript symbols through local declarations and imports with deterministic cycle and depth protection. An unresolved source is non-canonical; matching field shapes do not substitute for verified provenance.
Diagnostic order
The rule has no subchecks or internal severity settings. ESLint's configured severity is the sole severity.
- Alias identity reports primitive aliases, naked aliases, generic forwarding aliases, and import aliases.
- Declaration shape reports callable, constructor, runtime, brand, and non-schema computations as interfaces. A generic conditional, mapped, or indexed-access alias is classified as a type-level function instead and is exempt from this check and from canonical provenance.
- Canonical provenance reports data-shaped aliases without verified schema provenance.
- Exported naming requires retained aliases — including type-level functions — to end in
Type. - Readonly output reports mutable data aliases that author access policy.
An earlier verdict suppresses later advice for the same alias. Structural equality, near-match, and subsumption are not identity evidence: two data types may share a shape while representing different semantics. The rule therefore performs no heuristic imported-shape comparison and does not infer canonical identity from broader or narrower shapes.
The companion all-types-are-entities rule then requires a retained canonical alias to use the exact entity form.
Readonly output policy
Pure-data aliases describe mutable data. Readonly access belongs on interface contracts and use sites. The rule detects readonly properties, index signatures, arrays, tuples, mapped output modifiers, Readonly<T>, ReadonlyArray<T>, exposed readonly defaults, and readonly alias references.
The rule reports readonly output without a fixer. Removing readonly changes the type's mutability contract even when the program still type-checks, so the appropriate repair depends on the design intent.
Generic constraints, callable inputs, conditional operands, keyof operands, indexed-access operands, mapped keys, and -readonly modifiers inspect or constrain data without authoring output policy.
For example, UserSnapshotInterface may expose readonly value: UserEntity.Type together with a refresh(): Promise<void> method. The entity owns the data shape; the interface owns readonly access and runtime behavior.
Pure-data portions inside a contract interface reference separately declared entity types.
✗ Incorrect
type HandlerType = (value: string) => void;type InlineDataType = { value: string };type ListType<T> = Array<T>;import type { FromSchema, JSONSchema } from 'json-schema-to-ts';
const ValueSchema = { type: 'string' } as const satisfies JSONSchema;
type ValueType = FromSchema<typeof ValueSchema>;
export type ValueListType = readonly ValueType[];✓ Correct
import type { FromSchema, JSONSchema } from 'json-schema-to-ts';
const ValueSchema = { type: 'string' } as const satisfies JSONSchema;
export type ValueType = FromSchema<typeof ValueSchema>;import type { FromSchema, JSONSchema } from 'json-schema-to-ts';
const ValueSchema = { type: 'string' } as const satisfies JSONSchema;
type ValueType = FromSchema<typeof ValueSchema>;
export type ValueCollectionType = ValueType[] | null;type ConditionalType<T> = T extends string ? number : boolean;interface HandlerInterface {
(value: string): void;
}Configuration
The rule takes no options and recognizes no comment, declaration-name, member-name, package, or path exemptions. Enable or disable the complete rule through flat configuration; individual invariant checks cannot be configured independently:
export default [
{
files: ['src/**/*.ts'],
rules: {
'@studnicky/type-alias-invariants': 'error'
}
},
{
files: ['generated/**/*.ts'],
rules: {
'@studnicky/type-alias-invariants': 'off'
}
}
];Related rules
all-types-are-entitiesrequires the exact entity declaration form.interface-must-be-contractrejects pure-data interfaces.interfaces-compose-named-typesextracts pure-data portions from contract interfaces.no-mixed-callable-shapesowns a union or intersection that mixes a callable constituent with data — a shapealiasMustBeInterfacecannot direct to an interface, since TypeScript has no syntax for a union-shaped interface.