destack.sh

Comparison

It's generally helpful to learn a new thing in contrast to things we already know, and in that spirit we have compiled a short comparison overview of what Destack looks like coming from each of these languages. The Destack language is deliberately trying to be "TypeScript++", so that's where we begin, but there are many similarities and some differences to other well known languages - most notably Rust - that bear pointing out.

TypeScript

Destack is a superset of the "modern strict" subset of TypeScript. The intended use case for Destack is making TS-shaped code correct and optimal, which requires both cutting all accumulated dynamic magic and introducing systems-y features for memory control. Accordingly, Destack excludes legacy syntax and all sorts of dynamic shapes and protocols that are not statically sound, and also removes a few rarely used footguns.

It's important to note that Destack deliberately has no JavaScript or NPM interoperability: .ds code cannot import or call arbitrary JS, and Destack packages cannot depend on npm packages in any way. The main reason for this is that to take full advantage of Destack's features and full integration across the stack, we need to essentially rewrite all libraries anyway.

In general, most strict modern TypeScript needs no changes at all:

TypeScript.ds
1interface User {2    name: string;3    age: number;4}5 6function adults(users: User[]): User[] {7    return users.filter((user) => user.age >= 18);8}9 10const names = adults([{ name: "Ada", age: 36 }]).map((user) => user.name);

Modules

Destack supports only ESM syntax: one static module graph, nothing mutable at runtime.

Feature Example Ruling
Type-only imports / exports import type { User } from "./user" supported as plain import / export aliases
CommonJS require("x"), module.exports not supported, a legacy mutable runtime module system
Namespace declarations namespace Name { ... } not supported, use real modules
String module declarations declare module "pkg" { ... } not supported, declare real modules instead
Declaration merging two interface User blocks not supported, one declaration per name, extensions cover augmentation
Import type queries import("pkg").User not supported, use ordinary static imports
Dynamic module loading import(expr) not source-level loading, though JS output may still chunk
Import defer import defer * as ns from "pkg" not supported
Import assertions assert { type: "json" } not supported, use standardized with { ... } attributes
String export names export { value as "name" } not supported
Circular inference mutually inferred module exports not supported across modules
Flow and JSDoc typing /** @type {Foo} */ ignored, or rejected when not valid TS/TS++

Values

Bindings are strict-mode const and let with definite assignment, and the legacy spellings are gone.

Feature Example Ruling
Var declarations var x not supported, var scoping is unnecessary with const and let
Destructuring const { name } = user supported in declarations, assignments, parameters, catch bindings, and loops
Shadowable undefined let undefined = value not supported, undefined is a literal keyword just like null
Sequence expressions (a, b, c) not supported, parenthesized comma lists are explicit tuples in .ds
Definite assignment assertions let x!: T, field!: T rejected in .ds, locals and fields must be initialized before use
Sloppy mode duplicate declarations, with not supported, Destack targets strict mode
Callable Symbol Symbol("name") not supported, use Symbol.create("name")

Primitives

The TypeScript primitives keep their meaning, but we support more scalar types.

Feature Example Ruling
number let x: number stays the default numeric type, an alias for float64
Integer and float widths int32, uint8, float32, ... added as real scalar types
Single-quoted literals 'A' always a char; strings use double quotes
String indexing text[0] yields char in .ds, and traps on a lone surrogate
symbol let key: symbol supported as a regular property key type
unique symbol const key: unique symbol supported for statically known singleton keys

Characters

Single quotes are scalars, double quotes are strings:

Characters.ds
1const initial = 'A'; // char, not a one-character string2 3const text = "héllo";4 5text.length satisfies uint; // UTF-16 code units, as in JS6text[1] satisfies char;

Arithmetic

Integer arithmetic traps on overflow in all build configurations, and as never loses information.

Feature Example Ruling
Overflow int32 max + 1 traps in every build mode
Wrapping (a + b) & 0xff idioms explicit: wrappingAdd, Wrapping<T>
Numeric as big as int8 lossless conversions only, lossy ones are explicit methods

Conversions

Lossy numeric conversion requires an explicit method call:

