Specimen sheetType language for PHPPHP 8.1+
TypeLang is a declarative type language inspired by PHPStan and Psalm. It describes the types you already
write in doc-comments — int[], array<string, T>, Foo::CONST_*,
T is U ? A : B — and ships the parts to work with them: a parser, a printer, a reflection reader
and a PHPDoc parser. The tree below is drawn by a JavaScript port of the reference parser. Type a type.
PrettyTypePrinter
§ 01
Every construct in the language composes with every other. Start from a name and keep wrapping.
Any identifier is a type: a built-in, a class, an alias, even a hyphenated non-empty-string. The parser reads it as a NamedTypeNode. It checks that a type is well-formed, not that it exists — resolution is a separate concern.
A leading question mark wraps the type in a NullableTypeNode. It binds tighter than |, so ?A|B is (?A)|B.
Angle brackets carry generics. Any type goes inside, trailing commas are fine, * stands for “whatever”, and covariant, contravariant or invariant may precede an argument.
Unions are flat: A|B|C is one node with three children, not two nested pairs. Intersections use & and bind tighter.
A second argument describes the key. array-key, int, literal strings — the parser doesn't care which; your tool decides what they mean.
Braces turn a name into a shape: a list of fields, each with its own type. Keys can be identifiers, numbers, quoted strings or class constants; a field with no key is implicit.
Shape types ↗int<1, max> is just a generic with two literal-ish arguments. The trailing ... says “more keys may follow”, and may even describe them: ...<string, mixed>.
A question mark after the key marks the field optional. Notice how little of the tree has to move.
Shape types ↗Any name followed by parentheses is a callable type. Parameters may be named, variadic, by-reference or carry a default; the return type follows a colon and binds tightly: callable(): A|B is (callable(): A)|B.
A named callable like Closure can declare template parameters: variance, of / as and super bounds, and defaults. The return type here is conditional: T is Paid ? Receipt : null, or T is not Paid ? … to negate it.
§ 02
Every construct the grammar knows, set as a specimen. Hover a bracket to see the node behind it.
§ 03
Every grammar construct is a flag on TypeParserFeatures. Switch some off and the parser
rejects the syntax with a message that says why. Native PHP types only? Turn everything off but names.
§ 04
239 syntactic constructs, the basic ones and those on which PHPStan, Psalm and TypeLang disagree, each fed to the tool's own parser. A construct counts when the parser returns a node carrying what was written, not a silently truncated one.
Full table with the verdict for every construct, per tool, in the syntax comparison ↗. Phan is not in that table; it was run over the same constructs. The table also lists 34 malformed inputs that must be rejected: TypeLang rejects 33, PHPStan 29, Phan 29 and Psalm 5. Psalm answers even where it did not understand the question; the comparison explains how those answers are told apart.
§ 05
Five packages, one shared vocabulary. Each is published standalone and split from a single development monorepo.
AST node classes — the shared vocabulary of the ecosystem.
composer require type-lang/types
Parses TypeLang syntax into an AST, validating the grammar. Strict or partial mode, feature toggles, readable errors.
composer require type-lang/parser
Renders nodes back to strings: the full pretty form, or a native PHP type declaration.
composer require type-lang/printer
Builds nodes from the types exposed by PHP Reflection objects.
composer require type-lang/reader
Parses /** ... */ comments into a graph of description and tags.
composer require type-lang/phpdoc
// A string in, a tree out.
$parser = new TypeLang\Parser\TypeParser();
$type = $parser->parse('array{ key: int }');
// A tree in, a string out.
echo new TypeLang\Printer\NativeTypePrinter()->print($type); // array
echo new TypeLang\Printer\PrettyTypePrinter()->print($type); // array{ key: int }