Penggabungan record dinamis (record concatenation) sering menjadi sumber regresi halus pada TypeScript. Masalah muncul ketika representasi level runtime berhasil dieksekusi melalui spread operator ({ ...a, ...b }), namun level compiler menghasilkan inferensi tipe yang tidak presisi, seperti never, pelebaran ke any, atau penghapusan tipe atribut secara diam-diam (silently dropped fields).

Untuk mencegah kerusakan skema pada sistem bertingkat tinggi, verifikasi type inference record harus diuji pada level type system maupun level runtime. Pendekatan berbasis contoh statis (unit test konvensional) kerap melewatkan variasi tipe tak terbatas. Solusinya adalah memanfaatkan property-based testing menggunakan fast-check bersama compiler API assertions.

Tiga Titik Kegagalan Inferensi Skema Dinamis

Saat menggabungkan dua record arbitrer A dan B, utility type naive seperti A & B memicu anomali semantik:

  • Key Collision: Jika record A memiliki key id: string dan B memiliki id: number, hasil intersection (A & B)['id'] adalah string & number yang mengecil menjadi never. Di runtime, nilai milik B menimpa A, sehingga tipe hasil inferensi tidak merefleksikan nilai aktual.
  • Type Narrowing Mismatch: Record dinamis dengan properti opsional atau union tipe kompleks sering kehilangan narrowing setelah dimerge, mengembalikan tipe kembali ke primitif umum atau menghilangkan discriminator tag.
  • Recursive Expansion: Fungsi penggabungan record bersarang (deep merge) rentan terhadap infinite depth recursion yang melebihi batas compiler stack TypeScript (error TS2589), menyebabkan bail-out ke any tanpa peringatan eksplisit pada sisi consumer library.

Implementasi Safe Merge Utility Type

Implementasi tipe harus mendefinisikan prioritas overwrite secara eksplisit menggunakan mapped types dan Omit:

// Utility type untuk shallow merge dengan prioritas pada parameter kedua (B)
export type SafeMerge<A, B> = Omit<A, keyof B> & B;

// Implementasi runtime minimal
export function safeMerge<A extends object, B extends object>(
  sourceA: A,
  sourceB: B
): SafeMerge<A, B> {
  return { ...sourceA, ...sourceB } as SafeMerge<A, B>;
}

Harness Property-Based Testing dengan Fast-Check

Gunakan fast-check untuk menghasilkan ratusan pasang record acak dengan variasi struktur, key yang tumpang tindih, dan tipe nilai yang berbeda. Verifikasi dilakukan untuk memastikan properti runtime sinkron dengan kontrak skema tanpa ada field yang terhapus secara silent.

import fc from 'fast-check';
import { safeMerge, SafeMerge } from './merger';

// Type assertion helper di tingkat compiler
type AssertEqual<T, U> = [T] extends [U] ? ([U] extends [T] ? true : false) : false;

describe('Verifikasi Type Inference Record Dinamis', () => {
  it('harus mempertahankan integritas field dan prioritas precedence key collision', () => {
    const recordGen = fc.dictionary(
      fc.string({ minLength: 1, maxLength: 8 }),
      fc.oneof(fc.string(), fc.integer(), fc.boolean())
    );

    fc.assert(
      fc.property(recordGen, recordGen, (a, b) => {
        const merged = safeMerge(a, b);
        const keysA = Object.keys(a);
        const keysB = Object.keys(b);

        // Validasi runtime: seluruh key dari B harus mempertahankan nilainya
        for (const key of keysB) {
          if (merged[key as keyof typeof merged] !== (b as any)[key]) {
            return false;
          }
        }

        // Validasi runtime: key unik dari A tidak boleh terhapus (silently dropped)
        for (const key of keysA) {
          if (!(key in b)) {
            if (merged[key as keyof typeof merged] !== (a as any)[key]) {
              return false;
            }
          }
        }

        return true;
      }),
      { numRuns: 1000 }
    );
  });
});

Validasi Soundness Menggunakan Type-Level Testing di CI

Validasi runtime di atas belum menjamin bahwa compiler memetakan tipe dengan benar. Kita harus mengintegrasikan pengujian level tipe murni menggunakan utility seperti tsd atau expect-type ke dalam pipeline integrasi berkelanjutan (CI).

import { expectTypeOf } from 'expect-type';
import { safeMerge } from './merger';

function runCompileTimeAssertions() {
  const base = { id: 'uuid-1', count: 10, meta: { active: true } };
  const patch = { id: 100, label: 'test' };

  const result = safeMerge(base, patch);

  // Verifikasi collision: id harus resolved menjadi number, bukan never atau string
  expectTypeOf(result.id).toEqualTypeOf<number>();

  // Verifikasi non-colliding fields dipertahankan
  expectTypeOf(result.count).toEqualTypeOf<number>();
  expectTypeOf(result.label).toEqualTypeOf<string>();
  expectTypeOf(result.meta).toEqualTypeOf<{ active: boolean }>();
}

Integrasi Pipeline CI

Jalankan verifikasi dua lapis ini dalam alur validasi pull request untuk menghentikan regresi skema sebelum merge ke branch utama:

# Step pipeline CI (contoh: GitHub Actions atau GitLab CI)
- name: Verify Types and Invariants
  run: |
    # 1. Typecheck murni untuk assertions kompilasi
    npx tsc --noEmit
    # 2. Property-based fuzzing untuk invariant runtime
    npm test -- --testPathPattern=merger.spec.ts

Kombinasi property-based testing untuk runtime invariance dan compile-time assertion ketat mengeliminasi celah antara apa yang dipahami compiler dan apa yang dieksekusi mesin, menjaga stabilitas skema dinamis secara terukur.