Quick Genotype Language Tour
Welcome to the Genotype Programming Language guide!
Genotype is a language that allows you to define types and data structures and generate code implementing them in multiple programming languages, such as TypeScript, Rust, Python, and more.
Source Files
Section titled “Source Files”Genotype source files have .type extension. Each file is a module that exports all its type definitions.
Category: "fiction"
Book: { title: string, subtitle?: string,}Type Aliases
Section titled “Type Aliases”Type aliases assign a type to a name. They are the basic building blocks of Genotype.
Yeah: true
Greeting: { message: string}Type aliases are translated to the corresponding type definitions in the target languages:
export type Yeah = true;
export interface Greeting { message: string;}#[serde_literal(true)]#[derive(Serialize, Deserialize)]pub struct Yeah;
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]pub struct Greeting { pub message: String,}type Yeah = Literal[True]
class Greeting(Model): message: strReferences
Section titled “References”You can reference type aliases in other type definitions:
User: { name: string, address: Address,}
Address: { country: string, city: string,}The order of type definitions doesn’t matter. You can reference types defined later in the file.
Languages that don’t support forward references, such as Python, will generate code that works around this limitation.
Inline Type Aliases
Section titled “Inline Type Aliases”A type alias can also be defined inline, as part of another type definition:
Customer: { email: string, billingAddress: BillingAddress: { street: string, city: string, },}Modules
Section titled “Modules”Every Genotype file is a module that exports all type aliases defined in it.
You can import types from other modules via use statement.
use ./destination/Destination
Shipment: { trackingNumber: string, destination: Destination,}Destination: { country: string, city: string,}In this example ./destination is a relative module path (with .type extension omitted). The path might point to nested directories, e.g., ./models/shipment, as well as .. for parent directories, e.g., ../../shipment.
There are multiple ways to import types from other modules:
// Use multiple types from the same module:use ./location/{Location, PostalCode}
// Re-alias a type from a separate module:use ./place/{StreetAddress as Place}
// Glob import from another separate module:use ./coordinates/*
Venue: { location: Location, postalCode: PostalCode, latitude: Latitude, longitude: Longitude, // Inline import from its own module: venueLocation: ./venue/VenueLocation,}External Modules
Section titled “External Modules”Genotype currently have no fully-fledged package manager nor external module system (it’s planned though). But you can map certain paths to external modules per target language, e.g. to make shared_types available in all targets, configure using [<target>.dependencies] section in genotype.toml:
[ts]enabled = true
[ts.dependencies]genotype_core = "@genotype-lang/types"
[rs]enabled = true
[rs.dependencies]genotype_core = "genotype_core"
[py]enabled = true
[py.dependencies]genotype_core = "genotype_core"Then you can import types from genotype_core in your Genotype files:
use genotype_core/Node
Diagnostic: { kind: "error" | "warning", message: string, node: Node,}Unions
Section titled “Unions”One of the most basic yet powerful features of Genotype is the ability to define union types:
Response: ResponseSuccess | ResponseError
ResponseSuccess: { status: "success", body: { user: User }}
ResponseError: { status: "failure" | "timeout", error?: string,}In languages that support union types, such as TypeScript, it will translate into the corresponding syntax. Languages that don’t, i.e., Rust, will generate an enum type with variants for each union member:
export type Response = ResponseSuccess | ResponseError;
export interface ResponseSuccess { status: "success"; body: { user: User; };}
export interface ResponseError { status: "failure" | "timeout"; error?: string | undefined;}#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]pub struct User {}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]#[serde(untagged)]pub enum Response { Success(ResponseSuccess), Error(ResponseError),}
#[serde_literals]#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]#[literals(status = "success")]pub struct ResponseSuccess { pub body: ResponseSuccessBody,}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]pub struct ResponseSuccessBody { pub user: User,}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]pub struct ResponseError { pub status: ResponseErrorStatus, #[serde(default, skip_serializing_if = "Option::is_none")] pub error: Option<String>,}
#[serde_literals]#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]pub enum ResponseErrorStatus { #[literal("failure")] Failure, #[literal("timeout")] Timeout,}class ResponseSuccessBody(Model): user: User
class ResponseSuccess(Model): status: Literal["success"] body: ResponseSuccessBody
class ResponseError(Model): status: Literal["failure"] | Literal["timeout"] error: Optional[str] = None
type Response = ResponseSuccess | ResponseErrorSee more details on the union translation to TypeScript, Rust, and Python.
Comments and Docs
Section titled “Comments and Docs”Genotype supports line and block comments:
// Line comment.
/* A block comment. */
Hello: /* Inline block comment */ "world" // EOL comment.Line and block comments are omitted in generated code.
Doc Comments
Section titled “Doc Comments”Unlike regular comments, doc comments are preserved in generated code and translated into the target language’s documentation format.
//! Module documentation.//!//! Can span multiple lines.
/// Type documentation.Member: { /// Field documentation. displayName: string,}/** @file Module documentation. * * Can span multiple lines. */
/** Type documentation. */export interface Member { /** Field documentation. */ displayName: string;}//! Module documentation.//!//! Can span multiple lines.use serde::{Deserialize, Serialize};
/// Type documentation.#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]pub struct Member { /// Field documentation. #[serde(rename = "displayName")] pub display_name: String,}"""Module documentation.
Can span multiple lines."""
from __future__ import annotations
from pydantic import Fieldfrom genotype import Model
class Member(Model): """Type documentation."""
display_name: str = Field(alias="displayName") """Field documentation."""Data Types
Section titled “Data Types”Primitives
Section titled “Primitives”Genotype has several scalar types available:
- Numeric:
number,int,float(as well as sized types, i.e.,i32,i64,f32etc) - Boolean:
boolean. - String:
string.
Player: { username: string, score: int, isOnline: boolean,}Same as in TypeScript, Genotype also supports literal types:
Answer: 42
Yes: trueNo: false
Result: { status: "ok"}…as well as null:
Employee: { name: string, departmentId: int | null,}See more details on the primitive type translation to TypeScript, Rust, and Python.
Branded Primitives
Section titled “Branded Primitives”You can brand a primitive to create a distinct type that isn’t interchangeable with the original primitive:
UserId: @string
RowId: @intYou can brand any primitive, including booleans, sized integers, and floating-point types.
A branded primitive type is translated to the corresponding idiomatic type in the target language:
export type UserId = string & { [userIdBrand]: true };declare const userIdBrand: unique symbol;
export type RowId = number & { [rowIdBrand]: true };declare const rowIdBrand: unique symbol;#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]pub struct UserId(pub String);
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]pub struct RowId(pub i64);UserId = NewType("UserId", str)
RowId = NewType("RowId", int)See more details on the branded primitive translation to TypeScript, Rust, and Python.
Composite Types
Section titled “Composite Types”Objects
Section titled “Objects”An object type is a collection of named fields, each with its own type:
Resident: { fullName: string, yearsAtAddress: int, address: Address,}Objects also can be nested:
Event: { title: string, capacity: int, venue: { country: string, city: string, },}See more details on the object translation to TypeScript, Rust, and Python.
Optional Object Fields
Section titled “Optional Object Fields”An object field can be optional, which means it may or may not be present in the object:
Delivery: { recipient: string, weight: int, address?: Address,}See more details on the optional object field translation to TypeScript, Rust, and Python.
Object Extensions
Section titled “Object Extensions”Object fields can be extended with other object types:
MessageBase: { id: int, timestamp: int,}
MessageSuccess: { ...MessageBase, status: "success",}
MessageFailure: { ...MessageBase, status: "failure", error: string,}The languages that support extensions, such as TypeScript and Python, it would translate into the corresponding syntax. Languages that don’t, i.e., Rust, will copy the fields from the base type into the extended type:
export interface MessageBase { id: number; timestamp: number;}
export interface MessageSuccess extends MessageBase { status: "success";}
export interface MessageFailure extends MessageBase { status: "failure"; error: string;}#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]pub struct MessageBase { pub id: i64, pub timestamp: i64,}
#[serde_literals]#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]#[literals(status = "success")]pub struct MessageSuccess { pub id: i64, pub timestamp: i64,}
#[serde_literals]#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]#[literals(status = "failure")]pub struct MessageFailure { pub id: i64, pub timestamp: i64, pub error: String,}class MessageBase(Model): id: int timestamp: int
class MessageSuccess(MessageBase, Model): status: Literal["success"]
class MessageFailure(MessageBase, Model): status: Literal["failure"] error: strYou can extend with multiple object types:
AnimalMammal: { warmBlooded: boolean }AnimalPet: { name: string }
AnimalCat: { ...AnimalMammal, ...AnimalPet,}See more details on the object extension translation to TypeScript, Rust, and Python.
Arrays
Section titled “Arrays”An array type represents a list of elements of a given type:
Post: { title: string, tags: [string],}The element type can be a union, i.e., each element can have any type in the union:
Values: [string | int | boolean]In languages that don’t allow mixed types in array-like structures, such as Rust, Genotype will generate a Vec of an enum type:
pub type Values = Vec<ValuesItem>;
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]#[serde(untagged)]pub enum ValuesItem { String(String), Int(i64), Boolean(bool),}See more details on the array translation to TypeScript, Rust, and Python.
Tuples
Section titled “Tuples”A tuple type represents a fixed-length list of elements of given types:
Point: (float, float)Tuple elements can be union types, i.e., each element can have any type in the union:
Setting: (string | int, boolean)See more details on the tuple translation to TypeScript, Rust, and Python.
Records
Section titled “Records”A record type represents a collection of named fields, each with its own type, but unlike objects, records can have arbitrary field names:
Scores: { [int]: float }A record key can be string, boolean, a numeric type, or a reference to one of these primitives. You can also use references to branded primitives:
ProductId: @int
ProductNames: { [ProductId]: string }A record with string as a key can omit the key type:
AliasMap: { []: string }See more details on the record translation to TypeScript, Rust, and Python.
Special Data Types
Section titled “Special Data Types”There are several special data types available in Genotype:
Any Type
Section titled “Any Type”When you need to represent a value of any type, similar to TypeScript’s any, you can use the any type:
ApiResponse: { status: "ok" | "error", payload: any,}Languages that has any type, it will translate into the corresponding syntax. Languages that don’t, i.e., Rust, will use Genotype runtime to represent it:
export interface ApiResponse { status: "ok" | "error"; payload: any;}use genotype_runtime::Any;
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]pub struct ApiResponse { pub status: ApiResponseStatus, pub payload: Any,}
#[serde_literals]#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]pub enum ApiResponseStatus { #[literal("ok")] Ok, #[literal("error")] Error,}class ApiResponse(Model): status: Literal["ok"] | Literal["error"] payload: AnySee more details on the any type translation to TypeScript, Rust, and Python.
Generic Types
Section titled “Generic Types”Genotype supports basic generic types, enabling more of the type composition and reuse.
Message<Payload>: { id: int, timestamp: int, payload: Payload,}Generics also can be used in object extensions, allowing to reuse the same base type with different payloads:
Notification<Payload>: { notificationId: int, sentAt: int, payload: Payload,}
NotificationText: { ...Notification<string>}…or even compose generic types with other generic types:
Request<Payload>: { requestId: int, timeout: int, payload: Payload,}
RequestReply: { ...Request<Reply<string>>}
Reply<Body>: ReplySuccess<Body> | ReplyFailure
ReplyBase<Status>: { status: Status}
ReplySuccess<Body>: { ...ReplyBase<"success">, body: Body}
ReplyFailure: { ...ReplyBase<"failure">, error: string}See more details on the generic type translation to TypeScript, Rust, and Python.
Recursive Types
Section titled “Recursive Types”Types can refer to themselves or form cycles across definitions and modules:
LinkedNode: { value: string, next?: LinkedNode,}
Json: null | boolean | number | string | [Json] | { []: Json }Targets add the indirection their type systems need. E.g., Rust boxes direct recursive fields, while TypeScript’s Zod mode uses lazy schemas or object getters:
export interface LinkedNode { value: string; next?: LinkedNode | undefined;}
export type Json = | null | boolean | number | string | Array<Json> | Record<string, Json>;#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]pub struct LinkedNode { pub value: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub next: Option<Box<LinkedNode>>,}
#[serde_literals]#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]pub enum Json { #[literal(null)] Null, Boolean(bool), Number(f64), String(String), Vec(Vec<Json>), Map(BTreeMap<String, Json>),}type Json = Literal[None] | bool | float | str | list[Json] | dict[str, Json]
class LinkedNode(Model): value: str next: Optional[LinkedNode] = NoneSee more details on the recursive type translation to TypeScript, Rust, and Python.
Annotations
Section titled “Annotations”Annotations add structured metadata to types, fields, and union members. Most annotations belong to a specific target.
For example:
#[discriminator = "status"]JobResult: JobSuccess | JobFailure
JobSuccess: { status: "success", body: string,}
JobFailure: { status: "failure", error: string,}
JobStatus: | #[variant = "Ok"] "success" | #[variant = "Nope"] "failure"Here discriminator adds Python union schema metadata:
class JobSuccess(Model): status: Literal["success"] body: str
class JobFailure(Model): status: Literal["failure"] error: str
type JobResult = Annotated[ JobSuccess | JobFailure, Field(json_schema_extra={"discriminator": "status"}),]
# ...While variant customizes Rust enum variant names:
// ...
#[serde_literals]#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]pub enum JobStatus { #[literal("success")] Ok, #[literal("failure")] Nope,}Generators ignore annotations they don’t recognize.
See more details on the annotation translation to TypeScript, Rust, and Python.