This is a simple library with no outside dependencies that generates a typescript schema out of your defined schema. This can help creating tight contracts in rest APIs for your input and output, without the need and complexity of using GraphQL or similar.
composer require le0daniel/typescript-schema
Define your schema for input or output.
use TypescriptSchema\Definition\Schema;
$userType = Schema::object([
'name' => Schema::string()->nonEmpty()->minLength(1)->maxLength(255),
'email' => Schema::string()->email()->endsWith('.com')
]);
$userType->toDefinition()->input() // JsonSchema => ['type' => 'object', 'properties' => ['name' => ['type' => 'string'], 'email' => ['type' => 'string']], (...)]
$userType->toDefinition()->output() // JsonSchema => ['type' => 'object', 'properties' => ['name' => ['type' => 'string'], 'email' => ['type' => 'string']], (...)]
$schema = Schema::make($userType);
$schema->parse(['name' => 'The name', 'email' => '[email protected]', 'other']);
// => Result<['name' => 'The name', 'email' => '[email protected]']>
$schema->parse(['name' => 'The name', 'email' => 'INVALID', 'other']);
// => Result<Value::INVALID>
$schema->parseOrFail(['name' => 'The name', 'email' => 'INVALID', 'other']);
// => throws ParsingException
$schema->parseOrFail(['name' => 'The name', 'email' => '[email protected]', 'other']);
// => ['name' => 'The name', 'email' => '[email protected]']
The main difference is that parsing will cast some values to PHP types, whereas serialize will return a format that is JSON serializable. Use parse if you have unknown input that should be used in a PHP, use serialize if the result should be Json Serialized.
Parsing: Takes input and returns PHP Types where possible (Enums, DateTimeImmutable) Serializing: Takes input and serializes it to JsonSerializable format. Example (Enum => Enum->name, DateTimeImmutable => DateTimeImmutable->format(...))
By default, the toDefinition() method generates Json Schema output. You can use it to create a valid typescript declaration using the Typescript utility.
use TypescriptSchema\Utils\Typescript;
use TypescriptSchema\Definition\Schema;
$schema = Schema::make(
Schema::object([
'id' => Schema::int()->min(1),
'name' => Schema::string(),
])
);
Typescript::fromJsonSchema($schema->toDefinition()->output());
// => {id:number;name:string}
- String
Schema::string() | StringType::make()
- Int
Schema::float() | FloatType::make()
- Float
Schema::float() | FloatType::make()
- Boolean
Schema::boolean() | BoolType::make()
- Literal
Schema::literal('book') | LiteralType::make('book')
- DateTime
Schema::dateTime(format) | DateTimeType::make(format)
- Any
Schema::any() | AnyType::make()
- Enum
Schema::enum(class) | EnumType::make(class)
By default, type checks are strict. Meaning passing a number to a String type will result in a failure.
You can use coercion to make it less strict as following: Schema::string()->coerce()
. This is available for all
primitive types and tries to parse the value from any input.
This works as following for the different primitives:
- String:
(string) $value
- Int:
(int) $value
- Float:
(float) $value
- Boolean:
Accepts: 0, 1, '0', '1', 'true', 'false'
The string type supports following default validations:
- min (min amount of characters, including)
Schema::string()->min(5)
- max (max amount of characters, including)
Schema::string()->max(5)
- endsWith
Schema::string()->endsWith('.test')
- startsWith
Schema::string()->startsWith('Hello: ')
- notEmpty
Schema::string()->notEmpty()
- regex
Schema::string()->regex('/[a-z]+/')
- alphaNumeric
Schema::string()->alphaNumeric()
- email
Schema::string()->email()
The int type supports following validations:
- min
Schema::int()->min(5, including: true)
- max
Schema::int()->max(5, including: true)
The DateTime primitive accepts by default a string (in given format, defaults to DateTime::ATOM
) or an instance of
the DateTimeInterface
. All instances are cast into a DateTimeImmutable.
By default, the output type is as following: {date: string, timezone_type: number, timezone: string}
. This is the
default JsonSerialization of a DateTime class in PHP.
For output, it is useful to use Schema::dateTime()->asFormattedString()
, which will return the formatted string
instead of the DateTimeImmutableInstance.
An Enum type expects the input to be the Enum instance or a string with the name of the enum. In serialization mode,
the Enum name is outputted (Enum::CASE->name
).
use TypescriptSchema\Definition\Schema;
enum MyEnum {
case SUCCESS;
case FAILURE;
}
$type = Schema::enum(MyEnum::class);
$type->toDefinition()->input() // => 'SUCCESS'|'FAILURE'
$type->toDefinition()->output() // => 'SUCCESS'|'FAILURE'
// Parsing always returns the Enum case itself.
Schema::make($type)->parse(MyEnum::SUCCESS) // => MyEnum::SUCCESS
Schema::make($type)->parse('SUCCESS') // => MyEnum::SUCCESS
// Parsing as strings always returns the string with the enum name.
Schema::make($type)->serialize('SUCCESS') // => 'SUCCESS'
Schema::make($type)->serialize(MyEnum::SUCCESS) // => 'SUCCESS'
Both the unknown and any type pass all data through in the form it was before.
use TypescriptSchema\Definition\Schema;
$type = Schema::make(Schema::any());
$type->parse(null) // => Result<null>
$type->parse('string') // => Result<'string'>
$type->parse((object) []) // => Result<object{}>
$type->parse([]) // => Result<array{}>
Complex types build up on the primitives and add functionality around them. Following complex types are supported:
- Object
- Array
- Record
- Tuple
- Union / DiscriminatedUnion
Represents an Object in json with key value pairs that are known and typed. Objects are defined by an associative array in PHP, where the key needs to be a string and the value a Type or a Field.
use TypescriptSchema\Definition\Schema;
use \TypescriptSchema\Utils\Typescript;
$user = Schema::object([
'name' => Schema::string(),
]);
Typescript::fromJsonSchema($user->toDefinition()->input()) // => {name: string;}
Typescript::fromJsonSchema($user->toDefinition()->output()) // => {name: string;}
By default, resolving a value to a field is done by checking if the key exists in the array or a property of an object.
In some cases, you might want to customize this resolution (Ex: you have a computed output field).
use TypescriptSchema\Definition\Complex\Field;use TypescriptSchema\Definition\Schema;
use TypescriptSchema\Definition\Schema;
$user = Schema::object([
'fullName' => Field::ofType(Schema::string())
->resolvedBy(fn(User $user): string => $user->fullName()),
]);
Using fields adds more benefits. You can describe the field or even deprecate it. This is especially useful for output types that serialize your Data.
use TypescriptSchema\Definition\Complex\Field;
use TypescriptSchema\Definition\Schema;
$user = Schema::object([
'fullName' => Field::ofType(Schema::string())
->resolvedBy(fn(User $user): string => $user->fullName())
->describe('The combination of title, first and last name')
->deprecated('Use lastName, firstName and title to compute manually', DateTime::createFromFormat('Y-m-d', '2024-05-25')),
]);
You can define your own custom types, by implementing the Type interface. Two methods need to be implemented:
public function toDefinition(): SchemaDefinition;
public function parse(mixed $value, Context $context): mixed;
The definition method should define the Json Schema for input and output, whereas the parse function should parse the given value into the correct type. It MUST NOT throw any exception.
In case of failure, Value::INVALID
should be returned and all issues added to the context.
If your type needs to serialize values in a specific way you have to additionally implement the SerializesOutputValue
interface.
Remember, the types should be immutable.
Following utilities (traits) are available for types to use:
- Coerce: Enables
->coerce()
- HasDefaultValue:
->defaultValue()
- Nullable: Enables
->nullable()
- Refinable: Enables
->refine()
- Transformable: Enables
->transform()
- Validators: Enables Closure Validators to be used easily.
Example
use TypescriptSchema\Contracts\Type;
use TypescriptSchema\Definition\Shared\Nullable;
use TypescriptSchema\Definition\Shared\Refinable;
use TypescriptSchema\Definition\Shared\Transformable;
use TypescriptSchema\Definition\Shared\Validators;
use TypescriptSchema\Helpers\Context;
use TypescriptSchema\Data\Enum\Value;
use TypescriptSchema\Exceptions\Issue;
final class MyCustomType implements Type {
use Nullable, Refinable, Transformable, Validators;
public function toDefinition(): SchemaDefinition {
return new \TypescriptSchema\Data\Schema\Definition(
['type' => 'string'],
['type' => 'string'],
);
}
public function serializeValue(mixed $value, Context $context): Value|string {
if (!is_string($value)) {
$context->addIssue(Issue::custom('Value must be a string.'));
return Value::INVALID;
}
if (!$this->runValidators($value, $context)) {
return Value::INVALID;
}
return $value;
}
public function minLength(): MyCustomType {
return $this->addValidator(function (string $value) {
return strlen($value) > 5;
});
}
}