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: interface User { name: string; age: number; } function adults(users: User[]): User[] { return users.filter((user) => user.age >= 18); } const 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: const initial = 'A'; // char, not a one-character string const text = "héllo"; text.length satisfies uint; // UTF-16 code units, as in JS text[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 | | Numeric as | big as int8 | lossless conversions only, lossy ones are explicit methods | Conversions Lossy numeric conversion requires an explicit method call: const big: int32 = 100_000; const bad = big as int8; // ERROR: lossy conversion big.truncate(); // keep the low bits big.saturate(); // clamp to bounds big.tryInto(); // 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: declare const input: any; // ERROR: `any` is forbidden declare const data: unknown; if (data is string) { data satisfies string; } 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 | Record | closed utility type, use Map 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 Point = { x: number; y: number }; struct Rectangle { start: Point; // exact storage, as in TypeScript } interface PointLike { x: number; y: number; } struct Sprite { position: PointLike; // induces a hidden generic: Sprite } Freshness The classic typo stays caught, at every boundary: interface SquareConfig { color?: string; width?: number; } declare function createSquare(config: SquareConfig): void; createSquare({ 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: class Point2 { x = 0; y = 0; } class Point3 { x = 0; y = 0; z = 0; } const point: Point2 = new Point3(); // ERROR (OK in TypeScript) const 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: class Shape {} class Circle extends Shape {} declare const circles: Circle[]; const shapes: Shape[] = circles; // ERROR: writing a Square through `shapes` would corrupt `circles` const 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: function printAll(values: string | string[] | null): void { if (values) { // ERROR: condition must be boolean } if (values !== null) { // OK: same narrowing, explicit test } } Is is replaces both runtime typeof tests and predicate signatures, and the compiler checks it instead of trusting it: function padLeft(padding: number | string, input: string): string { if (padding is number) { return " ".repeat(padding) + input; } return padding + input; } 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 | value | not supported, use value as T or value satisfies T | Equality Value types compare with Equal, not identity: struct Point { x: int32; y: int32; } Point { 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 | () => value | not supported, ambiguous with TSX, use () => value | | Dynamic constructors | new (factory())() | not supported, construct values with type syntax like new Widget() | 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 | | 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 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 only | Exceptions The consuming side still reads like TypeScript: newtype ParseError = string; declare function parsePort(raw: string): Result; try { const port = parsePort(input)?; connect(port); } catch (error: ParseError) { report(error); } Promises Async failure travels as AsyncResult, which is just Promise>: declare function load(): Promise; load().catch(report); // ERROR: `catch` does not exist, promises do not reject declare function fetchUser(id: string): AsyncResult; async function rename(id: string, name: string): AsyncResult { const user = (await fetchUser(id))?; return Result.ok(user.with({ name })); } 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: let user = new User(); // managed: the runtime owns it let 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. let mut point = Point { x: 1, y: 2 }; let a = &mut point; let b = &mut point; // ERROR: cannot borrow `point` as mutable more than once let point = ^Point { x: 1, y: 2 }; let a = &point; let b = &point; // OK: mutable borrows may alias a.x = 3; b.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: 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: let mut items = vec![1, 2, 3]; let first = &items[1]; items.push(4); // ERROR: cannot borrow `items` as mutable first; let items: Array = [1, 2, 3]; let item = &readonly items[1]; items.push(4); // ERROR: growth needs exclusive access while `item` is live item; Moves Owned values move exactly like Rust values; managed handles copy freely. let a: ^Buffer = Buffer.open(); let b = a; a.length; // ERROR: `a` moved into `b` let x = new User(); let y = x; x.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. impl Drop for Connection { fn drop(&mut self) { self.close(); } } class Connection implements Drop { drop(): void { this.close(); } } Lifetimes Inference Lifetimes are ordinary comptime value parameters ( on Borrowed), and they are inferred everywhere, including from function bodies. fn first<'a>(items: &'a [String]) -> &'a str { &items[0] } function first(items: &[string]): &string { &items[0] } Structs Borrow-holding structs work like Rust's, with the lifetime as a comptime parameter: struct View<'a> { name: &'a str, } struct View { name: Borrowed; } 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: declare function choose( first: Borrowed, second: Borrowed, ): Borrowed; Traits Interfaces Traits map to newtype interfaces. The standard Iterator is deliberately the same shape: trait Iterator { type Item; fn next(&mut self) -> Option; } newtype interface Iterator { type Item; type Return = void; next(): IteratorResult; } Implementations There is no orphan rule: any module may implement any interface for any type, because Destack compiles whole programs and checks uniqueness globally. // ERROR: only traits defined in the current crate can be implemented (E0117) impl Display for Vec { ... } // fine: `implements` is globally unique per (type, interface) pair extension of Array implements Show { show(): string { `${this.length} bytes` } } 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. impl Pretty for T { ... } // only valid in the crate that owns Pretty extension of T implements Pretty { ... } // same rule, same reason Blankets over nominal applications (extension of Box 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: fn sum(values: impl Iterator) -> u32 { ... } function sum(values: Iterator): uint32 { ... } // sugar for Iterator Data Enums Rust enums map to newtype unions over object variants, with @derive(Tagged) supplying constructors and discriminants: enum Shape { Circle { radius: f64 }, Square { side: f64 }, } @derive(Tagged) newtype Shape = | { kind: "circle"; radius: float64 } | { kind: "square"; side: float64 }; match (shape) { Shape.Circle({ radius }) => radius * radius * 3.14 Shape.Square({ side }) => side * side } Optionality const some: int32 | null = 1; const none: int32 | null = null; type Present = NonNullable; Destack does not mirror Rust's Option 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. Errors Results Result and ? carry over directly, and try / catch is sugar over the same control flow: let user = load_user(id)?; 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. let result = std::panic::catch_unwind(|| risky()); // recoverable in-process 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: let handle = tokio::spawn(async { fetch().await }); // leaks if never awaited await using scope = TaskScope.open(); const 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: fn draw(shape: impl Shape) { ... } // argument position: universal fn make() -> impl Shape { ... } // return position: existential function draw(shape: Shape): void {} // same universal, no keyword function 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. struct Buffer { data: [u8; N] } struct Buffer { data: [uint8; N]; } Erasure dyn Trait maps to Dynamic, with DynamicSafe mirroring object safety: let shapes: Vec> = vec![circle, square]; let shapes: Dynamic[] = [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. let pointer = &user as *const User; unsafe { (*pointer).name.clone() } let pointer: *User = &user; @unsafe function read(pointer: *User): string { pointer.name.clone() } 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. #[derive(Serialize)] struct User { name: String } @derive(Serialize) struct User { name: string; } 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 SIZE: usize = compute_size(); // compute_size must be a const fn 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: function send(user: { name: string }) {} send({ name: "Ada", admin: true }); // ERROR: `admin` is missing in object type function send(user: { name: string }): void {} send({ name: "Ada", admin: true }); // ERROR: excess property `admin` Nominality Classes Both Flow and Destack make classes nominal, against TypeScript's structural treatment: class Left { value: int32 = 0; } class Right { value: int32 = 0; } const 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. export opaque type ID = string; // transparent inside this file, opaque outside 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: type Source<+T> = { +value: T }; interface Source { readonly value: T; // computes covariant on its own } Predicates Guards Flow's %checks functions assert refinements; Destack's is expressions are guards the compiler understands directly: function isString(value: mixed): boolean %checks { return typeof value === "string"; } if (value is string) { value satisfies string; } 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: function clamp(value: i32, low: i32, high: i32): i32 { ... } 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: let big: i32 = 2147483647; big + 1; // -2147483648, silently let big: int32 = 2147483647; big + 1; // traps: overflow big.wrappingAdd(1); // -2147483648, explicitly Wrapping(big) + 1; // or wrap by type Expressiveness Closures Closures over mutable locals - the everyday TypeScript AssemblyScript cannot compile - work natively: function counter(): () => int32 { let count = 0; return () => { count += 1; count }; } Unions Union types, the other major gap, are first-class and reified: function describe(value: int32 | string): string { value is string ? value : `#${value}` } Memory Values @unmanaged classes map to struct: value types are a declaration form, not an annotation on classes. @unmanaged class Vec2 { x: f32; y: f32; } struct Vec2 { x: float32; y: float32; } 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. struct Point { public double X, Y; } class User { public string Name = ""; } struct Point { x: float64; y: float64; } class User { name: string = ""; } Records Records map to structs for value semantics or newtypes for nominal wrappers, with equality via @derive(Equal) instead of being implicit: record Point(double X, double Y); @derive(Equal) struct Point { x: float64; y: float64; } (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: class Counter { private int value; public int Count { get => value; set => this.value = value; } } class Counter { private value: int32 = 0; get count(): int32 { this.value } set count(next: int32) { this.value = next; } } 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. interface ISource { T Take(); } interface Source { take(): T; } Constraints where clauses are nearly character-identical: T Largest(List values) where T : IComparable { ... } function largest(values: T[]): T where T: Comparable { ... } Errors Exceptions C# keeps exceptions; Destack moves failure into values like Rust: try { var user = await LoadUser(id); } catch (LoadException error) { Report(error); } const result = await loadUser(id); match (result) { Ok { value } => use(value) Err { error } => report(error) } Nullability C#'s nullable reference types map to plain nullable unions, checked the same way: string? name = FindName(id); if (name is not null) { Use(name); } const name: string | null = findName(id); if (name !== null) { use(name); } Concurrency Async async / await carries over unchanged, with one naming collision to know about: C#'s Task is Destack's Promise (the awaitable value), while Destack's Task is a scope-owned work item closer to a structured Task.Run. async Task LoadUser(string id) { ... } async function loadUser(id: string): Promise { ... }