@ts-for-gir/lib 4.0.0-rc.9 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,60 +1,60 @@
1
1
  {
2
- "name": "@ts-for-gir/lib",
3
- "version": "4.0.0-rc.9",
4
- "description": "Typescript .d.ts generator from GIR for gjs",
5
- "main": "src/index.ts",
6
- "module": "src/index.ts",
7
- "type": "module",
8
- "engines": {
9
- "node": ">=18"
10
- },
11
- "exports": {
12
- ".": "./src/index.ts"
13
- },
14
- "scripts": {
15
- "check": "tsc --noEmit"
16
- },
17
- "repository": {
18
- "type": "git",
19
- "url": "git+https://github.com/gjsify/ts-for-gir.git"
20
- },
21
- "author": "Pascal Garber <pascal@mailfreun.de>",
22
- "files": [
23
- "src"
24
- ],
25
- "license": "Apache-2.0",
26
- "bugs": {
27
- "url": "https://github.com/gjsify/ts-for-gir/issues"
28
- },
29
- "homepage": "https://github.com/gjsify/ts-for-gir#readme",
30
- "keywords": [
31
- "gjs",
32
- "typescript",
33
- "generate",
34
- "gir",
35
- "gobject-introspection",
36
- "gnome",
37
- "gtk",
38
- "glib",
39
- "gobject",
40
- "dts",
41
- "type definitions"
42
- ],
43
- "devDependencies": {
44
- "@ts-for-gir/tsconfig": "^4.0.0-rc.9",
45
- "@types/ejs": "^3.1.5",
46
- "@types/lodash": "^4.17.24",
47
- "@types/node": "^25.6.0",
48
- "rimraf": "^6.1.3",
49
- "typescript": "^6.0.3"
50
- },
51
- "dependencies": {
52
- "@gi.ts/parser": "^4.0.0-rc.9",
53
- "@ts-for-gir/reporter": "^4.0.0-rc.9",
54
- "@ts-for-gir/templates": "^4.0.0-rc.9",
55
- "colorette": "^2.0.20",
56
- "ejs": "^5.0.2",
57
- "glob": "^13.0.6",
58
- "lodash": "4.18.1"
59
- }
60
- }
2
+ "name": "@ts-for-gir/lib",
3
+ "version": "4.0.1",
4
+ "description": "Typescript .d.ts generator from GIR for gjs",
5
+ "main": "src/index.ts",
6
+ "module": "src/index.ts",
7
+ "type": "module",
8
+ "engines": {
9
+ "node": ">=18"
10
+ },
11
+ "exports": {
12
+ ".": "./src/index.ts"
13
+ },
14
+ "scripts": {
15
+ "check": "tsc --noEmit"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/gjsify/ts-for-gir.git"
20
+ },
21
+ "author": "Pascal Garber <pascal@mailfreun.de>",
22
+ "files": [
23
+ "src"
24
+ ],
25
+ "license": "Apache-2.0",
26
+ "bugs": {
27
+ "url": "https://github.com/gjsify/ts-for-gir/issues"
28
+ },
29
+ "homepage": "https://github.com/gjsify/ts-for-gir#readme",
30
+ "keywords": [
31
+ "gjs",
32
+ "typescript",
33
+ "generate",
34
+ "gir",
35
+ "gobject-introspection",
36
+ "gnome",
37
+ "gtk",
38
+ "glib",
39
+ "gobject",
40
+ "dts",
41
+ "type definitions"
42
+ ],
43
+ "devDependencies": {
44
+ "@ts-for-gir/tsconfig": "^4.0.1",
45
+ "@types/ejs": "^3.1.5",
46
+ "@types/lodash": "^4.17.24",
47
+ "@types/node": "^25.6.2",
48
+ "rimraf": "^6.1.3",
49
+ "typescript": "^6.0.3"
50
+ },
51
+ "dependencies": {
52
+ "@gi.ts/parser": "^4.0.1",
53
+ "@ts-for-gir/reporter": "^4.0.1",
54
+ "@ts-for-gir/templates": "^4.0.1",
55
+ "colorette": "^2.0.20",
56
+ "ejs": "^5.0.2",
57
+ "glob": "^13.0.6",
58
+ "lodash": "4.18.1"
59
+ }
60
+ }
@@ -4,7 +4,7 @@ import {
4
4
  ArrayType,
5
5
  ClosureType,
6
6
  type Generic,
7
- NullableType,
7
+ makeNullable,
8
8
  type TypeExpression,
9
9
  TypeIdentifier,
10
10
  UnknownType,
@@ -259,7 +259,7 @@ export class IntrospectedFunction extends IntrospectedNamespaceMember {
259
259
  }
260
260
  } else {
261
261
  if (isOptional) {
262
- params.push(p.copy({ type: new NullableType(type), isOptional: false }));
262
+ params.push(p.copy({ type: makeNullable(type), isOptional: false }));
263
263
  } else {
264
264
  params.push(p);
265
265
  }
@@ -5,6 +5,7 @@ import {
5
5
  Generic,
6
6
  GenericType,
7
7
  GenerifiedTypeIdentifier,
8
+ makeNullable,
8
9
  NullableType,
9
10
  type TypeExpression,
10
11
  TypeIdentifier,
@@ -77,7 +78,7 @@ function resolveNullableProperties(cls: IntrospectedBaseClass): void {
77
78
  const getter = cls.members.find((m) => m.name === getterName && !(m instanceof IntrospectedStaticClassFunction));
78
79
 
79
80
  if (getter instanceof IntrospectedClassFunction && getter.return() instanceof NullableType) {
80
- prop.type = new NullableType(prop.type);
81
+ prop.type = makeNullable(prop.type);
81
82
  }
82
83
  }
83
84
  }
@@ -1,4 +1,4 @@
1
- import { BinaryType, BooleanType, ClosureType, PromiseType, TupleType, TypeIdentifier, VoidType } from "../gir.ts";
1
+ import { BooleanType, ClosureType, makeUnion, PromiseType, TupleType, TypeIdentifier, VoidType } from "../gir.ts";
2
2
 
3
3
  import type { GirModule } from "../gir-module.ts";
4
4
  import type { IntrospectedAlias } from "./alias.ts";
@@ -65,7 +65,7 @@ export function promisifyNamespaceFunctions(namespace: GirModule) {
65
65
  parameters: sync_parameters,
66
66
  }),
