typescript - Başka bir genel parametreye bağlı olarak bir genel parametreyi isteğe bağlı yapma?
Böyle bir genel tip oluşturmaya çalışıyorum:
type HTTPMethod = "HEAD" | "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
type Endpoint<M extends HTTPMethod, S extends {}> = () => void;
Fikir, bir Endpoint fonksiyonu ilan ederken bir şekil S sağlamaktır. S, giriş/çıkış türü olarak kullanılabilir.
Ancak, S öğesinin POST, PUT, PATCH için gerekli, HEAD, GET, DELETE için ise isteğe bağlı olmasını istiyorum. Şöyle bir şey:
const a: Endpoint = () => {}; // < requires 1 type argument(s).
const b: Endpoint<"HEAD"> = () => {};
const c: Endpoint<"HEAD", {}> = () => {};
const d: Endpoint<"POST"> = () => {}; // < requires 2 type argument(s).
const e: Endpoint<"POST", {}> = () => {};
Görünüşe göre extends ve = kullanmak, beklediğim gibi çalışmıyor. Bu:
type Test<T extends string = number> = () => void;
Hatalar, çünkü sayı string ile eşleşmiyor. Ancak:
type Endpoint<M extends HTTPMethod, S extends M extends "POST" | "PUT" | "PATCH"
? S extends unknown
? never
: {}
: never = unknown> = () => void;
-her zaman geçerlidir, unknown'un never ya da {} olmamasına rağmen. Yukarıdaki ile birkaç saat oynadım, ama işe yarayan bir şey bulamıyorum gibi görünüyor.
Böyle bir şey mevcut TypeScript ile mümkün mü?
Cevap #1
TypeScript'teki genel tür parametreleri varsayılan tür argümanlarına sahip olabilir, ancak eğer onları kullanırsanız tür argümanı isteğe bağlıdır ve kullanmazsanız tür argümanı zorunludur. TypeScript'te "koşullu olarak isteğe bağlı" tür parametreleri yoktur. Bu, istediğiniz benzer davranışı elde etmenin mümkün olabileceği anlamına gelse de, bunu sağlamak için tasarlanmış herhangi bir özellik yoktur.
Makul bir çözüm, değişken sayıda tür parametresi yerine tek bir demet türü parametresi kullanmaktır. Yani Endpoint<X, Y> yerine Endpoint<[X, Y]> yazarsınız. Uzunluğu bazı bir koşula bağlı olan veya değişen uzunluktaki demetler için, genel tür parametrelerine kıyasla çok daha fazla destek vardır.
Bunu yapmanın bir yolu şudur:
type NeedExtra = "POST" | "PUT" | "PATCH";
type Allowed = HTTPMethod extends infer H ? H extends NeedExtra ?
[H, any] : [H, any?] : never;
// type Allowed = ["HEAD", any?] | ["GET", any?] | ["POST", any] |
// ["PUT", any] | ["PATCH", any] | ["DELETE", any?]
Yani Allowed, iki elemanlı tuple tiplerinin bir birleşimidir; burada ikinci eleman, birinci eleman tipine bağlı olarak isteğe bağlıdır.
Sonra Endpoint'i, tür parametresinin Allowed ile sınırlı olacak şekilde tanımlayabilirsiniz:
type Endpoint<M extends Allowed> = () => void;
const a: Endpoint<[]> = () => { }; // error
// -------------> ~~
// Source has 0 element(s) but target requires 1
const b: Endpoint<["HEAD"]> = () => { };
const c: Endpoint<["HEAD", {}]> = () => { };
const d: Endpoint<["POST"]> = () => { }; // error
// -------------> ~~~~~~~~
// Type at position 0 in source is not compatible with type at position 0 in target.
const e: Endpoint<["POST", {}]> = () => { };
Bu, istediğiniz gibi çalışır. Dezavantajı bir tuple sarmalayıcı gerektirmesidir, ancak avantajı oldukça basit olmasıdır. Sadece Allowed'a bakabilir ve ne yapıp ne yapamayacağınızı okuyabilirsiniz.
Başka bir çözüm yolu, istenen hatayı "eksik" ikinci argüman yerine ilk tür argümanında görünmesini sağlamaktır. Yani, ikinci argümanı "koşullu olarak isteğe bağlı" yapamayız, ancak ikinci tür argümanı için istenmeyen bir değer, ilk tür argümanının kısıtlamasını karşılamamasına neden olacak şekilde ilk tür parametresini sınırlandırabiliriz.
Böyle görünebilir:
type Endpoint<T extends (
[T, U] extends [NeedExtra, never] ?
Exclude<HTTPMethod, NeedExtra> :
HTTPMethod
), U = never> = () => void
Yani ilk tip parametresi T, kendisine ve ikinci tip parametresi U'ya bağlı bir şeye sınırlandırılmıştır. (Bu, döngüsellik alarmını tetiklemiyor; eğer tetikleseydi, bunu üçüncü bir tip parametresine taşıyabilirdik... ama konudan sapıyorum). İkinci tip argümanın U never olduğunu varsayacağım, o zaman eksik olmasıyla aynı sayılır (eğer never'i missing'den ayırmanız gerekiyorsa, bazı "işaret" tipi oluşturabiliriz, ama bu daha da garip olur).
Dolayısıyla T için kısıtlama, T ve U'ya bağlı olarak ya HTTPMethod ya da Exclude<HTTPMethod, NeedExtra> (NeedExtra olmayan her şey) olacaktır. Eğer T, NeedExtra'ye atanabilir ancak U never ise, o zaman bir sorun vardır ve T'yi NeedExtra olmayan her şeye kısıtlarız, bu da başarısız olacaktır. Aksi takdirde, hiçbir sorun yoktur ve T'yi HTTPMethod ile kısıtlarız.
Haydi deneyelim:
const a: Endpoint = () => { };
// ----> ~~~~~~~~
// error, Generic type 'Endpoint' requires between 1 and 3 type arguments.
const b: Endpoint<"HEAD"> = () => { };
const c: Endpoint<"HEAD", {}> = () => { };
const d: Endpoint<"POST"> = () => { }; // error,
// -------------> ~~~~~~
// Type 'string' does not satisfy the constraint '"HEAD" | "GET" | "DELETE"'.
const e: Endpoint<"POST", {}> = () => { };
Ayrıca iyi görünüyor. Hatalar makul ve istediğiniz şeye çok yakın olma avantajına sahip. Dezavantajı, kısıtlamanın daha kriptik olması ve kesinlikle dil tarafından desteklenmesi amaçlanmamış olması. Bu, gelecekteki bir TypeScript sürümünde bunu bozacak ve yeniden düzenleme gerektirecek bir değişiklik hayal edebilirim (özellikle döngüsellik için). Ama şimdilik çalışıyor ve korkunç değil.
Koda yönelik oyun alanı bağlantısı
