Masalah dengan properti generik saat mengetik pemetaan


11

Saya memiliki perpustakaan yang mengekspor jenis utilitas yang mirip dengan yang berikut:

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

Jenis utilitas ini memungkinkan Anda untuk mendeklarasikan fungsi yang akan berfungsi sebagai "tindakan". Ia menerima argumen umum Modelbahwa tindakan itu akan dilawan.

The dataArgumen dari "tindakan" kemudian diketik dengan jenis utilitas lain yang saya ekspor;

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

The Stateutilitas jenis pada dasarnya mengambil masuk Modelgenerik dan kemudian menciptakan jenis baru di mana semua properti yang bertipe Actiontelah dihapus.

Untuk misalnya di sini adalah implementasi dasar pengguna lahan di atas;

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;
  }
}

Di atas bekerja dengan sangat baik. πŸ‘

Namun, ada kasus yang saya perjuangkan, khususnya ketika definisi model generik didefinisikan, bersama dengan fungsi pabrik untuk menghasilkan contoh-contoh model generik.

Sebagai contoh;

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;
    }
  };
}

Pada contoh di atas saya berharap dataargumen akan diketik di mana doSomethingtindakan telah dihapus, dan valueproperti generik masih ada. Namun ini tidak terjadi - valueproperti juga telah dihapus oleh Stateutilitas kami .

Saya percaya penyebabnya Tadalah generik tanpa batasan jenis / penyempitan yang diterapkan padanya, dan karena itu sistem tipe memutuskan bahwa ia bersinggungan dengan suatu Actiontipe dan kemudian menghapusnya dari datatipe argumen.

Apakah ada cara untuk mengatasi batasan ini? Saya telah melakukan beberapa penelitian dan berharap akan ada beberapa mekanisme di mana saya dapat menyatakan bahwa Tada kecuali untuk Action. yaitu pembatasan tipe negatif.

Membayangkan:

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

Tetapi fitur itu tidak ada untuk TypeScript.

Apakah ada yang tahu cara saya bisa membuat ini berfungsi seperti yang saya harapkan?


Untuk membantu debugging di sini adalah cuplikan kode lengkap:

// 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;
    }
  };
}

Anda dapat bermain dengan contoh kode ini di sini: https://codesandbox.io/s/reverent-star-m4sdb?fontsize=14

Jawaban:


7

Ini masalah yang menarik. Script umumnya tidak bisa berbuat banyak sehubungan dengan parameter tipe generik dalam tipe kondisional. Itu hanya menolak evaluasi extendsjika menemukan bahwa evaluasi melibatkan parameter tipe.

Pengecualian berlaku jika kita bisa mendapatkan naskah untuk menggunakan jenis hubungan tipe khusus, yaitu, relasi kesetaraan (bukan relasi yang diperluas). Relasi kesetaraan mudah dipahami untuk kompiler, sehingga tidak perlu menunda evaluasi tipe bersyarat. Kendala umum adalah salah satu dari sedikit tempat di kompiler di mana jenis kesetaraan digunakan. Mari kita lihat sebuah contoh:

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" 
}

Tautan Playground

Kita dapat memanfaatkan perilaku ini untuk mengidentifikasi tipe tertentu. Sekarang, ini akan menjadi pencocokan jenis yang tepat, bukan pencocokan yang diperluas, dan pencocokan jenis yang tepat tidak selalu cocok. Namun, karena Actionini hanya tanda tangan fungsi, jenis pencocokan yang tepat mungkin bekerja dengan cukup baik.

Mari kita lihat apakah kita dapat mengekstraksi tipe yang cocok dengan tanda tangan fungsi yang lebih sederhana seperti (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;
  // }

}

Tautan Playground

Jenis di atas KeysOfIdenticalTypedekat dengan apa yang kita butuhkan untuk penyaringan. Sebab other, nama properti dipertahankan. Untuk itu action, nama properti dihapus. Hanya ada satu masalah sial di sekitar value. Karena valuetipe T, tidak sepele dipecahkan itu T, dan (v: T) => voidtidak identik (dan sebenarnya mereka mungkin tidak).

