Repository navigation
TypeScript mapping
This document defines how IDL constructs map to TypeScript types.
The mapping targets ES2022 and is designed to be compatible with DDS-JSON. The
types will transparently serialize to JSON via JSON.stringify() and
JSON.parse().
| IDL Type | TypeScript Type | Notes |
|---|---|---|
void |
void |
Return types only |
boolean |
boolean |
|
char, wchar
|
string |
Single character |
int8, octet
|
number |
|
uint8 |
number |
|
int16, short
|
number |
|
uint16, unsigned short
|
number |
|
int32, long
|
number |
|
uint32, unsigned long
|
number |
|
int64, long long
|
number | string |
String outside ±2⁵³ |
uint64, unsigned long long
|
number | string |
String outside ±2⁵³ |
float, double
|
number |
|
long double |
number |
May lose precision |
DDS-JSON specifies that 64-bit integers in the range [−2⁵³ + 1, 2⁵³ − 1] are represented
as JSON numbers, while values outside that range are represented as strings. We map to
number | string to reflect this.
A command-line flag enables mapping to bigint instead, which is more ergonomic but
requires a custom JSON library like lossless-json for serialization.
struct Primitives {
boolean flag;
int32 count;
int64 bigNumber;
};interface Primitives {
flag: boolean;
count: number;
bigNumber: number | string;
}Structs become interfaces. We use interfaces rather than classes because structs are pure
data without behavior, and interfaces work directly with object literals and JSON.parse()
without requiring instantiation.
struct Point {
long x;
long y;
};
struct Point3D : Point {
long z;
};interface Point {
x: number;
y: number;
}
interface Point3D extends Point {
z: number;
}Self-referencing works as expected:
struct Node {
long value;
sequence<Node> children;
};interface Node {
value: number;
children: Node[];
}We use regular enums rather than const enum because regular enums exist at runtime,
which is needed for reverse lookup during deserialization.
enum Color {
RED,
GREEN,
BLUE
};
enum Status {
OK = 0,
WARNING = 100,
ERROR = 200
};enum Color {
RED = 0,
GREEN = 1,
BLUE = 2,
}
enum Status {
OK = 0,
WARNING = 100,
ERROR = 200,
}Bitmasks become enums with 2 ** n values. We use 2 ** n rather than 1 << n because
JavaScript bitwise operators are 32-bit, which breaks for positions >= 31.
bitmask Permissions {
READ,
WRITE,
EXECUTE
};enum Permissions {
READ = 2 ** 0,
WRITE = 2 ** 1,
EXECUTE = 2 ** 2,
}The @position annotation specifies bit index, not value:
bitmask Flags {
@position(0) FLAG_A,
@position(4) FLAG_B,
};enum Flags {
FLAG_A = 2 ** 0, // 1
FLAG_B = 2 ** 4, // 16
}Bit positions are limited to 0-52 due to JavaScript's 53-bit integer precision. Combined
bitmask values have type number, not the enum type, because TypeScript doesn't
understand bitwise combinations.
IDL unions become TypeScript discriminated unions with a $discriminator field, as
specified by DDS-JSON. Checking the discriminator narrows to the correct variant.
enum ValueType {
INT,
DOUBLE,
STRING
};
union Value switch (ValueType) {
case INT:
long int_value;
case DOUBLE:
double double_value;
case STRING:
string string_value;
};enum ValueType {
INT = 0,
DOUBLE = 1,
STRING = 2,
}
type Value =
| { $discriminator: ValueType.INT; int_value: number }
| { $discriminator: ValueType.DOUBLE; double_value: number }
| { $discriminator: ValueType.STRING; string_value: string };Multiple labels per case become union types in the discriminator:
union Multi switch (long) {
case 1:
case 2:
case 3:
long small;
default:
boolean fallback;
};type Multi =
| { $discriminator: 1 | 2 | 3; small: number }
| { $discriminator: number; fallback: boolean };Exceptions become classes extending Error. Unlike structs, exceptions need to be
classes because they require throw/catch semantics and the Error prototype chain.
exception InvalidArgument {
string message;
long error_code;
};class InvalidArgument extends Error {
error_code: number;
constructor(message: string, error_code: number, options?: { cause?: Error }) {
super(message, options);
this.name = 'InvalidArgument';
this.error_code = error_code;
}
}IDL interfaces become TypeScript interfaces with methods.
interface Calculator {
long add(in long a, in long b);
long subtract(in long a, in long b);
};interface Calculator {
add(a: number, b: number): number;
subtract(a: number, b: number): number;
}The in direction is a normal parameter. The out and inout directions are included
in the return value rather than mutating parameters, since mutation is unidiomatic in
TypeScript.
When there's a single output (return value, out, or inout), we return it directly.
With multiple outputs, we bundle them into an object using parameter names as keys. If
there's a non-void return plus out/inout parameters, the return value uses $return as
its key.
interface Service {
void getValues(out long x, out long y);
void modify(inout long value);
long compute(in long x, inout long state, out long debug);
};interface Service {
getValues(): { x: number; y: number };
modify(value: number): number;
compute(x: number, state: number): {
$return: number;
state: number;
debug: number
};
}Valuetypes have both data members and methods. We split them into a data interface (for serialization) and a full interface (with methods).
valuetype ComplexValue {
public long x;
public long y;
long magnitude();
};interface ComplexValueData {
x: number;
y: number;
}
interface ComplexValue extends ComplexValueData {
magnitude(): number;
}typedef long Integer;
typedef sequence<long> IntList;type Integer = number;
type IntList = number[];Both become T[]. Bounds are validated at runtime.
typedef long Numbers[3];
typedef sequence<long, 100> BoundedList;type Numbers = number[];
type BoundedList = number[];Maps become Record<K, V> rather than ES6 Map because Map doesn't serialize
transparently to JSON.
TypeScript's Record<K, V> requires K to be string | number | symbol. Since JSON
object keys are always strings, non-string key types use template literal types to
represent string-encoded values:
| IDL Key Type | TypeScript Key Type |
|---|---|
string |
string |
| Integer types | `${number}` |
boolean |
`${boolean}` |
| Enum types | keyof typeof EnumType |
For enums, we use keyof typeof EnumType because DDS-JSON prefers enumerator names
as keys (e.g., "RED" rather than 0), though both are accepted. This gives the
type "RED" | "GREEN" | "BLUE" for the enum's member names.
typedef map<string, long> StringIntMap;
typedef map<long, string> IntStringMap;
typedef map<boolean, long> BoolIntMap;type StringIntMap = Record<string, number>;
type IntStringMap = Record<`${number}`, string>;
type BoolIntMap = Record<`${boolean}`, number>;For complex key types (structs, unions), the keys are serialized as strings. Since
there is no well-defined canonical string representation, these maps use string
as the key type.
Both string and wstring map to string since JavaScript strings are UTF-16
internally. Bounds are validated at runtime.
IDL modules map to ES6 module files. We use ES6 modules rather than TypeScript namespaces because they are the modern standard, support tree-shaking, and work well with bundlers.
module math {
struct Point {
long x;
long y;
};
const long ANSWER = 42;
};// math.ts
export interface Point {
x: number;
y: number;
}
export const ANSWER = 42;Nested modules become subdirectories:
module outer {
module inner {
struct Data {
long value;
};
};
};// outer/inner.ts
export interface Data {
value: number;
}
// outer/index.ts
export * as inner from './inner';const long MAX_SIZE = 1024;
const string GREETING = "Hello";export const MAX_SIZE = 1024;
export const GREETING = "Hello";The @optional annotation maps to optional property syntax. Per DDS-JSON, null
values are omitted from serialization rather than included as explicit null.
struct User {
string id;
@optional string email;
};interface User {
id: string;
email?: string;
}The IDL any type maps to unknown, not TypeScript's any. We use unknown because
it requires type checking before use, catching errors at compile time. With any,
TypeScript skips all type checking.
struct Message {
any data;
};interface Message {
data: unknown;
}Reserved names are escaped by appending an underscore (e.g., class becomes class_).
These cannot be used as identifiers anywhere.
| break | case | catch | class |
| const | continue | debugger | default |
| delete | do | else | enum |
| export | extends | false | finally |
| for | function | if | import |
| in | instanceof | new | null |
| return | super | switch | this |
| throw | true | try | typeof |
| var | void | while | with |
TypeScript modules always run in strict mode, so these are effectively reserved.
| arguments | let | public |
| eval | package | static |
| implements | private | yield |
| interface | protected |
These are technically valid as identifiers in some places, but using them causes confusion or linter errors. We escape them in generated code.
| Keyword | Risk |
|---|---|
| any | TS primitive type |
| as | Casting syntax |
| async | Async functions |
| await | Reserved in async/top-level |
| boolean | TS primitive type |
| constructor | Reserved method name |
| declare | Declaration files |
| get | Getter definition |
| infer | Conditional types |
| is | Type guards |
| keyof | Index type queries |
| module | Module declaration |
| namespace | Namespace declaration |
| never | TS primitive type |
| number | TS primitive type |
| readonly | Property modifier |
| require | CommonJS import |
| set | Setter definition |
| string | TS primitive type |
| symbol | TS primitive type |
| type | Type alias definition |
| unique | unique symbol |
| unknown | TS primitive type |