Tür eşleme yaparken genel özelliklerle ilgili sorun


11

Aşağıdakine benzer bir yardımcı program türü veren bir kütüphane var:

type Action<Model extends object> = (data: State<Model>) => State<Model>;

Bu yardımcı program türü, "eylem" olarak işlev görecek bir işlevi bildirmenize olanak tanır. ModelEylemin karşı çıkacağı genel bir argüman alır .

data"Eylem" argümanı sonra ihracat başka yarar tipiyle yazılan;

type State<Model extends object> = Omit<Model, KeysOfType<Model, Action<any>>>;

Yardımcı Stateprogram türü temel olarak gelen Modelgenel kodu alır ve sonra türdeki tüm özelliklerin Actionkaldırıldığı yeni bir tür oluşturur .

Örneğin, yukarıdakilerin temel kullanıcı arazi uygulaması;

interface MyModel {
  counter: number;
  increment: Action<Model>;
}

const myModel = {
  counter: 0,
  increment: (data) => {
    data.counter; // Exists and typed as `number`
    data.increment; // Does not exist, as stripped off by State utility 
    return data;
  }
}

Yukarıdakiler çok iyi çalışıyor. 👍

Bununla birlikte, özellikle genel bir model tanımı tanımlandığında, genel modelin örneklerini üretmek için bir fabrika işleviyle birlikte mücadele ettiğim bir durum var.

Örneğin;

interface MyModel<T> {
  value: T; // 👈 a generic property
  doSomething: Action<MyModel<T>>;
}

function modelFactory<T>(value: T): MyModel<T> {
  return {
    value,
    doSomething: data => {
      data.value; // Does not exist 😭
      data.doSomething; // Does not exist 👍
      return data;
    }
  };
}

Yukarıdaki örnekte, databağımsız değişkenin doSomethingeylemin kaldırıldığı yere yazılmasını bekliyorum ve genel valueözellik hala var. Ancak durum böyle değildir - valuemülk de tesisimiz tarafından kaldırılmıştır State.

Bunun nedeni, Therhangi bir tür kısıtlaması / daralması uygulanmadan genel olması gerektiğine inanıyorum ve bu nedenle tür sistemi, bir türle kesiştiğine karar veriyor Actionve daha sonra onu dataargüman türünden kaldırıyor .

Bu kısıtlamanın üstesinden gelmenin bir yolu var mı? Biraz araştırma yaptım ve bunun dışındaT herhangi bir şey olduğunu söyleyebileceğim bir mekanizma olacağını umuyordum . yani negatif tip kısıtlaması.Action

Hayal etmek:

