Skip to content

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.

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 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;
}

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.

A type alias can also be defined inline, as part of another type definition:

Customer: {
email: string,
billingAddress: BillingAddress: {
street: string,
city: string,
},
}

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,
}

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,
}

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,
}

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;
}

See more details on the union translation to TypeScript, Rust, and Python.

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.

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;
}

Genotype has several scalar types available:

  • Numeric: number, int, float (as well as sized types, i.e., i32, i64, f32 etc)
  • Boolean: boolean.
  • String: string.
Player: {
username: string,
score: int,
isOnline: boolean,
}

Same as in TypeScript, Genotype also supports literal types:

Answer: 42
Yes: true
No: 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.

You can brand a primitive to create a distinct type that isn’t interchangeable with the original primitive:

UserId: @string
RowId: @int

You 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;

See more details on the branded primitive translation to TypeScript, Rust, and Python.

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.

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 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;
}

You 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.

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.

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.

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.

There are several special data types available in Genotype:

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;
}

See more details on the any type translation to TypeScript, Rust, and Python.

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.

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>;

See more details on the recursive type translation to TypeScript, Rust, and Python.

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.