67
67
  node.copy({
68
- return_type: new BinaryType(async_return, node.return()),
68
+ return_type: makeUnion(async_return, node.return()),
69
69
  }),
70
70
  ]);
71
71
  }
@@ -1,4 +1,4 @@
1
- import { BinaryType, BooleanType, ClosureType, PromiseType, TupleType, TypeIdentifier, VoidType } from "../gir.ts";
1
+ import { BooleanType, ClosureType, makeUnion, PromiseType, TupleType, TypeIdentifier, VoidType } from "../gir.ts";
2
2
  import { IntrospectedConstructor } from "./constructor.ts";
3
3
  import type { IntrospectedClassFunction } from "./introspected-classes.ts";
4
4
  import {
@@ -42,7 +42,7 @@ function generatePromisifyOverloadedSignatures(
42
42
  // Union overload (with optional callback)
43
43
  const unionOverload = node.copy({
44
44
  parameters: [...async_parameters, sync_parameters[sync_parameters.length - 1].copy({ isOptional: true })],
45
- returnType: new BinaryType(async_return, VoidType),
45
+ returnType: makeUnion(async_return, VoidType),
46
46
  });
47
47
 
48
48
  return [promiseOverload, callbackOverload, unionOverload];
@@ -1,5 +1,5 @@
1
1
  import type { FormatGenerator } from "../generators/generator.ts";
2
- import { NullableType, type TypeExpression } from "../gir.ts";
2
+ import { makeNullable, type TypeExpression } from "../gir.ts";
3
3
  import type { GirFieldElement, GirPropertyElement } from "../index.ts";
4
4
  import type { OptionsLoad } from "../types/index.ts";
5
5
  import type { Options } from "../types/introspected.ts";
@@ -210,7 +210,7 @@ export class IntrospectedProperty extends IntrospectedBase<IntrospectedEnum | In
210
210
  property.getter = element.$.getter;
211
211
 
212
212
  if (element.$.nullable === "1" || element.$["allow-none"] === "1") {
213
- property.type = new NullableType(property.type);
213
+ property.type = makeNullable(property.type);
214
214
  }
215
215
 
216
216
  return property;
package/src/gir/signal.ts CHANGED
@@ -3,6 +3,7 @@ import { GirDirection } from "@gi.ts/parser";
3
3
  import type { FormatGenerator } from "../generators/generator.ts";
4
4
  import {
5
5
  ArrayType,
6
+ makeNullable,
6
7
  NativeType,
7
8
  NullableType,
8
9
  NumberType,
@@ -171,7 +172,7 @@ export class IntrospectedSignal extends IntrospectedClassMember<IntrospectedClas
171
172
  }
172
173
  } else {
173
174
  if (isOptional) {
174
- params.push(p.copy({ type: new NullableType(type), isOptional: false }));
175
+ params.push(p.copy({ type: makeNullable(type), isOptional: false }));
175
176
  } else {
176
177
  params.push(p);
177
178
  }
package/src/gir.ts CHANGED
@@ -348,9 +348,9 @@ export class NativeType extends TypeExpression {
348
348
  export class OrType extends TypeExpression {
349
349
  readonly types: ReadonlyArray<TypeExpression>;
350
350
 
351
- constructor(type: TypeExpression, ...types: TypeExpression[]) {
351
+ constructor(...types: TypeExpression[]) {
352
352
  super();
353
- this.types = [type, ...types];
353
+ this.types = [...types];
354
354
  }
355
355
 
356
356
  rewrap(type: TypeExpression): TypeExpression {
@@ -362,17 +362,15 @@ export class OrType extends TypeExpression {
362
362
  }
363
363
 
364
364
  resolve(namespace: IntrospectedNamespace, options: OptionsGeneration): TypeExpression {
365
- const [type, ...types] = this.types;
366
-
367
- return new OrType(type.resolve(namespace, options), ...types.map((t) => t.resolve(namespace, options)));
365
+ return makeUnion(...this.types.map((t) => t.resolve(namespace, options)));
368
366
  }
369
367
 
370
368
  print(namespace: IntrospectedNamespace, options: OptionsGeneration): string {
371
- return `(${this.types.map((t) => t.print(namespace, options)).join(" | ")})`;
369
+ return `${this.types.map((t) => t.print(namespace, options)).join(" | ")}`;
372
370
  }
373
371
 
374
372
  rootPrint(namespace: IntrospectedNamespace, options: OptionsGeneration): string {
375
- return `${this.types.map((t) => t.print(namespace, options)).join(" | ")}`;
373
+ return this.print(namespace, options);
376
374
  }
377
375
 
378
376
  equals(type: TypeExpression) {
@@ -384,6 +382,36 @@ export class OrType extends TypeExpression {
384
382
  }
385
383
  }
386
384
 
385
+ export function makeUnion(...inputTypes: TypeExpression[]) {
386
+ const types: Set<TypeExpression> = new Set();
387
+ for (const type of inputTypes) {
388
+ if (type instanceof BinaryType) {
389
+ types.add(type.a);
390
+ types.add(type.b);
391
+ } else if (type instanceof OrType && !(type instanceof TupleType)) {
392
+ for (const t of type.types) {
393
+ types.add(t);
394
+ }
395
+ } else {
396
+ types.add(type);
397
+ }
398
+ }
399
+ if (types.size === 1) {
400
+ return [...types][0];
401
+ }
402
+ if (types.size === 2) {
403
+ const typesArray = [...types];
404
+ if (typesArray[0] === NullType) {
405
+ return new NullableType(typesArray[1]);
406
+ }
407
+ if (typesArray[1] === NullType) {
408
+ return new NullableType(typesArray[0]);
409
+ }
410
+ return new BinaryType(...typesArray);
411
+ }
412
+ return new OrType(...types);
413
+ }
414
+
387
415
  export class TupleType extends OrType {
388
416
  print(namespace: IntrospectedNamespace, options: OptionsGeneration): string {
389
417
  return `[${this.types.map((t) => t.print(namespace, options)).join(", ")}]`;
@@ -624,7 +652,7 @@ export class NullableType extends BinaryType {
624
652
  }
625
653
 
626
654
  rewrap(type: TypeExpression): TypeExpression {
627
- return new NullableType(this.a.rewrap(type));
655
+ return makeNullable(this.a.rewrap(type));
628
656
  }
629
657
 
630
658
  get type() {
@@ -632,6 +660,13 @@ export class NullableType extends BinaryType {
632
660
  }
633
661
  }
634
662
 
663
+ export function makeNullable(type: TypeExpression) {
664
+ if (type instanceof NullableType) return type;
665
+ if (type === RawPointerType) return NullType;
666
+ if (type === AnyType) return AnyType;
667
+ return makeUnion(type, NullType);
668
+ }
669
+
635
670
  export class PromiseType extends TypeExpression {
636
671
  type: TypeExpression;
637
672
 
@@ -828,6 +863,8 @@ export class ArrayType extends TypeExpression {
828
863
  typeSuffix = "".padStart(2 * depth, "[]");
829
864
  }
830
865
 
866
+ if (this.type instanceof OrType && !(this.type instanceof TupleType))
867
+ return `(${this.type.print(namespace, options)})${typeSuffix}`;
831
868
  return `${this.type.print(namespace, options)}${typeSuffix}`;
832
869
  }
833
870
 
@@ -866,5 +903,8 @@ export const NullType = new NativeType("null");
866
903
  export const VoidType = new NativeType("void");
867
904
  export const UnknownType = new NativeType("unknown");
868
905
  export const AnyFunctionType = new NativeType("(...args: any[]) => any");
906
+ // Distinct from NeverType, so that we can transform it into NullType when
907
+ // marshalled from C to JS
908
+ export const RawPointerType = new NativeType("never");
869
909
 
870
910
  export type GirClassField = IntrospectedProperty | IntrospectedField;
@@ -1,6 +1,6 @@
1
1
  import type { IntrospectedNamespace } from "../gir/namespace.ts";
2
2
  import type { NSRegistry } from "../gir/registry.ts";
3
- import { NullableType, OrType, TypeIdentifier } from "../gir.ts";
3
+ import { makeUnion, NullType, TypeIdentifier } from "../gir.ts";
4
4
 
5
5
  const shellTemplate = (version: string) => ({
6
6
  namespace: "Shell",
@@ -24,8 +24,10 @@ const shellTemplate = (version: string) => ({
24
24
  if (addGlslSnippet) {
25
25
  // Create a new parameter with updated type using copy()
26
26
  const updatedParameter = addGlslSnippet.parameters[0].copy({
27
- type: new NullableType(
28
- new OrType(new TypeIdentifier("SnippetHook", "Shell"), new TypeIdentifier("SnippetHook", "Cogl")),
27
+ type: makeUnion(
28
+ new TypeIdentifier("SnippetHook", "Shell"),
29
+ new TypeIdentifier("SnippetHook", "Cogl"),
30
+ NullType,
29
31
  ),
30
32
  });
31
33
 
@@ -17,8 +17,8 @@ import {
17
17
  BigintOrNumberType,
18
18
  ClosureType,
19
19
  GenerifiedTypeIdentifier,
20
+ makeNullable,
20
21
  NativeType,
21
- NullableType,
22
22
  type TypeExpression,
23
23
  TypeIdentifier,
24
24
  VoidType,
@@ -242,11 +242,11 @@ export function getType(
242
242
  !(variableType instanceof NativeType) &&
243
243
  variableType !== BigintOrNumberType
244
244
  ) {
245
- return new NullableType(variableType);
245
+ return makeNullable(variableType);
246
246
  }
247
247
 
248
248
  if ((!parameter.$?.direction || parameter.$.direction === GirDirection.In) && nullable) {
249
- return new NullableType(variableType);
249
+ return makeNullable(variableType);
250
250
  }
251
251
 
252
252
  variableType.isPointer = isPointer;
@@ -7,17 +7,21 @@ import {
7
7
  BigintOrNumberType,
8
8
  BinaryType,
9
9
  BooleanType,
10
+ GenerifiedTypeIdentifier,
11
+ makeNullable,
12
+ makeUnion,
10
13
  NativeType,
11
14
  NeverType,
12
15
  NullableType,
16
+ NullType,
13
17
  NumberType,
14
18
  ObjectType,
15
- OrType,
16
19
  PromiseType,
20
+ RawPointerType,
17
21
  StringType,
18
22
  ThisType,
19
23
  TupleType,
20
- type TypeExpression,
24
+ TypeExpression,
21
25
  TypeIdentifier,
22
26
  Uint8ArrayType,
23
27
  UnknownType,
@@ -188,8 +192,13 @@ export function resolvePrimitiveType(name: string): TypeExpression | null {
188
192
  return BigintOrNumberType;
189
193
  case "gboolean":
190
194
  return BooleanType;
191
- case "gpointer": // This is typically used in callbacks to pass data, so we'll allow anything.
192
- return AnyType;
195
+ case "gpointer":
196
+ // You can't use pointers. Pointer arguments are mostly not exposed
197
+ // in GJS, but any exposed pointer arguments are always marshalled
198
+ // as null pointers. If the argument is nullable, this will combine
199
+ // with `null` to produce `null`, but if the argument isn't nullable
200
+ // it's impossible to pass a valid parameter to the function.
201
+ return RawPointerType;
193
202
  case "object": // Support TS "object"
194
203
  return ObjectType;
195
204
  case "va_list":
@@ -224,52 +233,193 @@ export function resolveDirectedType(type: TypeExpression, direction: GirDirectio
224
233
  type.type.equals(Uint8ArrayType) &&
225
234
  type.arrayDepth === 0
226
235
  ) {
227
- return new BinaryType(type, StringType);
236
+ return makeUnion(type, StringType);
228
237
  } else {
229
238
  // Rewrap arrays if they have directional types
230
239
  return type.rewrap(resolveDirectedType(type.type, direction) ?? type.type);
231
240
  }
232
241
  } else if (type instanceof TypeIdentifier) {
233
242
  if ((direction === GirDirection.In || direction === GirDirection.Inout) && type.is("GLib", "Bytes")) {
234
- return new BinaryType(type, Uint8ArrayType);
243
+ return makeUnion(type, Uint8ArrayType);
235
244
  } else if (type.is("GObject", "Value")) {
236
245
  if (direction === GirDirection.In || direction === GirDirection.Inout) {
237
- return new BinaryType(type, AnyType);
246
+ return makeUnion(type, AnyType);
238
247
  } else {
239
248
  // GJS converts GObject.Value out parameters to their unboxed type, which we don't know,
240
249
  // so type as `unknown`
241
250
  return UnknownType;
242
251
  }
243
252
  } else if (type.is("GLib", "HashTable")) {
244
- if (direction === GirDirection.In) {
245
- // Intentional `any`GLib.HashTable maps to a JS dict with dynamic values in generated output
246
- return new BinaryType(new NativeType("{ [key: string]: any }"), type);
247
- } else {
248
- return type;
249
- }
253
+ // GJS marshalls `GHashTable<K, V>` to and from a plain JS object
254
+ // in both directions only string-typed keys (`utf8`, `filename`)
255
+ // and integer-typed keys (`bool`, signed/unsigned 8/16/32-bit
256
+ // ints) are supported. `gunichar` accepts either string or
257
+ // integer. Anything else throws during marshalling — see
258
+ // https://gitlab.gnome.org/GNOME/gjs/-/blob/main/gi/arg.cpp#L316-420
259
+ // and the discussion at
260
+ // https://github.com/gjsify/ts-for-gir/issues/392.
261
+ //
262
+ // The TS type `GLib.HashTable<K, V>` does not represent any value
263
+ // that user code can actually hold — there's no GHashTable
264
+ // constructor exposed to JS. So in EVERY direction we emit the
265
+ // concrete JS shape (`{ [key: string]: V }` for string-keyed,
266
+ // `{ [key: number]: V }` for integer-keyed) and fall back to
267
+ // `never` when the declared key type can't be marshalled —
268
+ // statically encoding that the function can't be called rather
269
+ // than silently lying with a HashTable type that has no runtime
270
+ // instances.
271
+ return hashTableToJsDict(type);
250
272
  }
251
273
  } else if (type === BigintOrNumberType && direction === GirDirection.Out) {
252
274
  // 64-bit integers accept number or bigint, but only return number to JS
253
275
  return NumberType;
276
+ } else if (type === RawPointerType && direction === GirDirection.Out) {
277
+ // Raw pointers are always marshalled as JS null.
278
+ return NullType;
254
279
  } else if (type instanceof PromiseType) {
255
280
  // Propagate direction into the Promise's inner type so e.g. async
256
281
  // functions returning 64-bit ints resolve to `Promise<number>` rather
257
282
  // than `Promise<bigint | number>`.
258
283
  const resolvedInner = resolveDirectedType(type.type, direction);
259
284
  if (resolvedInner) return new PromiseType(resolvedInner);
260
- } else if (type instanceof BinaryType && !(type instanceof NullableType)) {
285
+ } else if (type instanceof NullableType) {
286
+ // Walk into the wrapped type and rebuild as NullableType so e.g.
287
+ // `GLib.HashTable<string, string> | null` becomes
288
+ // `{ [key: string]: string } | null` (without the rebuild, the
289
+ // outer NullableType-aware BinaryType branch below would skip
290
+ // this case to preserve NullableType identity, leaving the inner
291
+ // type unrewritten).
292
+ const inner = resolveDirectedType(type.a, direction);
293
+ if (inner !== null) return makeNullable(inner);
294
+ } else if (type instanceof TupleType) {
295
+ // Walk into each tuple element so a `[HashTable<string, V>, …]`
296
+ // (typical async [result, out-params, …] shape) gets each element
297
+ // rewritten. BinaryType's branch below would also match TupleType
298
+ // since it extends OrType → BinaryType is one of its supertypes,
299
+ // but the rebuild via `makeUnion` would collapse tuple semantics
300
+ // into a union — preserve the tuple by rebuilding the same class.
301
+ let changed = false;
302
+ const inner = type.types.map((t) => {
303
+ const resolved = resolveDirectedType(t, direction);
304
+ if (resolved !== null) {
305
+ changed = true;
306
+ return resolved;
307
+ }
308
+ return t;
309
+ });
310
+ if (changed) {
311
+ const [first, ...rest] = inner;
312
+ return new TupleType(first, ...rest);
313
+ }
314
+ } else if (type instanceof BinaryType) {
261
315
  // Walk through binary unions like `Promise<T> | void` (the dual-call
262
316
  // async overload) so the inner types still get direction propagation.
263
- // NullableType is skipped to preserve its subclass behaviour.
264
317
  const a = resolveDirectedType(type.a, direction) ?? type.a;
265
318
  const b = resolveDirectedType(type.b, direction) ?? type.b;
266
- if (a !== type.a || b !== type.b) return new BinaryType(a, b);
267
- } else if (type instanceof OrType && !(type instanceof BinaryType || type instanceof TupleType)) {
268
- // flatten "bigint | number" out of another OR-type
269
- const types = type.types.map((t) => resolveDirectedType(t, direction) ?? t);
270
- if (types.length === 1) return types[0];
271
- return new OrType(types[0], ...types.slice(1));
319
+ if (a !== type.a || b !== type.b) return makeUnion(a, b);
320
+ }
321
+
322
+ return null;
323
+ }
324
+
325
+ /**
326
+ * Map a `GLib.HashTable<K, V>` reference to the concrete JS object shape
327
+ * GJS marshals it to/from. Direction-independent — the marshalling is
328
+ * symmetric (a method that accepts a HashTable and a method that returns
329
+ * one both see a plain object on the JS side).
330
+ *
331
+ * Key-type rules (per gi/arg.cpp's `gjs_value_from_g_hash` /
332
+ * `gjs_value_to_g_hash`):
333
+ *
334
+ * string-shaped (`utf8`, `filename`) → `{ [key: string]: V }`
335
+ * integer-shaped (`gboolean`, `gint8`…`guint32`) → `{ [key: number]: V }`
336
+ * `gunichar` → `{ [key: string]: V }`
337
+ * (also accepts numbers,
338
+ * but the broader string
339
+ * case is more useful)
340
+ * anything else (raw pointers, classes, records, …) → `never`
341
+ * (uncallable on the JS side)
342
+ *
343
+ * The fallback for "type information missing" (e.g. an introspection
344
+ * record with bare `HashTable` and no generics) is `{ [key: string]: any }`
345
+ * — the most common shape, matching the historical generator output.
346
+ */
347
+ function hashTableToJsDict(type: TypeIdentifier): TypeExpression {
348
+ if (!(type instanceof GenerifiedTypeIdentifier) || type.generics.length === 0) {
349
+ // Bare `HashTable` with no generics — emit the catch-all dict shape.
350
+ return new NativeType("{ [key: string]: any }");
351
+ }
352
+
353
+ const [keyType, valueType] = type.generics;
354
+ const keyShape = jsKeyShapeFor(keyType);
355
+ if (keyShape === null) {
356
+ // Unsupported key type — function is uncallable from JS.
357
+ return NeverType;
272
358
  }
273
359
 
360
+ // Return a TypeExpression subclass that defers printing of V until a
361
+ // real namespace is available (which is the case both at type-resolve
362
+ // time and at template-emit time). A plain `NativeType((options) =>
363
+ // …)` would not work — its callback receives only `options`, not the
364
+ // namespace V's own print needs.
365
+ return new HashTableDictType(keyShape, valueType ?? AnyType);
366
+ }
367
+
368
+ /**
369
+ * Decide which TypeScript index-signature key type a GHashTable key maps to.
370
+ * Returns `null` for unsupported key types — the caller emits `never` so the
371
+ * containing method/property surfaces as uncallable rather than lying with a
372
+ * synthetic key type that doesn't match runtime marshalling.
373
+ */
374
+ function jsKeyShapeFor(keyType: TypeExpression): "string" | "number" | null {
375
+ if (keyType === StringType) return "string";
376
+ if (keyType instanceof TypeIdentifier && keyType.is("GLib", "filename")) return "string";
377
+ // Integer-shaped keys: signed/unsigned 8/16/32-bit ints + gboolean
378
+ // (0/1 number) all collapse to TS `number`. 64-bit ints accept either
379
+ // number or bigint at the JS boundary; pick `number` for index keys
380
+ // since object keys are always strings/numbers in JS (no bigint keys).
381
+ if (keyType === NumberType) return "number";
382
+ if (keyType === BooleanType) return "number";
383
+ if (keyType === BigintOrNumberType) return "number";
274
384
  return null;
275
385
  }
386
+
387
+ /**
388
+ * `{ [key: K]: V }` shape for `GLib.HashTable<K, V>` — a TypeExpression
389
+ * subclass so `V.rootPrint(namespace, options)` happens at emit time with
390
+ * the right namespace, instead of being baked in at type-resolve time.
391
+ */
392
+ class HashTableDictType extends TypeExpression {
393
+ constructor(
394
+ readonly keyShape: "string" | "number",
395
+ readonly valueType: TypeExpression,
396
+ ) {
397
+ super();
398
+ }
399
+
400
+ print(namespace: IntrospectedNamespace, options: import("../types/index.ts").OptionsGeneration): string {
401
+ return `{ [key: ${this.keyShape}]: ${this.valueType.rootPrint(namespace, options)} }`;
402
+ }
403
+
404
+ resolve(_namespace: IntrospectedNamespace, _options: import("../types/index.ts").OptionsGeneration): TypeExpression {
405
+ return this;
406
+ }
407
+
408
+ equals(type: TypeExpression): boolean {
409
+ return (
410
+ type instanceof HashTableDictType && this.keyShape === type.keyShape && this.valueType.equals(type.valueType)
411
+ );
412
+ }
413
+
414
+ rewrap(_type: TypeExpression): TypeExpression {
415
+ return this;
416
+ }
417
+
418
+ unwrap(): TypeExpression {
419
+ return this;
420
+ }
421
+
422
+ deepUnwrap(): TypeExpression {
423
+ return this.valueType.deepUnwrap();
424
+ }
425
+ }