function modelFactory<T extends any except Action<any>>(value: T): UserDefinedModel<T> {

Ancak bu özellik TypeScript için mevcut değildir.

Herkes beklediğim gibi bu işe almak için bir yol biliyor mu?


Burada hata ayıklamaya yardımcı olmak için tam bir kod snippet'i bulunmaktadır:

// Returns the keys of an object that match the given type(s)
type KeysOfType<A extends object, B> = {
  [K in keyof A]-?: A[K] extends B ? K : never
}[keyof A];

// Filters out an object, removing any key/values that are of Action<any> type
type State<Model extends object> = Omit<Model, KeysOfType<Model, Action<any>>>;

// My utility function.
type Action<Model extends object> = (data: State<Model>) => State<Model>;

interface MyModel<T> {
  value: T; // 👈 a generic property
  doSomething: Action<MyModel<T>>;
}

function modelFactory<T>(value: T): MyModel<T> {
  return {
    value,
    doSomething: data => {
      data.value; // Does not exist 😭
      data.doSomething; // Does not exist 👍
      return data;
    }
  };
}

Bu kod örneğiyle burada oynayabilirsiniz: https://codesandbox.io/s/reverent-star-m4sdb?fontsize=14

Yanıtlar:


7

Bu ilginç bir problem. Daktilo yazımı genellikle koşullu yazım türlerindeki genel yazım parametreleri ile ilgili pek bir şey yapamaz. extendsDeğerlendirmenin bir tür parametresi içerdiğini tespit edip etmediğini değerlendirir .

Özel bir tür ilişkisini, yani bir eşitlik ilişkisini ( uzatılmış bir ilişki değil) kullanmak için dizgi yazabiliyorsak, bir istisna uygulanır . Bir eşitlik ilişkisinin derleyici için anlaşılması kolaydır, bu nedenle koşullu tür değerlendirmesinin ertelenmesine gerek yoktur. Genel kısıtlamalar derleyicide tür eşitliğinin kullanıldığı birkaç yerden biridir. Bir örneğe bakalım:

function m<T, K>() {
  type Bad = T extends T ? "YES" : "NO" // unresolvable in ts, still T extends T ? "YES" : "NO"

  // Generic type constrains are compared using type equality, so this can be resolved inside the function 
  type Good = (<U extends T>() => U) extends (<U extends T>() => U) ? "YES" : "NO" // "YES"

  // If the types are not equal it is still un-resolvable, as K may still be the same as T
  type Meh = (<U extends T>()=> U) extends (<U extends K>()=> U) ? "YES": "NO" 
}

Oyun Alanı Bağlantısı

Belirli türleri tanımlamak için bu davranıştan yararlanabiliriz. Şimdi, bu tam bir eşleme olacak, uzatılmış bir eşleşme olmayacak ve kesin tip eşlemeler her zaman uygun olmayacaktır. Ancak, Actionyalnızca bir işlev imzası olduğundan, tam tür eşleşmeleri yeterince iyi çalışabilir.

Daha basit bir işlev imzasıyla eşleşen türleri çıkartıp çıkartamayacağımızı görelim (v: T) => void:

interface Model<T> {
  value: T,
  other: string
  action: (v: T) => void
}

type Identical<T, TTest, TTrue, TFalse> =
  ((<U extends T>(o: U) => void) extends (<U extends TTest>(o: U) => void) ? TTrue : TFalse);

function m<T>() {
  type M = Model<T>
  type KeysOfIdenticalType = {
    [K in keyof M]: Identical<M[K], (v: T) => void, never, K>
  }
  // Resolved to
  // type KeysOfIdenticalType = {
  //     value: Identical<T, (v: T) => void, never, "value">;
  //     other: "other";
  //     action: never;
  // }

}

Oyun Alanı Bağlantısı

Yukarıdaki tip KeysOfIdenticalTypefiltreleme için ihtiyacımız olan şeye yakındır. İçin other, mülk adı korunur. İçin, actionözellik adı silinir. Etrafta sadece bir sinir bozucu sorun var value. Yana valuetiptedir T, trivially bu çözülebilir değildir olan Tve (v: T) => voidözdeş değildir (ve aslında olmayabilir).

Yine de valuebunun aynı olduğunu belirleyebiliriz T: tür özellikleri Tiçin bu denetimi (v: T) => voidile kesiştirin never. neverİle kesişme önemsiz bir şekilde çözülebilir never. Daha sonra Tbaşka bir kimlik denetimi kullanarak türün özelliklerini geri ekleyebiliriz :

interface Model<T> {
  value: T,
  other: string
  action: (v: T) => void
}

type Identical<T, TTest, TTrue, TFalse> =
  ((<U extends T>(o: U) => void) extends (<U extends TTest>(o: U) => void) ? TTrue : TFalse);

function m<T>() {
  type M = Model<T>
  type KeysOfIdenticalType = {
    [K in keyof M]:
      (Identical<M[K], (v: T) => void, never, K> & Identical<M[K], T, never, K>) // Identical<M[K], T, never, K> will be never is the type is T and this whole line will evaluate to never
      | Identical<M[K], T, K, never> // add back any properties of type T
  }
  // Resolved to
  // type KeysOfIdenticalType = {
  //     value: "value";
  //     other: "other";
  //     action: never;
  // }

}

Oyun Alanı Bağlantısı

Son çözüm şuna benzer:

// Filters out an object, removing any key/values that are of Action<any> type
type State<Model extends object, G = unknown> = Pick<Model, {
    [P in keyof Model]:
      (Identical<Model[P], Action<Model, G>, never, P> & Identical<Model[P], G, never, P>)
    | Identical<Model[P], G, P, never>
  }[keyof Model]>;

// My utility function.
type Action<Model extends object, G = unknown> = (data: State<Model, G>) => State<Model, G>;


type Identical<T, TTest, TTrue, TFalse> =
  ((<U extends T>(o: U) => void) extends (<U extends TTest>(o: U) => void) ? TTrue : TFalse);

interface MyModel<T> {
  value: T; // 👈 a generic property
  str: string;
  doSomething: Action<MyModel<T>, T>;
  method() : void
}


function modelFactory<T>(value: T): MyModel<T> {
  return {
    value,
    str: "",
    method() {

    },
    doSomething: data => {
      data.value; // ok
      data.str //ok
      data.method() // ok 
      data.doSomething; // Does not exist 👍
      return data;
    }
  };
}

/// Still works for simple types
interface MyModelSimple {
  value: string; 
  str: string;
  doSomething: Action<MyModelSimple>;
}


function modelFactory2(value: string): MyModelSimple {
  return {
    value,
    str: "",
    doSomething: data => {
      data.value; // Ok
      data.str
      data.doSomething; // Does not exist 👍
      return data;
    }
  };
}

Oyun Alanı Bağlantısı

NOTLAR: Buradaki sınırlama, bunun yalnızca bir tip parametresiyle çalışmasıdır (ancak daha fazlasına uyarlanabilir). Ayrıca, API herhangi bir tüketici için biraz kafa karıştırıcıdır, bu nedenle bu en iyi çözüm olmayabilir. Henüz belirlemediğim sorunlar olabilir. Eğer bulursan, bana bildir 😊


2
Beyaz Gandalf'ın kendini açığa vurduğunu hissediyorum. BH TBH Bunu derleyici sınırlaması olarak yazmaya hazırdım. Yani bunu denemek için stoked. Teşekkür ederim! 🙇
ctrlplusb

@ctrlplusb 😂 LOL, bu yorum günümü yaptı 😊
Titian Cernicova-Dragomir

Ödemeyi bu cevaba uygulamak istedim ama devam eden ve yanlış yapılan ciddi uyku eksikliğim var. Özür dilerim! Bu fevkalade anlayışlı bir cevap. Doğada oldukça karmaşık olsa da. Answer Cevaplamak için zaman ayırdığınız için çok teşekkür ederim.
ctrlplusb

@ctrlplusb :( Oh iyi .. biraz kazanın :)
Titian Cernicova-Dragomir

2

T'nin Action türünde olmadığını ifade edebilmek harika olurdu. Bir çeşit tersinir

Tam olarak söylediğin gibi, sorun şu ki, henüz olumsuz bir kısıtlamamız yok. Umarım yakında böyle bir özelliği indirebilirler. Beklerken, şöyle bir geçici çözüm öneriyorum:

type KeysOfNonType<A extends object, B> = {
  [K in keyof A]-?: A[K] extends B ? never : K
}[keyof A];

// CHANGE: use `Pick` instead of `Omit` here.
type State<Model extends object> = Pick<Model, KeysOfNonType<Model, Action<any>>>;

type Action<Model extends object> = (data: State<Model>) => State<Model>;

interface MyModel<T> {
  value: T;
  doSomething: Action<MyModel<T>>;
}

function modelFactory<T>(value: T): MyModel<T> {
  return {
    value,
    doSomething: data => {
      data.value; // Now it does exist 😉
      data.doSomething; // Does not exist 👍
      return data;
    }
  } as MyModel<any>; // <-- Magic!
                     // since `T` has yet to be known
                     // it literally can be anything
}

İdeal değil, ama yarı bir geçici çözüm bilmek harika :)
ctrlplusb