Conversions.ds
1const big: int32 = 100_000;2 3const bad = big as int8;    // ERROR: lossy conversion4big.truncate<int8>();       // keep the low bits5big.saturate<int8>();       // clamp to bounds6big.tryInto<int8>();        // checked

Unknown

unknown stays; any dies.

Feature Example Ruling
any let x: any forbidden in all sources, an unchecked escape hatch in both directions
unknown declare const data: unknown supported, and induces a generic in constraint positions

Any and Unknown

unknown does the safe half of any's job:

Any and Unknown.ds
1declare const input: any; // ERROR: `any` is forbidden2 3declare const data: unknown;4 5if (data is string) {6    data satisfies string;7}

Shapes

Object shapes are static and exact: no prototype tricks, no runtime mutation, and fresh literals are checked everywhere.

Feature Example Ruling
Interchangeable type / interface data-shaped interface Point in a field diverges in storage positions
Record<K, V> Record<string, User> closed utility type, use Map<K, V> for dynamic keyed storage
object let value: object not supported, use a structural shape, unknown, or an interface
Declaration expressions const C = class {} not supported, runtime type generation is not statically knowable
Prototype objects .prototype, .__proto__, Object.setPrototypeOf not supported
Shape mutation delete obj.x, Object.defineProperty forbidden, object shapes must stay statically known
Metaobject dispatch Proxy, most Reflect.* APIs not supported
Array holes [1,,3] not supported, sequences are dense
Excess properties fresh literal with extra fields rejected at every boundary, not just some

Type vs Interface

In TypeScript, type and interface are mostly interchangeable. In Destack they diverge in storage positions: aliases are exact data shapes, interfaces are constraints and induce hidden generics when stored.

