Repository: software-mansion/TypeGPU
Pull request #1427 (2025-06-29 14:54:30)

fix: createBindGroup with wide layout type should accept wide resource types


Patch:
diff --git a/packages/typegpu/src/core/root/rootTypes.ts b/packages/typegpu/src/core/root/rootTypes.ts
index 9fc2c33d..c99ff634 100644
--- a/packages/typegpu/src/core/root/rootTypes.ts
+++ b/packages/typegpu/src/core/root/rootTypes.ts
@@ -10,7 +10,7 @@ import type {
   Prettify,
 } from '../../shared/utilityTypes.ts';
 import type {
-  LayoutEntryToInput,
+  ExtractBindGroupInputFromLayout,
   TgpuBindGroup,
   TgpuBindGroupLayout,
   TgpuLayoutEntry,
@@ -523,9 +523,7 @@ export interface TgpuRoot extends Unwrapper {
     >,
   >(
     layout: TgpuBindGroupLayout<Entries>,
-    entries: {
-      [K in keyof OmitProps<Entries, null>]: LayoutEntryToInput<Entries[K]>;
-    },
+    entries: ExtractBindGroupInputFromLayout<Entries>,
   ): TgpuBindGroup<Entries>;
 
   /**
diff --git a/packages/typegpu/src/data/wgslTypes.ts b/packages/typegpu/src/data/wgslTypes.ts
index 6190a2cd..e874c7ed 100644
--- a/packages/typegpu/src/data/wgslTypes.ts
+++ b/packages/typegpu/src/data/wgslTypes.ts
@@ -1380,7 +1380,8 @@ export type AnyWgslData =
   | AnyWgslStruct
   | WgslArray
   | Ptr
-  | Atomic
+  | Atomic<U32>
+  | Atomic<I32>
   | Decorated
   | AbstractInt
   | AbstractFloat
diff --git a/packages/typegpu/src/shared/utilityTypes.ts b/packages/typegpu/src/shared/utilityTypes.ts
index 0e94626b..164c6bb2 100644
--- a/packages/typegpu/src/shared/utilityTypes.ts
+++ b/packages/typegpu/src/shared/utilityTypes.ts
@@ -23,6 +23,26 @@ export type OmitProps<T extends Record<string, unknown>, Prop> = Pick<
   }[keyof T]
 >;
 
+/**
+ * Removes properties from record type that equal `Prop`
+ */
+export type OmitPropsExact<T extends Record<string, unknown>, Prop> = Pick<
+  T,
+  {
+    [Key in keyof T]: [T[Key], Prop] extends [Prop, T[Key]] ? never : Key;
+  }[keyof T]
+>;
+
+export type NullableToOptional<T> =
+  & {
+    // Props where the value extends `null` -> make them optional and remove null from the type
+    [K in keyof T as T[K] extends null ? K : never]?: T[K];
+  }
+  & {
+    // All other props remain unchanged
+    [K in keyof T as T[K] extends null ? never : K]: T[K];
+  };
+
 /**
  * The opposite of Readonly<T>
  */
diff --git a/packages/typegpu/src/tgpuBindGroupLayout.ts b/packages/typegpu/src/tgpuBindGroupLayout.ts
index 830a3cb3..7185632f 100644
--- a/packages/typegpu/src/tgpuBindGroupLayout.ts
+++ b/packages/typegpu/src/tgpuBindGroupLayout.ts
@@ -58,7 +58,11 @@ import type { TgpuNamable } from './shared/meta.ts';
 import { getName, setName } from './shared/meta.ts';
 import type { Infer, MemIdentity } from './shared/repr.ts';
 import { $internal } from './shared/symbols.ts';
-import type { Default, Prettify } from './shared/utilityTypes.ts';
+import type {
+  Default,
+  NullableToOptional,
+  Prettify,
+} from './shared/utilityTypes.ts';
 import type { TgpuShaderStage } from './types.ts';
 import type { Unwrapper } from './unwrapper.ts';
 
@@ -265,44 +269,60 @@ type GetStorageTextureRestriction<T extends TgpuLayoutStorageTexture> = Default<
   }
   : never;
 
-export type LayoutEntryToInput<T extends TgpuLayoutEntry | null> = T extends
-  TgpuLayoutUniform ?
-    | (
-      & TgpuBuffer<MemIdentity<UnwrapRuntimeConstructor<T['uniform']>>>
-      & UniformFlag
-    )
-    | GPUBuffer
-  : T extends TgpuLayoutStorage ?
-      | (
-        & TgpuBuffer<MemIdentity<UnwrapRuntimeConstructor<T['storage']>>>
-        & StorageFlag
-      )
+export type LayoutEntryToInput<T extends TgpuLayoutEntry | null> =
+  // Widest type
+  TgpuLayoutEntry | null extends T ?
+      | TgpuBuffer<AnyWgslData>
       | GPUBuffer
-  : T extends TgpuLayoutSampler ? TgpuSampler | GPUSampler
-  : T extends TgpuLayoutComparisonSampler ? TgpuComparisonSampler | GPUSampler
-  : T extends TgpuLayoutTexture ?
-      | GPUTextureView
-      | (
-        & Sampled
-        & TgpuTexture<
-          Prettify<TextureProps & GetTextureRestriction<T>>
-        >
-      )
-      | TgpuSampledTexture<
-        Default<T['viewDimension'], '2d'>,
-        ChannelFormatToSchema[T['texture']]
-      >
-  : T extends TgpuLayoutStorageTexture ?
+      | TgpuSampler
+      | GPUSampler
+      | TgpuComparisonSampler
+      | GPUSampler
+      | TgpuTexture
+      | TgpuSampledTexture
+      | TgpuMutableTexture
+      | TgpuReadonlyTexture
+      | TgpuWriteonlyTexture
       | GPUTextureView
-      | (
-        & StorageFlag
-        & TgpuTexture<
-          Prettify<TextureProps & GetStorageTextureRestriction<T>>
+      | GPUExternalTexture
+    // Strict type-checking
+    : T extends TgpuLayoutUniform ?
+        | (
+          & TgpuBuffer<MemIdentity<UnwrapRuntimeConstructor<T['uniform']>>>
+          & UniformFlag
+        )
+        | GPUBuffer
+    : T extends TgpuLayoutStorage ?
+        | (
+          & TgpuBuffer<MemIdentity<UnwrapRuntimeConstructor<T['storage']>>>
+          & StorageFlag
+        )
+        | GPUBuffer
+    : T extends TgpuLayoutSampler ? TgpuSampler | GPUSampler
+    : T extends TgpuLayoutComparisonSampler ? TgpuComparisonSampler | GPUSampler
+    : T extends TgpuLayoutTexture ?
+        | GPUTextureView
+        | (
+          & Sampled
+          & TgpuTexture<
+            Prettify<TextureProps & GetTextureRestriction<T>>
+          >
+        )
+        | TgpuSampledTexture<
+          Default<T['viewDimension'], '2d'>,
+          ChannelFormatToSchema[T['texture']]
         >
-      )
-      | StorageTextureUsageForEntry<T>
-  : T extends TgpuLayoutExternalTexture ? GPUExternalTexture
-  : never;
+    : T extends TgpuLayoutStorageTexture ?
+        | GPUTextureView
+        | (
+          & StorageFlag
+          & TgpuTexture<
+            Prettify<TextureProps & GetStorageTextureRestriction<T>>
+          >
+        )
+        | StorageTextureUsageForEntry<T>
+    : T extends TgpuLayoutExternalTexture ? GPUExternalTexture
+    : never;
 
 export type BindLayoutEntry<T extends TgpuLayoutEntry | null> = T extends
   TgpuLayoutUniform ? TgpuBufferUniform<T['uniform']>
@@ -330,9 +350,9 @@ export type InferLayoutEntry<T extends TgpuLayoutEntry | null> = T extends
 
 export type ExtractBindGroupInputFromLayout<
   T extends Record<string, TgpuLayoutEntry | null>,
-> = {
-  [K in keyof T]: LayoutEntryToInput<T[K]>;
-};
+> = NullableToOptional<
+  { [K in keyof T]: LayoutEntryToInput<T[K]> }
+>;
 
 export type TgpuBindGroup<
   Entries extends Record<string, TgpuLayoutEntry | null> = Record<
@@ -617,9 +637,7 @@ export class TgpuBindGroupImpl<
 
   constructor(
     public readonly layout: TgpuBindGroupLayout<Entries>,
-    public readonly entries: {
-      [K in keyof Entries]: LayoutEntryToInput<Entries[K]>;
-    },
+    public readonly entries: ExtractBindGroupInputFromLayout<Entries>,
   ) {
     // Checking if all entries are present.
     for (const key of Object.keys(layout.entries)) {
@@ -639,7 +657,7 @@ export class TgpuBindGroupImpl<
             return null;
           }
 
-          const value = this.entries[key];
+          const value = this.entries[key as keyof typeof this.entries];
 
           if (value === undefined) {
             throw new Error(


Test patch:
diff --git a/packages/typegpu/tests/bindGroupLayout.test.ts b/packages/typegpu/tests/bindGroupLayout.test.ts
index 11cebc5a..2f278d5f 100644
--- a/packages/typegpu/tests/bindGroupLayout.test.ts
+++ b/packages/typegpu/tests/bindGroupLayout.test.ts
@@ -1,33 +1,20 @@
 import { beforeEach, describe, expect, expectTypeOf } from 'vitest';
 import { comparisonSampler, sampler } from '../src/core/sampler/sampler.ts';
-import {
-  arrayOf,
-  atomic,
-  type F32,
-  f32,
-  type I32,
-  i32,
-  struct,
-  type U32,
-  u32,
-  type Vec3f,
-  vec3f,
-  type Vec4f,
-  type Vec4i,
-  type WgslArray,
-  type WgslStruct,
-} from '../src/data/index.ts';
+import * as d from '../src/data/index.ts';
 import tgpu, {
   type TgpuBindGroupLayout,
+  type TgpuBuffer,
   type TgpuBufferMutable,
   type TgpuBufferReadonly,
   type TgpuBufferUniform,
   type TgpuMutableTexture,
   type TgpuSampledTexture,
   type TgpuWriteonlyTexture,
+  type UniformFlag,
 } from '../src/index.ts';
 import { getName } from '../src/shared/meta.ts';
 import {
+  type ExtractBindGroupInputFromLayout,
   MissingBindingError,
   type TgpuBindGroup,
   type TgpuLayoutComparisonSampler,
@@ -44,8 +31,8 @@ const DEFAULT_READONLY_VISIBILITY_FLAGS = GPUShaderStage.COMPUTE |
 describe('TgpuBindGroupLayout', () => {
   it('names bound elements', () => {
     const layout = tgpu.bindGroupLayout({
-      uniBuffer: { uniform: vec3f },
-      stoBuffer: { storage: vec3f },
+      uniBuffer: { uniform: d.vec3f },
+      stoBuffer: { storage: d.vec3f },
       defTexture: { texture: 'depth' },
       stoTexture: { storageTexture: 'bgra8unorm' },
       extTexture: { externalTexture: {} },
@@ -64,69 +51,69 @@ describe('TgpuBindGroupLayout', () => {
 
   it('infers the bound type of a uniform entry', () => {
     const layout = tgpu.bindGroupLayout({
-      position: { uniform: vec3f },
+      position: { uniform: d.vec3f },
     });
 
     const { position } = layout.bound;
 
-    expectTypeOf(position).toEqualTypeOf<TgpuBufferUniform<Vec3f>>();
+    expectTypeOf(position).toEqualTypeOf<TgpuBufferUniform<d.Vec3f>>();
   });
 
   it('infers the bound type of a readonly storage entry', () => {
     const layout = tgpu.bindGroupLayout({
-      a: { storage: vec3f },
-      b: { storage: vec3f, access: 'readonly' },
+      a: { storage: d.vec3f },
+      b: { storage: d.vec3f, access: 'readonly' },
     });
 
     const { a, b } = layout.bound;
 
-    expectTypeOf(a).toEqualTypeOf<TgpuBufferReadonly<Vec3f>>();
-    expectTypeOf(b).toEqualTypeOf<TgpuBufferReadonly<Vec3f>>();
+    expectTypeOf(a).toEqualTypeOf<TgpuBufferReadonly<d.Vec3f>>();
+    expectTypeOf(b).toEqualTypeOf<TgpuBufferReadonly<d.Vec3f>>();
   });
 
   it('infers the bound type of a mutable storage entry', () => {
     const layout = tgpu.bindGroupLayout({
-      a: { storage: vec3f, access: 'mutable' },
+      a: { storage: d.vec3f, access: 'mutable' },
     });
 
     const { a } = layout.bound;
 
-    expectTypeOf(a).toEqualTypeOf<TgpuBufferMutable<Vec3f>>();
+    expectTypeOf(a).toEqualTypeOf<TgpuBufferMutable<d.Vec3f>>();
   });
 
   it('works for entries passed as functions returning TgpuData', ({ root }) => {
     const layout = tgpu.bindGroupLayout({
       a: {
-        storage: (arrayLength: number) => arrayOf(u32, arrayLength),
+        storage: (arrayLength: number) => d.arrayOf(d.u32, arrayLength),
         access: 'mutable',
       },
-      b: { storage: (arrayLength: number) => arrayOf(vec3f, arrayLength) },
+      b: { storage: (arrayLength: number) => d.arrayOf(d.vec3f, arrayLength) },
     });
 
     tgpu.bindGroupLayout({
       // @ts-expect-error
-      c: { uniform: (arrayLength: number) => arrayOf(vec3f, arrayLength) },
+      c: { uniform: (arrayLength: number) => d.arrayOf(d.vec3f, arrayLength) },
     });
 
     expectTypeOf(layout).toEqualTypeOf<
       TgpuBindGroupLayout<{
         a: {
-          storage: (_: number) => WgslArray<U32>;
+          storage: (_: number) => d.WgslArray<d.U32>;
           access: 'mutable';
         };
         b: {
-          storage: (_: number) => WgslArray<Vec3f>;
+          storage: (_: number) => d.WgslArray<d.Vec3f>;
         };
       }>
     >();
 
     const { a, b } = layout.bound;
 
-    expectTypeOf(a).toEqualTypeOf<TgpuBufferMutable<WgslArray<U32>>>();
-    expectTypeOf(b).toEqualTypeOf<TgpuBufferReadonly<WgslArray<Vec3f>>>();
+    expectTypeOf(a).toEqualTypeOf<TgpuBufferMutable<d.WgslArray<d.U32>>>();
+    expectTypeOf(b).toEqualTypeOf<TgpuBufferReadonly<d.WgslArray<d.Vec3f>>>();
 
-    const aBuffer = root.createBuffer(arrayOf(u32, 4)).$usage('storage');
-    const bBuffer = root.createBuffer(arrayOf(vec3f, 4)).$usage('storage');
+    const aBuffer = root.createBuffer(d.arrayOf(d.u32, 4)).$usage('storage');
+    const bBuffer = root.createBuffer(d.arrayOf(d.vec3f, 4)).$usage('storage');
 
     const bindGroup = root.createBindGroup(layout, {
       a: aBuffer,
@@ -136,11 +123,11 @@ describe('TgpuBindGroupLayout', () => {
     expectTypeOf(bindGroup).toEqualTypeOf<
       TgpuBindGroup<{
         a: {
-          storage: (_: number) => WgslArray<U32>;
+          storage: (_: number) => d.WgslArray<d.U32>;
           access: 'mutable';
         };
         b: {
-          storage: (_: number) => WgslArray<Vec3f>;
+          storage: (_: number) => d.WgslArray<d.Vec3f>;
         };
       }>
     >();
@@ -190,16 +177,16 @@ describe('TgpuBindGroupLayout', () => {
   it('omits null properties', ({ root }) => {
     const layout = tgpu
       .bindGroupLayout({
-        a: { uniform: vec3f }, // binding 0
+        a: { uniform: d.vec3f }, // binding 0
         _0: null, // binding 1
-        c: { storage: vec3f }, // binding 2
+        c: { storage: d.vec3f }, // binding 2
       });
 
     expectTypeOf(layout).toEqualTypeOf<
       TgpuBindGroupLayout<{
-        a: { uniform: Vec3f };
+        a: { uniform: d.Vec3f };
         _0: null;
-        c: { storage: Vec3f };
+        c: { storage: d.Vec3f };
       }>
     >();
 
@@ -251,18 +238,18 @@ describe('TgpuBindGroupLayout', () => {
 
 describe('TgpuBindGroup', () => {
   describe('buffer layout', () => {
-    let layout: TgpuBindGroupLayout<{ foo: { uniform: Vec3f } }>;
+    let layout: TgpuBindGroupLayout<{ foo: { uniform: d.Vec3f } }>;
 
     beforeEach(() => {
       layout = tgpu
         .bindGroupLayout({
-          foo: { uniform: vec3f },
+          foo: { uniform: d.vec3f },
         })
         .$name('example');
     });
 
     it('populates a simple layout with a raw buffer', ({ root }) => {
-      const buffer = root.createBuffer(vec3f).$usage('uniform');
+      const buffer = root.createBuffer(d.vec3f).$usage('uniform');
       const bindGroup = root.createBindGroup(layout, {
         foo: root.unwrap(buffer),
       });
@@ -286,7 +273,7 @@ describe('TgpuBindGroup', () => {
     });
 
     it('populates a simple layout with a typed buffer', ({ root }) => {
-      const buffer = root.createBuffer(vec3f).$usage('uniform');
+      const buffer = root.createBuffer(d.vec3f).$usage('uniform');
       const bindGroup = root.createBindGroup(layout, { foo: buffer });
 
       expect(root.device.createBindGroupLayout).not.toBeCalled();
@@ -310,31 +297,32 @@ describe('TgpuBindGroup', () => {
 
   describe('simple layout with atomics', () => {
     let layout: TgpuBindGroupLayout<{
-      foo: { uniform: U32 };
-      bar: { uniform: WgslArray<I32> };
-      baz: { storage: WgslStruct<{ a: U32; b: I32 }> };
-      qux: { storage: (n: number) => WgslArray<I32> };
+      foo: { uniform: d.U32 };
+      bar: { uniform: d.WgslArray<d.I32> };
+      baz: { storage: d.WgslStruct<{ a: d.U32; b: d.I32 }> };
+      qux: { storage: (n: number) => d.WgslArray<d.I32> };
     }>;
-
     beforeEach(() => {
       layout = tgpu
         .bindGroupLayout({
-          foo: { uniform: u32 },
-          bar: { uniform: arrayOf(i32, 4) },
-          baz: { storage: struct({ a: u32, b: i32 }) },
-          qux: { storage: (n: number) => arrayOf(i32, n) },
+          foo: { uniform: d.u32 },
+          bar: { uniform: d.arrayOf(d.i32, 4) },
+          baz: { storage: d.struct({ a: d.u32, b: d.i32 }) },
+          qux: { storage: (n: number) => d.arrayOf(d.i32, n) },
         })
         .$name('example');
     });
 
     it('populates a simple layout with a raw buffer', ({ root }) => {
-      const scalarBuffer = root.createBuffer(u32).$usage('uniform');
-      const arrayBuffer = root.createBuffer(arrayOf(i32, 4)).$usage('storage');
+      const scalarBuffer = root.createBuffer(d.u32).$usage('uniform');
+      const arrayBuffer = root.createBuffer(d.arrayOf(d.i32, 4)).$usage(
+        'storage',
+      );
       const structBuffer = root
-        .createBuffer(struct({ a: u32, b: i32 }))
+        .createBuffer(d.struct({ a: d.u32, b: d.i32 }))
         .$usage('uniform');
       const runtimeArrayBuffer = root
-        .createBuffer(arrayOf(i32, 4))
+        .createBuffer(d.arrayOf(d.i32, 4))
         .$usage('storage');
       const bindGroup = root.createBindGroup(layout, {
         foo: root.unwrap(scalarBuffer),
@@ -380,17 +368,17 @@ describe('TgpuBindGroup', () => {
     });
 
     it('populates a simple layout with an atomic version of a primitive buffer', ({ root }) => {
-      const atomicScalarBuffer = root.createBuffer(atomic(u32)).$usage(
+      const atomicScalarBuffer = root.createBuffer(d.atomic(d.u32)).$usage(
         'uniform',
       );
       const atomicArrayBuffer = root
-        .createBuffer(arrayOf(atomic(i32), 4))
+        .createBuffer(d.arrayOf(d.atomic(d.i32), 4))
         .$usage('uniform');
       const atomicStructBuffer = root
-        .createBuffer(struct({ a: atomic(u32), b: atomic(i32) }))
+        .createBuffer(d.struct({ a: d.atomic(d.u32), b: d.atomic(d.i32) }))
         .$usage('storage');
       const atomicRuntimeArrayBuffer = root
-        .createBuffer(arrayOf(atomic(i32), 4))
+        .createBuffer(d.arrayOf(d.atomic(d.i32), 4))
         .$usage('storage');
       const bindGroup = root.createBindGroup(layout, {
         foo: atomicScalarBuffer,
@@ -766,8 +754,10 @@ describe('TgpuBindGroup', () => {
 
       const { foo, bar } = layout.bound;
 
-      expectTypeOf(foo).toEqualTypeOf<TgpuSampledTexture<'2d', F32>>();
-      expectTypeOf(bar).toEqualTypeOf<TgpuSampledTexture<'cube-array', F32>>();
+      expectTypeOf(foo).toEqualTypeOf<TgpuSampledTexture<'2d', d.F32>>();
+      expectTypeOf(bar).toEqualTypeOf<
+        TgpuSampledTexture<'cube-array', d.F32>
+      >();
     });
   });
 
@@ -973,8 +963,8 @@ describe('TgpuBindGroup', () => {
 
       const { foo, bar } = layout.bound;
 
-      expectTypeOf(foo).toEqualTypeOf<TgpuMutableTexture<'3d', Vec4f>>();
-      expectTypeOf(bar).toEqualTypeOf<TgpuWriteonlyTexture<'2d', Vec4i>>();
+      expectTypeOf(foo).toEqualTypeOf<TgpuMutableTexture<'3d', d.Vec4f>>();
+      expectTypeOf(bar).toEqualTypeOf<TgpuWriteonlyTexture<'2d', d.Vec4i>>();
     });
   });
 
@@ -1019,26 +1009,26 @@ describe('TgpuBindGroup', () => {
 
   describe('multiple-entry layout', () => {
     let layout: TgpuBindGroupLayout<{
-      a: { uniform: Vec3f };
-      b: { storage: U32; access: 'mutable' };
+      a: { uniform: d.Vec3f };
+      b: { storage: d.U32; access: 'mutable' };
       _: null;
-      d: { storage: F32; access: 'readonly' };
+      d: { storage: d.F32; access: 'readonly' };
     }>;
 
     beforeEach(() => {
       layout = tgpu
         .bindGroupLayout({
-          a: { uniform: vec3f },
-          b: { storage: u32, access: 'mutable' },
+          a: { uniform: d.vec3f },
+          b: { storage: d.u32, access: 'mutable' },
           _: null,
-          d: { storage: f32, access: 'readonly' },
+          d: { storage: d.f32, access: 'readonly' },
         })
         .$name('example');
     });
 
     it('requires all non-null entries to be populated', ({ root }) => {
-      const aBuffer = root.createBuffer(vec3f).$usage('uniform');
-      const bBuffer = root.createBuffer(u32).$usage('storage');
+      const aBuffer = root.createBuffer(d.vec3f).$usage('uniform');
+      const bBuffer = root.createBuffer(d.u32).$usage('storage');
 
       expect(() => {
         // @ts-expect-error
@@ -1050,9 +1040,9 @@ describe('TgpuBindGroup', () => {
     });
 
     it('creates bind group in layout-defined order, not the insertion order of the populate parameter', ({ root }) => {
-      const aBuffer = root.createBuffer(vec3f).$usage('uniform');
-      const bBuffer = root.createBuffer(u32).$usage('storage');
-      const dBuffer = root.createBuffer(f32).$usage('storage');
+      const aBuffer = root.createBuffer(d.vec3f).$usage('uniform');
+      const bBuffer = root.createBuffer(d.u32).$usage('storage');
+      const dBuffer = root.createBuffer(d.f32).$usage('storage');
 
       const bindGroup = root.createBindGroup(layout, {
         // purposefully out of order
@@ -1090,24 +1080,74 @@ describe('TgpuBindGroup', () => {
       });
     });
   });
+
+  describe('wide type', () => {
+    it('accepts wide buffer', ({ root }) => {
+      const layout = tgpu.bindGroupLayout({}) as TgpuBindGroupLayout;
+      const buffer = root.createBuffer(d.f32) as TgpuBuffer<d.AnyWgslData>;
+
+      root.createBindGroup(layout, {
+        foo: buffer,
+      });
+    });
+
+    it('accepts wide uniform buffer', ({ root }) => {
+      const layout = tgpu.bindGroupLayout({}) as TgpuBindGroupLayout;
+
+      const buffer = root.createBuffer(d.f32).$usage('uniform') as
+        & TgpuBuffer<d.AnyWgslData>
+        & UniformFlag;
+
+      root.createBindGroup(layout, {
+        foo: buffer,
+      });
+    });
+
+    it('allows to write generic function implementations', ({ root }) => {
+      function fooLib() {
+        const fooBuffer = root.createBuffer(d.f32).$usage('uniform');
+
+        function createGroupWithFoo<T extends { foo: { uniform: d.F32 } }>(
+          layout: TgpuBindGroupLayout<T>,
+          rest: Omit<ExtractBindGroupInputFromLayout<T>, 'foo'>,
+        ) {
+          return root.createBindGroup(
+            layout,
+            { ...rest, foo: fooBuffer } as ExtractBindGroupInputFromLayout<T>,
+          );
+        }
+
+        return {
+          createGroupWithFoo,
+        };
+      }
+
+      const { createGroupWithFoo } = fooLib();
+
+      const layout = tgpu.bindGroupLayout({
+        custom: { storage: d.u32, access: 'readonly' },
+        foo: { uniform: d.f32 }, // required by the library
+      });
+
+      const customBuffer = root.createBuffer(d.u32).$usage('storage');
+
+      // the library fulfills the `foo` resource
+      const group = createGroupWithFoo(layout, { custom: customBuffer });
+    });
+  });
 });
 
 describe('UnwrapRuntimeConstructor', () => {
   it('unwraps return types of functions returning TgpuData', () => {
-    expectTypeOf<UnwrapRuntimeConstructor<U32>>().toEqualTypeOf<U32>();
-    expectTypeOf<UnwrapRuntimeConstructor<WgslArray<Vec3f>>>().toEqualTypeOf<
-      WgslArray<Vec3f>
-    >();
-
-    expectTypeOf<
-      UnwrapRuntimeConstructor<(_: number) => U32>
-    >().toEqualTypeOf<U32>();
+    expectTypeOf<UnwrapRuntimeConstructor<d.U32>>().toEqualTypeOf<d.U32>();
+    expectTypeOf<UnwrapRuntimeConstructor<d.WgslArray<d.Vec3f>>>()
+      .toEqualTypeOf<d.WgslArray<d.Vec3f>>();
     expectTypeOf<
-      UnwrapRuntimeConstructor<(_: number) => WgslArray<Vec3f>>
-    >().toEqualTypeOf<WgslArray<Vec3f>>();
+      UnwrapRuntimeConstructor<(_: number) => d.WgslArray<d.Vec3f>>
+    >().toEqualTypeOf<d.WgslArray<d.Vec3f>>();
 
     expectTypeOf<
-      UnwrapRuntimeConstructor<F32 | ((_: number) => U32)>
-    >().toEqualTypeOf<F32 | U32>();
+      UnwrapRuntimeConstructor<d.F32 | ((_: number) => d.U32)>
+    >().toEqualTypeOf<d.F32 | d.U32>();
   });
 });