Kita masih dapat menentukan yang valueidentik dengan T: untuk properti tipe T, memotong cek ini (v: T) => voiddengan never. Setiap persimpangan dengan nevertrivially resolvable to never. Kami kemudian dapat menambahkan kembali properti jenis Tmenggunakan pemeriksaan identitas lain:

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;
  // }

}

Tautan Playground

Solusi akhir terlihat seperti ini:

// 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;
    }
  };
}

Tautan Playground

CATATAN: Batasan di sini adalah ini hanya berfungsi dengan satu tipe parameter (meskipun mungkin dapat disesuaikan lebih banyak). Selain itu, API agak membingungkan bagi konsumen mana pun, jadi ini mungkin bukan solusi terbaik. Mungkin ada masalah yang belum saya identifikasi. Jika Anda menemukannya, beri tahu saya 😊


2
Saya merasa seperti Gandalf si Putih baru saja mengungkapkan dirinya. 🀯 TBH Saya siap untuk menulis ini sebagai batasan penyusun. Jadi terpacu untuk mencoba ini. Terima kasih! πŸ™‡
ctrlplusb

@ctrlplusb πŸ˜‚ LOL, komentar itu membuat hari saya 😊
Titian Cernicova-Dragomir

Saya bermaksud menerapkan karunia untuk jawaban ini, tetapi saya sangat kurang tidur otak bayi terjadi dan salah ketik. Permintaan maaf saya! Ini adalah jawaban yang luar biasa wawasan. Meskipun sifatnya cukup kompleks. πŸ˜… Terima kasih banyak telah meluangkan waktu untuk menjawabnya.
ctrlplusb

@ctrlplusb :( Oh well .. menangkan beberapa kehilangan :)
Titian Cernicova-Dragomir

2

Akan lebih bagus jika saya bisa menyatakan bahwa T bukan tipe Action. Semacam kebalikan dari ekstensi

Persis seperti yang Anda katakan, masalahnya adalah kita belum memiliki kendala negatif. Saya juga berharap mereka bisa mendapatkan fitur seperti itu segera. Sambil menunggu, saya mengusulkan solusi seperti ini:

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
}

Tidak ideal, tetapi senang mengetahui solusi semi :):
ctrlplusb

1

countdan valueakan selalu membuat compiler tidak senang. Untuk memperbaikinya Anda dapat mencoba sesuatu seperti ini:

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

Karena Partialtipe utilitas sedang digunakan, Anda akan baik-baik saja jika transformmetode case tidak ada.

Stackblitz


1
"Hitung dan nilai akan selalu membuat kompiler tidak bahagia" - Saya akan menghargai beberapa wawasan mengapa di sini. xx
ctrlplusb

1

Secara umum saya membaca itu dua kali dan tidak sepenuhnya mengerti apa yang ingin Anda capai. Dari pemahaman saya, Anda ingin menghilangkan transformdari jenis yang diberikan dengan tepat transform. Untuk mencapai itu sederhana, kita perlu menggunakan Abaikan :

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
    },
  };
}

Tidak yakin apakah ini yang Anda inginkan karena kompleksitas yang Anda berikan pada tipe utilitas tambahan. Semoga ini bisa membantu.


Terima kasih, ya saya berharap. Tapi ini adalah jenis utilitas yang saya ekspor untuk konsumsi pihak ke-3. Saya tidak tahu bentuk / sifat benda mereka. Saya hanya tahu saya perlu menghapus semua properti fungsi dan memanfaatkan hasilnya terhadap argumen transformasi data func.
ctrlplusb

Saya telah memperbarui uraian masalah saya dengan harapan membuatnya lebih jelas.
ctrlplusb

2
Masalah utama adalah bahwa T bisa juga tipe tindakan karena tidak didefinisikan untuk mengecualikannya. Harapan akan menemukan beberapa solusi. Tapi saya berada di tempat di mana hitungan ok tapi T masih dihilangkan karena persimpangan dengan Action
Maciej Sikora

Akan lebih bagus jika saya bisa menyatakan bahwa T bukan tipe Action. Semacam kebalikan dari ekstensi.
ctrlplusb

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.