Typescript generik parametrelerini (a: T, B: T) aynı türde olacak şekilde zorlayın


Verilen :

function sorter<T extends string | number> () {
  const icf = new Intl.Collator().compare
  return (a: T, b: T) => typeof a === 'number' ? a - b : icf(a, b)
}

Bir Typescript tür hatası alıyoruz:

Biliyoruz ki eğer a bir sayı ise b de bir sayı olacak ve benzer şekilde eğer a bir string ise b de bir string olacak. Bu bilgi, sorter fonksiyonunu çağıran fonksiyon tarafından bilinmemektedir, yani yalnızca döndürülen fonksiyon çağrıldığında bilinir. Tasarım gereği asla çeşitli türde öğeler almayı beklemiyoruz.

Bu türleri doğru bir şekilde belirtmek için hangi seçenekler var?

Typescript Oyun Alanı

Cevap #1

Böyle bir şey yapabilirsin:

type T = number | string;

function numSorter(a: number, b: number){
  return a - b;
}

function stringSorter(a: string, b: string, locale: string) {
  const icf = new Intl.Collator(locale).compare;
  return icf(a,b)
}

function picker(a: T, b: T, locale: string) {
  if (typeof a === 'number' && typeof b === 'number') {
    return numSorter(a,b);
  } else if (typeof a === 'string' && typeof b === 'string') {
    return stringSorter(a,b, locale);
  } else {
    throw Error("incompatible types");
  }
}

function sorter(locale: string) {
  return (a:T, b:T) => picker(a,b,locale);
}

const mySorter = sorter("US");

console.log(mySorter(1,2)); //both numbers, so ok
console.log(mySorter("world","hello")); // both strings, so ok
console.log(mySorter(1,"hello")); // incompatible types

Bu şekilde bir çıktı verir:

Yanılıyor olabilirim, ama sanırım bu tür bir yaklaşım statik tip kontrolünün amacını boşa çıkarıyor. Genel referansın hâlâ somut bir uygulamaya sahip olması gerekiyor.

Bu durumda, gerçekleştirdiğiniz işlemler farklı türler number ve string için farklıdır, bu yüzden bir örtüşme yoktur. Genel yaklaşım, işlevsellik açısından bazı ortak yönleri olan nesneler için faydalı olabilir, örneğin her ikisinin de sahip olduğu ortak bir yöntem gibi, belgelerde gösterildiği gibi bir dizinin uzunluğunu bulmak için yardımcı bir yöntem istiyorsanız:

function loggingIdentity<Type>(arg: Array<Type>): Array<Type> {
  console.log(arg.length); // Array has a .length, so no more error
  return arg;
}

Bu genel işlev, herhangi bir dizinin herhangi bir türü için çalışacaktır, çünkü herhangi bir dizinin length özelliği vardır.

Genel olarak, bunun bu durum için faydalı olduğunu düşünmüyorum, bence baştan itibaren somut implementasyonlara sahip olmak ve gerektiğinde doğru sıralama fonksiyonunu çağırmak daha iyi olabilir. Generikler dürüst olmak gerekirse biraz baş ağrısı, özellikle TypeScript'te.

Cevap #2

Sorununuz, T extends string | number generik tür üzerine konan kısıtlamanın, (a: T, b: T) => ... ifadesinde a ve b'nin aynı ilkel türe sahip olmasını zorunlu kılmamasıdır. Eğer T kendisi string | number birleşim türü ise, biri string diğeri sayı olabilir.

Örneğin:

// this compiles fine, even though the parameters have different types
const a: number | string = 2;
const b: number | string = '2';
sorter("en-us")(a, b);

Ayrıca, sorter fonksiyonunu genel hale getirdiniz, bu da karşılaştırıcı fonksiyonun parametrelerinin türünün sorter çağrıldığı anda seçildiği anlamına geliyor. Aslında, bu tür sorter() tarafından döndürülen fonksiyonun çağıranı tarafından seçilmelidir. Eğer bu anonim fonksiyonu <T,>(a: T, b: T) => typeof ... döndürerek genel yaparsak, aynı sorunla karşılaşırız: bu, çağıranın T türü için string | number seçmesine ve karşılaştırma için bir string ile bir number geçirmesine izin verir.

Bu biraz çirkin, ama yerine yalnızca (string, string) => number ve (number, number) => number için beyan edilmiş aşırı yüklemelere sahip bir fonksiyon döndürerek istenen davranışı elde edebiliriz. Tek bir tür doğrulaması yapmamız gerekiyor, ancak bu yalnızca fonksiyon aşırı yüklemelerinde tür çıkarımının bir sınırlamasından kaynaklanıyor:

export function sorter(locale: string) {
  const icf = new Intl.Collator(locale).compare

  function comp(a: number, b: number): number;
  function comp(a: string, b: string): number;
  function comp(a: number | string, b: number | string) : number {
    if (typeof a === "number") {
      // assertion needed here: TS can't infer one param's type from another's via
      // their relationship in overloads
      // (but it is safe: this fn can only be called via a declared overload)
      return a - (b as number);
    } else {
      return icf(a, b as string);
    }
  }

  return comp;
}