Type vs Interface.ds
1type Point = { x: number; y: number };2 3struct Rectangle {4    start: Point; // exact storage, as in TypeScript5}6 7interface PointLike {8    x: number;9    y: number;10}11 12struct Sprite {13    position: PointLike; // induces a hidden generic: Sprite<T: PointLike>14}

Freshness

The classic typo stays caught, at every boundary:

Freshness.ds
1interface SquareConfig {2    color?: string;3    width?: number;4}5 6declare function createSquare(config: SquareConfig): void;7 8createSquare({ colour: "red", width: 100 }); // ERROR: excess property `colour`

Classes

Classes keep their TypeScript surface but become properly nominal.

Feature Example Ruling
Structural classes same-shaped classes interchangeable classes are nominal, structure does not substitute for declarations
Private fields #field not supported, redundant with real private in .ds
Parameter properties constructor(private name: string) not supported, declare fields and assignments explicitly
Class index signatures class C { [key: string]: T } not supported, classes have fixed declared members

Nominality

Two classes with the same shape are still two classes:

Nominality.ds
1class Point2 {2    x = 0;3    y = 0;4}5 6class Point3 {7    x = 0;8    y = 0;9    z = 0;10}11 12const point: Point2 = new Point3(); // ERROR (OK in TypeScript)13const plain: Point2 = { x: 0, y: 0 }; // ERROR (also OK in TypeScript!)

TypeScript classes only turn nominal once they have a private member, which is why the branding hack works; Destack classes are nominal always.

Enums

Enum fields are nominal constants, without TypeScript's number leakage.

Feature Example Ruling
Enum coercion Level.A as number no implicit coercion, enum fields are nominal constants

Generics

Generics work as in TypeScript, with explicitness required where inference would have to guess.

Feature Example Ruling
Declaration parameter inference function f(x = 1) {} not supported, public declaration surfaces need explicit parameter types
Bodyless concrete overloads function f(x: string); not supported outside declaration contexts, just write multiple bodies

Variance

TypeScript's best known soundness hole is closed: mutable positions are invariant.

Feature Example Ruling
Mutable covariance Circle[] as Shape[] not supported, mutable generic positions are invariant

Arrays

Readonly views keep the safe half of array covariance:

Arrays.ds
1class Shape {}2class Circle extends Shape {}3 4declare const circles: Circle[];5 6const shapes: Shape[] = circles;        // ERROR: writing a Square through `shapes` would corrupt `circles`7const view: readonly Shape[] = circles; // OK: readonly views are covariant

Guards

Flow narrowing works as in TypeScript; only the guards themselves change.

Feature Example Ruling
Truthiness if (value) boolean values only, control flow needs explicit tests
Runtime typeof narrowing typeof x === "string" not supported, use is or instanceof
Type queries typeof value supported in type position only
Type predicate / assertion signatures value is T, asserts value is T not supported, callable type guards claim refinements that cannot be checked

Truthiness

if takes booleans, not values:

Truthiness.ds
1function printAll(values: string | string[] | null): void {2    if (values) {3        // ERROR: condition must be boolean4    }5 6    if (values !== null) {7        // OK: same narrowing, explicit test8    }9}

Is

is replaces both runtime typeof tests and predicate signatures, and the compiler checks it instead of trusting it:

Is.ds
1function padLeft(padding: number | string, input: string): string {2    if (padding is number) {3        return " ".repeat(padding) + input;4    }5 6    return padding + input;7}

Operators

Operators keep their JavaScript meaning wherever JavaScript has one, and stop existing where it does not.

Feature Example Ruling
Loose equality coercion a == b on objects no object coercion
=== on value types pointA === pointB rejected for structs, tuples, and fixed arrays, use Equal
Coercion hooks valueOf, Symbol.toPrimitive not used for implicit coercion
Symbol magic Symbol.hasInstance, Symbol.species not supported, use typed protocols
Non-null assertions value! supported as Try / must unwrapping, an explicit runtime operation
Type angle assertions <T>value not supported, use value as T or value satisfies T

Equality

Value types compare with Equal, not identity:

Equality.ds
1struct Point {2    x: int32;3    y: int32;4}5 6Point { x: 1, y: 2 } === Point { x: 1, y: 2 }; // ERROR: `===` has no JS meaning for value types

Closures

Callable context is explicit: no ambient arguments, no dynamic this rebinding.

Feature Example Ruling
Ambient call metadata arguments, new.target, dynamic this not supported as magic bindings, callable context is explicit
Ambiguous generic arrow <T>() => value not supported, ambiguous with TSX, use <T,>() => value
Dynamic constructors new (factory())() not supported, construct values with type syntax like new Widget<T>()

Loops

Every loop form works unchanged; only the manual iteration protocol is replaced. (e.g., switch still has TypeScript fallthrough semantics)

Feature Example Ruling
Manual iterator protocol iter.next().done not supported, iteration is the nominal Iterator protocol, while for-of and spread lower unchanged
Destructuring in for-of for (const { name } of users) supported
for-in for (const key in object) supported for object-shaped receivers and yields string keys

Trees

TSX works as tree syntax, minus XML namespace semantics.

Feature Example Ruling
XML namespace resolution <svg:path /> no xmlns binding semantics, namespaced tags are intrinsic string tag names

Errors

Failures are Result values: throw is gone, and a Promise never rejects.

Feature Example Ruling
Exceptions executing throw not supported in native Destack code
try / catch / finally try { ... } catch (e) { ... } works, as sugar over Result control flow
Catch destructuring catch ({ message }) supported when the pattern is irrefutable for the failure value
Promise rejection .catch, Promise.reject gone, a Promise<T> always fulfills
Rejection-shaped APIs Promise.any, allSettled, two-arg then gone with rejection
Floating promises bare refresh(); statement denied by default, await it, return it, or hand it to a scope
Thenables await customThenable not supported, await works on the well known Promise<T> only

Exceptions

The consuming side still reads like TypeScript:

Exceptions.ds
1newtype ParseError = string;2 3declare function parsePort(raw: string): Result<uint16, ParseError>;4 5try {6    const port = parsePort(input)?;7    connect(port);8} catch (error: ParseError) {9    report(error);10}

Promises

Async failure travels as AsyncResult<T, E>, which is just Promise<Result<T, E>>:

Promises.ds
1declare function load(): Promise<User>;2 3load().catch(report); // ERROR: `catch` does not exist, promises do not reject4 5declare function fetchUser(id: string): AsyncResult<User, LoadError>;6 7async function rename(id: string, name: string): AsyncResult<User, LoadError> {8    const user = (await fetchUser(id))?;9    return Result.ok(user.with({ name }));10}

Decorators

Decorators stay, and move to compile time: a decorator is a comptime value that may transform its target as a macro.

Feature Example Ruling
Class and member decorators @route("/users") class ... supported, evaluated at compile time
Runtime decorator metadata emitDecoratorMetadata not supported, use reflection

Comptime

Runtime code generation conflicts with ahead-of-time compilation, so generation moves to compile time.

Feature Example Ruling
Dynamic code generation runtime eval, new Function not supported except explicit comptime eval

Rust

Destack's memory model is Rust-shaped with inverted defaults: values are managed unless you opt into ownership, and mutability is decoupled from exclusivity.

Ownership

Defaults

In Rust every value has exactly one owner. In Destack the same is true, but for managed values the owner is the runtime, and ownership in the Rust sense is opt-in with ^T:

Defaults.ds
1let user = new User();         // managed: the runtime owns it2let owned: ^User = new User(); // owned: single owner, deterministic drop

The managed default is why existing TypeScript works unchanged.

Borrowing

Unlike in Rust, mutability is decoupled from borrowing: there are three borrow forms, and the aliased mutable &T sits between Rust's two.

Borrowing.rust
1let mut point = Point { x: 1, y: 2 };2let a = &mut point;3let b = &mut point; // ERROR: cannot borrow `point` as mutable more than once
Borrowing.ds
1let point = ^Point { x: 1, y: 2 };2let a = &point;3let b = &point; // OK: mutable borrows may alias4 5a.x = 3;6b.y = 4;

This is sound because a Worker is a single-threaded execution domain with explicit suspension points, so the writes cannot race. &exclusive is &mut, and it still exists for when we do want no aliasing:

Borrowing.ds
let c = &exclusive point; // ERROR: `a` and `b` are still live

Interior mutability (Cell, RefCell) exists too, but aliased &T covers most of what Rust needs it for.

Exclusivity

Exclusivity is reserved for operations that may invalidate another borrow, instead of being required for every write. Where invalidation is possible, Destack and Rust agree:

Exclusivity.rust
1let mut items = vec![1, 2, 3];2let first = &items[1];3items.push(4); // ERROR: cannot borrow `items` as mutable4first;
Exclusivity.ds
1let items: Array<int32> = [1, 2, 3];2let item = &readonly items[1];3 4items.push(4); // ERROR: growth needs exclusive access while `item` is live5item;

Moves

Owned values move exactly like Rust values; managed handles copy freely.

Moves.ds
1let a: ^Buffer = Buffer.open();2let b = a;3a.length; // ERROR: `a` moved into `b`4 5let x = new User();6let y = x;7x.name; // OK: managed handles alias

Drop

Drop exists with the same meaning: owned values run their finalizer deterministically when their lifetime ends, and managed values run it when the garbage collector reclaims them.

Drop.rust
1impl Drop for Connection {2    fn drop(&mut self) { self.close(); }3}
Drop.ds
1class Connection implements Drop {2    drop(): void {3        this.close();4    }5}

Lifetimes

Inference

Lifetimes are ordinary comptime value parameters (<comptime L: Lifetime> on Borrowed<T, L>), and they are inferred everywhere, including from function bodies.

Inference.rust
1fn first<'a>(items: &'a [String]) -> &'a str {2    &items[0]3}
Inference.ds
1function first(items: &[string]): &string {2    &items[0]3}

Structs

Borrow-holding structs work like Rust's, with the lifetime as a comptime parameter:

Structs.rust
1struct View<'a> {2    name: &'a str,3}
Structs.ds
1struct View<comptime L: Lifetime> {2    name: Borrowed<string, L>;3}

Declarations

The one place lifetimes are spelled out is declare signatures, because there is no body to infer from. They participate in regular type algebra, including unions:

Declarations.ds
1declare function choose<comptime L1: Lifetime, comptime L2: Lifetime>(2    first: Borrowed<string, L1>,3    second: Borrowed<string, L2>,4): Borrowed<string, L1 | L2>;

Traits

Interfaces

Traits map to newtype interfaces. The standard Iterator is deliberately the same shape:

Interfaces.rust
1trait Iterator {2    type Item;3    fn next(&mut self) -> Option<Self::Item>;4}
Interfaces.ds
1newtype interface Iterator {2    type Item;3    type Return = void;4    next(): IteratorResult<this.Item, this.Return>;5}

Implementations

There is no orphan rule: any module may implement any interface for any type, because Destack compiles whole programs and checks uniqueness globally.

Implementations.rust
1// ERROR: only traits defined in the current crate can be implemented (E0117)2impl Display for Vec<u8> { ... }
Implementations.ds
1// fine: `implements` is globally unique per (type, interface) pair2extension of Array<uint8> implements Show {3    show(): string {4        `${this.length} bytes`5    }6}

Conflicts between packages are whole-program compile errors, and libraries should only implement pairs they own one side of (a default-warn diagnostic nudges accordingly).

Blankets

Blanket implementations are supported with one familiar restriction: an implements blanket over a bare bounded parameter may only come from the package that declares the interface.

Blankets.rust
impl<T: Display> Pretty for T { ... } // only valid in the crate that owns Pretty
Blankets.ds
extension<T: Show> of T implements Pretty { ... } // same rule, same reason

Blankets over nominal applications (extension<T> of Box<T> implements Show) are open to anyone, which Rust's orphan rule forbids.

Associated Types

Associated types and constants work as in Rust, with positional refinement as sugar:

Associated Types.rust
fn sum(values: impl Iterator<Item = u8>) -> u32 { ... }
Associated Types.ds
function sum(values: Iterator<uint8>): uint32 { ... } // sugar for Iterator<type Item = uint8>

Data

Enums

Rust enums map to newtype unions over object variants, with @derive(Tagged) supplying constructors and discriminants:

Enums.rust
1enum Shape {2    Circle { radius: f64 },3    Square { side: f64 },4}
Enums.ds
1@derive(Tagged)2newtype Shape =3    | { kind: "circle"; radius: float64 }4    | { kind: "square"; side: float64 };5 6match (shape) {7    Shape.Circle({ radius }) => radius * radius * 3.148    Shape.Square({ side }) => side * side9}

Optionality

Optionality.ds
1const some: int32 | null = 1;2const none: int32 | null = null;3 4type Present = NonNullable<int32 | null | undefined>;

Destack does not mirror Rust's Option<T> as a standard carrier. Ordinary absence uses TypeScript-style null / undefined unions directly, while APIs that need to distinguish completion from a yielded nullish value use explicit tagged results like IteratorResult<Y, R>.

Errors

Results

Result<T, E> and ? carry over directly, and try / catch is sugar over the same control flow:

Results.rust
let user = load_user(id)?;
Results.ds
const user = loadUser(id)?;

Panics

Panics take string messages only, and there is no catch_unwind: the Worker is the fault boundary, and the supervising parent observes the exit.

Panics.rust
let result = std::panic::catch_unwind(|| risky()); // recoverable in-process
Panics.ds
panic("invariant broken"); // unwinds the Worker; the parent sees WorkerExit "panicked"

Concurrency

Send and Sync

Send and Sync exist with the same meaning and the same structural derivation, applied per memory form:

Form Crosses Workers when
local managed T never
^T T: Send
&readonly T T: Sync, owned or static source
&exclusive T T: Send, owned or static source
&T never

The &T row is the cost of aliased mutability: it is only sound within one Worker, so it never crosses.

Tasks

Where tokio::spawn hands back a detachable JoinHandle, spawned work here is owned by a scope that cannot exit while children are pending:

Tasks.rust
let handle = tokio::spawn(async { fetch().await }); // leaks if never awaited
Tasks.ds
1await using scope = TaskScope.open();2const task = scope.spawn(() => fetch()); // owned: cancelled or joined before the scope exits

Workers

Threads map to Workers: isolated heaps, explicit shared memory, and message passing. Borrows of owned data may cross into scoped child tasks (subject to Send), which gives fork-join parallelism over borrowed data without Arc ceremony.

Generics

Monomorphization

Both languages monomorphize, and Destack additionally induces generics implicitly wherever a constraint appears in a parameter, so impl Trait maps to nothing at all:

Monomorphization.rust
1fn draw(shape: impl Shape) { ... }     // argument position: universal2fn make() -> impl Shape { ... }        // return position: existential
Monomorphization.ds
1function draw(shape: Shape): void {}   // same universal, no keyword2function make(): Shape { ... }         // same existential, no keyword

Constants

Const generics map to comptime parameters, and value constraints are spelled as interval bounds or checked with comptime assert at instantiation.

Constants.rust
struct Buffer<const N: usize> { data: [u8; N] }
Constants.ds
1struct Buffer<comptime N: 0..=4096> {2    data: [uint8; N];3}

Erasure

dyn Trait maps to Dynamic<T>, with DynamicSafe mirroring object safety:

Erasure.rust
let shapes: Vec<Box<dyn Shape>> = vec![circle, square];
Erasure.ds
let shapes: Dynamic<Shape>[] = [circle, square];

Variance

Rust infers variance and never surfaces it, while Destack computes it the same way but lets declarations pin it with in / out, checked against usage like TypeScript.

Unsafe

@unsafe plays the same role as unsafe, with the same culture: raw pointers are safe to create and carry, and only dereferencing them needs the marker.

Unsafe.rust
1let pointer = &user as *const User;2unsafe { (*pointer).name.clone() }
Unsafe.ds
1let pointer: *User = &user;2 3@unsafe4function read(pointer: *User): string {5    pointer.name.clone()6}

Comptime

Macros

Rust splits metaprogramming across macro_rules!, proc macros, and derive macros, each with its own toolchain. Destack has one mechanism: a decorator is a static term, and one that implements Macro may transform its target as a typed declaration using the well known eval function.

Macros.rust
1#[derive(Serialize)]2struct User { name: String }
Macros.ds
1@derive(Serialize)2struct User {3    name: string;4}

Generated code comes from comptime eval over reflected declarations, so macros compose with ordinary functions and expansion runs to a fixed point. In this example the derive can usually even be emitted since Destack additionally supports auto-derive for builtin derive macros.

Const Functions

const fn maps to comptime evaluation of ordinary functions: anything the compiler can run is fair game, with a budget instead of a stability whitelist.

Const Functions.rust
const SIZE: usize = compute_size(); // compute_size must be a const fn
Const Functions.ds
const SIZE: usize = comptime computeSize(); // any function, evaluated during compilation

Flow

Flow tried sound-by-default inside TypeScript-shaped syntax first, and Destack lands on many of the same answers.

Objects

Exactness

Flow made object types exact by default; Destack gets the same rejections through freshness and excess property checks:

Exactness.flow
1function send(user: { name: string }) {}2 3send({ name: "Ada", admin: true }); // ERROR: `admin` is missing in object type
Exactness.ds
1function send(user: { name: string }): void {}2 3send({ name: "Ada", admin: true }); // ERROR: excess property `admin`

Nominality

Classes

Both Flow and Destack make classes nominal, against TypeScript's structural treatment:

Classes.ds
1class Left {2    value: int32 = 0;3}4 5class Right {6    value: int32 = 0;7}8 9const value: Left = new Right(); // ERROR in Flow and Destack, OK in TypeScript

Opaque Types

Flow's opaque type is the closest thing in the TS family to newtype, with one difference: Flow's opacity ends at the module boundary, while a newtype is nominal everywhere and constructed explicitly.

Opaque Types.flow
export opaque type ID = string; // transparent inside this file, opaque outside
Opaque Types.ds
export newtype Id = string; // nominal everywhere, constructed as Id("...")

Variance

Sigils

Flow annotates variance per property with + / -; Destack computes it from the member surface, and in / out exist only as checked assertions:

Sigils.flow
type Source<+T> = { +value: T };
Sigils.ds
1interface Source<T> {2    readonly value: T; // computes covariant on its own3}

Predicates

Guards

Flow's %checks functions assert refinements; Destack's is expressions are guards the compiler understands directly:

Guards.flow
1function isString(value: mixed): boolean %checks {2    return typeof value === "string";3}
Guards.ds
1if (value is string) {2    value satisfies string;3}

AssemblyScript

AssemblyScript is the closest cousin: TypeScript syntax compiled ahead-of-time with explicit numeric types. Its gaps show what a TS-shaped native language cannot afford to give up.

Numbers

Naming

The numeric types correspond one-to-one, spelled out so they read as TypeScript:

Naming.ts
function clamp(value: i32, low: i32, high: i32): i32 { ... }
Naming.ds
function clamp(value: int32, low: int32, high: int32): int32 { ... }

number stays an alias for float64, so unannotated TypeScript keeps its meaning.

Overflow

AssemblyScript wraps silently, matching WASM; Destack traps on overflow in every build mode, and wrapping is explicit:

Overflow.ts
1let big: i32 = 2147483647;2big + 1; // -2147483648, silently
Overflow.ds
1let big: int32 = 2147483647;2big + 1;                 // traps: overflow3big.wrappingAdd(1);      // -2147483648, explicitly4Wrapping(big) + 1;       // or wrap by type

Expressiveness

Closures

Closures over mutable locals - the everyday TypeScript AssemblyScript cannot compile - work natively:

Closures.ds
1function counter(): () => int32 {2    let count = 0;3 4    return () => {5        count += 1;6        count7    };8}

Unions

Union types, the other major gap, are first-class and reified:

Unions.ds
1function describe(value: int32 | string): string {2    value is string ? value : `#${value}`3}

Memory

Values

@unmanaged classes map to struct: value types are a declaration form, not an annotation on classes.

Values.ts
@unmanaged class Vec2 { x: f32; y: f32; }
Values.ds
1struct Vec2 {2    x: float32;3    y: float32;4}

Equality

AssemblyScript's == / === split confused users badly enough that they changed it. Destack keeps === only where JavaScript gives it a meaning (primitives and managed identity) and rejects it elsewhere, so the question never comes up for value types.

C#

C# is the other Hejlsberg language, and several of its core distinctions transfer almost verbatim.

Values

Structs and Classes

The struct / class split means the same thing: values copied inline versus managed references.

Structs and Classes.csharp
1struct Point { public double X, Y; }2 3class User { public string Name = ""; }
Structs and Classes.ds
1struct Point {2    x: float64;3    y: float64;4}5 6class User {7    name: string = "";8}

Records

Records map to structs for value semantics or newtypes for nominal wrappers, with equality via @derive(Equal) instead of being implicit:

Records.csharp
record Point(double X, double Y);
Records.ds
1@derive(Equal)2struct Point {3    x: float64;4    y: float64;5}

(Though again it should be noted that Destack suports auto-deriving for builtin derives, so equality would be derived automatically here.)

Properties

C# properties map to accessors directly:

Properties.csharp
1class Counter {2    private int value;3    public int Count {4        get => value;5        set => this.value = value;6    }7}
Properties.ds
1class Counter {2    private value: int32 = 0;3 4    get count(): int32 {5        this.value6    }7 8    set count(next: int32) {9        this.value = next;10    }11}

Generics

Variance

The in / out spelling is C#'s, with one difference: variance is computed from usage, and the annotations are checked assertions instead of requirements.

Variance.csharp
interface ISource<out T> { T Take(); }
Variance.ds
1interface Source<out T> {2    take(): T;3}

Constraints

where clauses are nearly character-identical:

Constraints.csharp
T Largest<T>(List<T> values) where T : IComparable<T> { ... }
Constraints.ds
function largest<T>(values: T[]): T where T: Comparable<T> { ... }

Errors

Exceptions

C# keeps exceptions; Destack moves failure into values like Rust:

Exceptions.csharp
1try {2    var user = await LoadUser(id);3} catch (LoadException error) {4    Report(error);5}
Exceptions.ds
1const result = await loadUser(id);2 3match (result) {4    Ok { value } => use(value)5    Err { error } => report(error)6}

Nullability

C#'s nullable reference types map to plain nullable unions, checked the same way:

Nullability.csharp
1string? name = FindName(id);2if (name is not null) { Use(name); }
Nullability.ds
1const name: string | null = findName(id);2 3if (name !== null) {4    use(name);5}

Concurrency

Async

async / await carries over unchanged, with one naming collision to know about: C#'s Task<T> is Destack's Promise<T> (the awaitable value), while Destack's Task<T> is a scope-owned work item closer to a structured Task.Run.

Async.csharp
async Task<User> LoadUser(string id) { ... }
Async.ds
async function loadUser(id: string): Promise<User> { ... }