1

countve valuederleyiciyi her zaman mutsuz yapar. Düzeltmek için şöyle bir şey deneyebilirsiniz:

{
  value,
  count: 1,
  transform: (data: Partial<Thing<T>>) => {
   ...
  }
}

Yana Partialyarar tipi kullanılıyor durumda, ok olacak transformyöntem mevcut değildir.

Stackblitz


1
"saymak ve değer her zaman derleyici mutsuz yapacak" - Ben neden burada bazı içgörü takdir ediyorum. xx
ctrlplusb

1

Genellikle bunu iki kez okudum ve ne elde etmek istediğinizi tam olarak anlamadım. Anladığım kadarıyla transform, tam olarak verilen türden atlamak istersiniz transform. Bunu başarmak için Omit'i kullanmamız gerekir :

interface Thing<T> {
  value: T; 
  count: number;
  transform: (data: Omit<Thing<T>, 'transform'>) => void; // here the argument type is Thing without transform
}

// 👇 the factory function accepting the generic
function makeThing<T>(value: T): Thing<T> {
  return {
    value,
    count: 1,
      transform: data => {
        data.count; // exist
        data.value; // exist
    },
  };
}

Ek yardımcı program türlerinde verdiğiniz karmaşıklık nedeniyle bunun istediğiniz şey olup olmadığından emin değilim. Umarım yardımcı olur.


Teşekkürler, evet keşke. Ama bu üçüncü taraf tüketimi için ihraç ettiğim bir hizmet türüdür. Nesnelerinin şeklini / özelliklerini bilmiyorum. Ben sadece tüm fonksiyon özelliklerini kapalı ve dönüşüm func veri bağımsız değişkeni karşı sonucu kullanmanız gerektiğini biliyorum.
ctrlplusb

Sorun açıklamamı daha açık hale getirmesini umarak güncelledim.
ctrlplusb

2
Ana sorun, T'nin dışarıda bırakılacak şekilde tanımlanmadığı için Eylem türü olabilmesidir. Umut bir çözüm bulacak. Ama sayımın tamam olduğu yerdeyim ama T hala atlanmış çünkü Action ile kesişme
Maciej Sikora

T'nin Action türünde olmadığını ifade edebilmek harika olurdu. Bir çeşit tersi uzanır.
ctrlplusb

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.