typescript - Fonksiyon argümanındaki diziden tipleri genel bir kesişim tipine çıkar


Bir fonksiyonun argümanı olarak geçirilen bir dizide bulunan nesnelerin türlerinin birleşimi olan genel bir türü çıkarmaya çalışıyorum. Bunu kodla göstermek en kolay olanıdır, bu yüzden aşağıdaki örneğe bakınız:

type Extension = {
  name: string,
  fields: {
    [key: string]: string
  }
}

type Config = {
  name: string,
  extensions: Extension[]
}

const ext1: Extension = {
  name: 'ext1',
  fields: {
    field1: 'test',
    field2: 'test'
  }
}

const ext2: Extension = {
  name: 'ext1',
  fields: {
    field3: 'test',
    field4: 'test'
  }
}

const config: Config = {
  name: 'Test',
  extensions: [ext1, ext2]
}

// Ignore the obviously wrong typings here
function create(config: Config): T { ... }

T'nin aşağıdaki tipe sahip olduğunu nasıl garanti edebilirim:

type CombinedFields = {
  field1: string,
  field2: string,
} & {
  field3: string,
  field4: string
}

Cevap #1

Bunun mümkün olabilmesi için derleyicinin ext1, ext2 ve config değişkenlerinin özel anahtar ve değerlerini takip etmesi gerekir. Ancak bu değişkenleri Extension ve Config gibi geniş türlerle açıklarsanız, takip etmeniz gereken bilgiyi kaybetmiş olursunuz. Bu yüzden açıklama yapamazsınız. Bunun yerine, değişkenlerin bu türlere genişletilmeden atanabilir olduğunu kontrol etmek için satisfies operatörünü kullanabilirsiniz:

const ext1 = {
  name: 'ext1',
  fields: {
    field1: 'test',
    field2: 'test'
  }
} satisfies Extension;

/* const ext1: {
    name: string;
    fields: {
        field1: string;
        field2: string;
    };
} */

const ext2 = {
  name: 'ext1',
  fields: {
    field3: 'test',
    field4: 'test'
  }
} satisfies Extension;

/* const ext2: {
    name: string;
    fields: {
        field3: string;
        field4: string;
    };
} */

Tamam, bu türler alan adlarını biliyor. config için biraz daha ileri gitmemiz gerekiyor; extensions öğelerinin uzunluğunu ve sırasını takip etmek önemlidir, böylece [ext1, ext2] ile [Math.random()<0.5 ? ext1 : ext2] arasında ayrım yapabilir (ya da en azından bu ikisi arasında ayrım yapmamız gerektiğini varsayıyorum). Bu, config üzerinde bir const bildirimi kullanmamız ve Config'in extensions özelliğinin bir readonly dizi türü olmasına izin vermemiz gerektiği anlamına gelir, çünkü const bildirimleri size bunu sağlar:

type Config = {
  name: string,
  extensions: readonly Extension[]
}
const config = {
  name: 'Test',
  extensions: [ext1, ext2]
} as const satisfies Config;

/* const config: {
    readonly name: "Test";
    readonly extensions: readonly [{
        name: string;
        fields: {
            field1: string;
            field2: string;
        };
    }, {
        name: string;
        fields: {
            field3: string;
            field4: string;
        };
    }];
} */

Tamam, artık ilerlemek için yeterli bilgi bu.


İşte bir yaklaşım:

type ExtensionsToIntersection<T extends readonly Extension[]> =
  { [I in keyof T]: (x: T[I]["fields"]) => void }[number] extends
  (x: infer I) => void ? I : never;

declare function create<T extends Config>(config: T):
  ExtensionsToIntersection<T["extensions"]>;

ExtensionsToIntersection<T> türü, Extension atamaya uygun öğelerden oluşan bir demeti alır ve bunların fields özelliklerinin kesişimine dönüştürür. Bu teknik, TypeScript merge generic array sorusuna verilen cevapta açıklanan yöntemle oldukça benzerdir. Genel olarak, kesiştirmek istediğimiz öğeleri kontravariante bir tür konumuna (bu durumda bir fonksiyonun parametresi) eşlemek ve ardından bunların birliği için tek bir tür çıkarmak fikridir (fonksiyonların birleşiminden parametre konumundan türetmek, koşullu türlerin sürüm notlarında açıklandığı gibi, bu parametre türlerinin kesişimini verir).

Haydi onu test edelim:

const ret = create(config);
/* const ret: {
    readonly field1: "test";
    readonly field2: "test";
} & {
    readonly field3: "test";
    readonly field4: "test";
} */

Güzel görünüyor!

Koda yönelik oyun alanı bağlantısı