Redux: Dukungan untuk tindakan yang diketik (TypeScript)

Dibuat pada 2 Nov 2015  ·  71Komentar  ·  Sumber: reduxjs/redux

Terkait dengan dua masalah ini:

Hai Dan,

Perusahaan saya menggunakan Redux serta TypeScript, dan kami ingin menambahkan beberapa jaminan waktu kompilasi tambahan untuk tindakan kami. Saat ini ini tidak mungkin karena batasan bahwa tindakan harus berupa objek biasa. Anda mengatakan bahwa ini karena 'rekam/putar ulang dan fitur terkait tidak akan berfungsi dengan kelas tindakan' dan saya mengerti bahwa itu adalah fitur yang berharga dan tidak ingin kehilangannya. Tetapi saya memiliki kode menggunakan kelas TypeScript yang (ketika redux devtools diaktifkan) masih dapat merekam, membatalkan, dan memutar ulang tindakan dengan baik (menggunakan kode yang saya posting di edisi 355).

Ketika saya menghapus devtools, saya hanya mendapatkan kesalahan dari pemeriksaan itu bahwa tindakan adalah objek biasa dan eksekusi berhenti di situ. Bagi saya sepertinya terlalu ketat, memeriksa lebih dari yang benar-benar diperlukan. Saya lebih suka memiliki cek yang mengharuskan tindakan dapat serial. Saya tahu kinerja menjadi perhatian - IMO seharusnya tidak menjamin/membuktikan bahwa mereka dapat serial, cukup periksa apakah objek tindakan itu sendiri mengatakannya, dan kemudian percayai mereka.
(Tidak yakin mengapa pemeriksaan tidak juga gagal ketika devtools diaktifkan - mungkin Anda dapat menjelaskannya).

Saya sangat ingin dapat memanfaatkan keunggulan TypeScript dan Redux. Semoga Anda dapat membantu saya (dan orang lain yang menggunakan kedua alat ini) sampai pada solusi.

Saya juga ingin menegaskan kembali bahwa (sebanyak yang saya inginkan sebaliknya!) TypeScript tidak memiliki serikat pekerja yang didiskriminasi, jadi solusi berdasarkan pendekatan itu tidak akan berhasil.

Terima kasih!

discussion

Komentar yang paling membantu

Saya menggunakan FSA dan saya merasa sangat nyaman untuk memiliki antarmuka Action generik dengan parameter tipe yang sesuai dengan tipe payload :

interface Action<T>{
  type: string;
  payload: T;
  error?: boolean;
  meta?: any;
}

Kemudian saya mengekspor jenis muatan di samping jenis tindakan menggunakan nama yang sama:

export const ADD_TODO = 'ADD_TODO';
export type ADD_TODO = {text: string};

Dengan cara ini ketika Anda mengimpor ADD_TODO , Anda sebenarnya mengimpor jenis tindakan dan jenis muatan:

import {ADD_TODO} from './actions';
import {handleActions} from 'redux-actions';

const reducer = handleActions({
  [ADD_TODO]: (state, action:Action<ADD_TODO>) => {
    // enabled type checking for action.payload
    // ...
  }
});

Semua 71 komentar

Apakah TypeScript memungkinkan Anda mengetik-memeriksa bentuk objek biasa alih-alih mendefinisikan kelas?
Satu-satunya masalah saya adalah dengan menggunakan instance kelas untuk tindakan.

Bukankah antarmuka TypeScript cukup untuk mengetikkan tindakan objek biasa dengan kuat?

Hampir - masalahnya adalah mereka hanya ada pada waktu kompilasi. Jadi peredam tidak akan memiliki cara untuk mengetahui tindakan masuk mana dari antarmuka mana. ...Kecuali saya menyimpan properti 'type' pada setiap tindakan dan kemudian secara manual membuangnya setelah memeriksa properti itu. Dan pada saat itu saya tidak mendapatkan keamanan tipe tambahan apa pun hanya dengan memperlakukan mereka sebagai objek biasa.

@nickknw Yah tidak ada cara untuk mengetikkan tindakan itu dengan kuat! Maksud saya itu tidak mungkin bahkan jika itu C#! Hanya saja arsitekturnya, tidak ada hubungannya dengan redux. Saya sendiri menggunakan redux dengan TypeScript juga. Dan yang terbaik yang bisa saya temukan adalah memiliki antarmuka dengan nama yang sama dengan tipe tindakan saya, menjaga antarmuka dan konstanta di tempat yang sama, menegakkan actionCreators saya untuk menghormati pengetikan dan memasukkan ke dalam reduksi. Pemeliharaannya tidak terlalu sulit dan ini memberi saya jenis keamanan yang saya butuhkan dengan maksud yang jelas mengapa antarmuka ada. Saya menyarankan Anda untuk mempertimbangkan pendekatan ini, itu berhasil untuk saya :grin: dan proyek saya agak besar :D

Anda mengatakan bahwa ini karena 'rekam/putar ulang dan fitur terkait tidak akan berfungsi dengan kelas tindakan' dan saya mengerti bahwa itu adalah fitur yang berharga dan tidak ingin kehilangannya. Tetapi saya memiliki kode menggunakan kelas TypeScript yang (ketika redux devtools diaktifkan) masih dapat merekam, membatalkan, dan memutar ulang tindakan dengan baik (menggunakan kode yang saya posting di edisi 355).

Ya, tetapi kemudian jika Anda menggunakan penambah persistState() , membuat cerita bersambung, dan kemudian memulihkan status Anda dan menghapusnya, mereka tidak akan berfungsi lagi karena kode Anda mengandalkan cek instanceof .

"Objek biasa" tidak benar-benar intinya. Apa yang ingin kami terapkan adalah Anda tidak bergantung pada tindakan yang menjadi instance dari kelas tertentu di reduksi Anda, karena ini tidak akan pernah terjadi setelah (de)serialisasi.

Dengan kata lain, ini adalah anti-pola:

function reducer(state, action) {
  if (action instanceof SomeAction) {
    return ...
  } else {
    return ...
  }
}

Jika kami memungkinkan untuk pengguna TypeScript, saya tahu pengguna JavaScript akan tergoda dan akan menyalahgunakan ini karena banyak orang yang berasal dari bahasa OO tradisional terbiasa menulis kode dengan cara ini. Kemudian mereka akan kehilangan manfaat merekam/memutar ulang di Redux.

Salah satu poin dari Redux adalah untuk menerapkan beberapa batasan yang kami anggap penting, dan jika tidak diterapkan, tidak akan memungkinkan adanya alat yang kaya di kemudian hari. Jika kita membiarkan orang menggunakan kelas untuk tindakan, mereka akan melakukannya, dan mengimplementasikan sesuatu seperti redux-recorder tidak akan mungkin karena tidak ada jaminan orang menggunakan tindakan objek biasa. Apalagi jika proyek boilerplate tiba-tiba memutuskan untuk mengadopsi kelas karena "wow, sekarang diperbolehkan, itu pasti ide yang bagus".

Apa yang ingin kami terapkan adalah Anda tidak bergantung pada tindakan yang menjadi instance dari kelas tertentu di reduksi Anda, karena ini tidak akan pernah terjadi setelah (de)serialisasi.

Saya pikir ini adalah di mana Anda kehilangan saya. Saya tidak mengikuti mengapa membuat serial dan deserializing suatu objek berarti objek itu akan kehilangan informasi terkait kelasnya.

let actions = [new SomeAction(), new OtherAction()];
let json = JSON.stringify(actions);
let revivedActions = JSON.parse(json);
// reducers no longer understand revivedActions because type info is stripped.

Alasan lain mengapa saya tidak ingin mengizinkan orang membuat kelas untuk tindakan adalah karena ini akan mendorong orang untuk

a) Buat hierarki kelas
b) Letakkan logika di sana, misalnya metode "tambalan" dari https://github.com/rackt/redux/issues/437#issue -99905895

Keduanya bertentangan dengan bagaimana saya bermaksud menggunakan Redux. Namun React menunjukkan bahwa jika Anda memberikan kelas ES6 kepada orang-orang, mereka akan menggunakan pewarisan bahkan ketika ada alternatif yang jauh lebih baik, hanya karena itu tertanam dalam pengembangan perangkat lunak. Sulit untuk melepaskan kebiasaan lama kecuali Anda terpaksa.

Sangat menyedihkan bahwa TypeScript memaksa Anda untuk mengubah hal-hal yang ingin Anda ketik dengan kuat ke dalam kelas, tetapi saya yakin ini adalah kesalahan TypeScript, bukan kesalahan kami. Seperti yang dijelaskan di https://github.com/rackt/redux/issues/992#issuecomment -153138035 , saya menemukan casting sebagai solusi terbaik yang dapat kami tawarkan. Anda juga dapat memublikasikan proyek redux-typescript-store dengan createStore() yang tidak memiliki batasan ini, tetapi saya tetap tidak harus menghapusnya.

Akhirnya: sebagai catatan, saya tidak membenci kelas.
Itu bukan poin saya.

Satu-satunya poin saya adalah bahwa tindakan harus _deskripsi pasif tentang apa yang terjadi_. Jika kita mengizinkannya menjadi instance kelas, orang _will_ membangun abstraksi aneh di atas tindakan dengan kelas, dan akibatnya menderita.

Tidak ingin.

Ahh oke, terima kasih untuk contohnya. Saya tidak menyadari bahwa JSON.stringify membuang informasi prototipe. Melakukan hal itu masuk akal bagi saya dalam retrospeksi, mengingat keterbatasan dari apa yang dapat disimpan JSON.

Salah satu poin dari Redux adalah untuk menerapkan beberapa batasan yang kami anggap penting, dan jika tidak diterapkan, tidak akan memungkinkan adanya alat yang kaya di kemudian hari.

Ya, saya mengerti, saya setuju, dan tidak ingin berkompromi dengan jaminan yang dapat diandalkan oleh alat tersebut. Saya tidak ingin membuat hierarki kelas atau melampirkan logika ke kelas, hanya ingin dapat membedakannya pada waktu kompilasi.

Sangat menyedihkan bahwa TypeScript memaksa Anda untuk mengubah hal-hal yang ingin Anda ketik dengan kuat ke dalam kelas, tetapi saya yakin ini adalah kesalahan TypeScript, bukan kesalahan kami.

Antarmuka bekerja cukup baik untuk apa yang dirancang, tetapi saya setuju sangat disayangkan untuk kasus penggunaan ini bahwa mereka tidak menyimpan informasi jenis apa pun saat runtime (atau mengizinkan pemeriksaan runtime 'apakah ini sesuai dengan antarmuka ini' di tempat lain jalan).

Satu-satunya poin saya adalah bahwa tindakan harus merupakan deskripsi pasif dari apa yang terjadi.

100% setuju dengan Anda di sini

Jika kita mengizinkannya menjadi instance kelas, orang akan membangun abstraksi aneh di atas tindakan dengan kelas, dan akibatnya menderita.

Ugh, kurasa itu benar juga.


Saya masih berpikir mungkin ada ruang untuk jalan tengah di sini, dengan cara serialisasi tindakan yang sedikit lebih fleksibel. Ada parameter 'reviver' dan 'replacer' untuk parse dan stringify - mungkin ada cara untuk membuat fungsi serialisasi yang dapat menyimpan informasi yang cukup bagi saya untuk meneruskan beberapa informasi tipe dasar tetapi juga cukup tahu tentang struktur untuk memperingatkan/kesalahan tentang skenario yang ingin Anda cegah.

Lebih suka tidak mengikuti rute tindakan casting secara manual - saya mungkin jika tidak ada yang berhasil tetapi pada saat itu jenisnya tidak menambahkan banyak nilai untuk upaya tersebut.

Dan ya, alangkah baiknya jika TypeScript memiliki opsi pengetikan yang lebih fleksibel sehingga saya tidak perlu menggunakan kelas tetapi itu bukan sesuatu yang secara realistis dapat saya pengaruhi atau andalkan untuk diubah dalam waktu dekat.

mungkin ada cara untuk membuat fungsi serialisasi yang dapat menyimpan informasi yang cukup bagi saya untuk melewatkan beberapa informasi tipe dasar tetapi juga cukup tahu tentang struktur untuk memperingatkan/kesalahan tentang skenario yang ingin Anda cegah.

Ini akan melibatkan pengguna yang mengimplementasikan fungsi yang memetakan string type ke kelas yang cukup banyak casting manual lagi. Dan kami tidak ingin pengguna menerapkan hal semacam ini agar rekaman/pemutaran ulang berfungsi. Ini adalah poin lain dari Redux: Anda mendapatkan hot reload, record/replay _for free_, tanpa menerapkan serialize() , hydrate() , dll.

Ini akan melibatkan fungsi implementasi pengguna yang memetakan string tipe ke kelas yang cukup banyak casting manual lagi.

Idealnya itu hanya akan melibatkan penerapan sepasang fungsi yang melakukan itu, dan kemudian semua kelas tindakan akan diimplementasikan sedemikian rupa sehingga fungsi-fungsi itu dapat membuat serial. Jadi, alih-alih mungkin memperkenalkan kesalahan manusia sekali untuk setiap tindakan berbeda yang ditangani, itu hanya akan sekali.

Dan kami tidak ingin pengguna menerapkan hal semacam ini agar rekaman/pemutaran ulang berfungsi.

Bukan apa yang saya usulkan - ini bisa menjadi sesuatu yang ikut serta. Jika Anda tidak perlu melakukannya, semuanya berfungsi seperti biasa. Saya mengerti Anda mungkin tidak antusias membuka kaleng cacing itu. Saya tidak keberatan menggali ini di beberapa titik dan melihat apakah saya dapat menemukan beberapa cara untuk mempertahankan jenis informasi yang saya inginkan tanpa membuka pintu lebar-lebar untuk yang lainnya.

Ini adalah poin lain dari Redux: Anda mendapatkan hot reload, record/replay gratis, tanpa mengimplementasikan serialize(), hydrate(), dll.

Ini adalah manfaat besar dari Redux dan saya tidak ingin mengubahnya. Tetapi saya juga ingin mendapatkan beberapa jaminan waktu kompilasi untuk tindakan saat menggunakan Redux.

Saya tidak ingin mengubahnya di inti karena alasan di atas. Seperti yang saya katakan sebelumnya, jangan ragu untuk membuat paket redux-typescript dengan versi createStore ramah TS dan (mungkin?) fungsi Redux lainnya. Kami tidak bermaksud untuk melanggar perubahan di Redux pada saat ini sehingga Anda tidak perlu khawatir untuk mengikuti API. Dan selama API cocok, toko khusus tersebut akan berfungsi dengan baik. (Dan implementasinya kecil, lihat sumber createStore ).

Saya pikir Anda dapat menyelesaikan masalah dengan menggunakan TypeScript 1.6 penjaga tipe yang ditentukan pengguna (lihat http://blogs.msdn.com/b/typescript/archive/2015/09/16/announcing-typescript-1-6.aspx ) .

Tentukan antarmuka untuk tindakan dasar dan satu untuk setiap tindakan lainnya. Selain itu untuk setiap Tindakan buat fungsi isXYZAction(a: Action) yang memeriksa tipe berdasarkan nilai bidang tipe (yang didefinisikan dalam antarmuka tindakan dasar.

Gunakan fungsi ini di peredam untuk mengakses bidang tindakan spesifik dengan aman setelah pemeriksaan:

interface Action {
  type: string
}

interface AddTodoAction extends Action {
  text: string
}

function isAddTodoAction(action: Action): action is AddTodoAction {
  return action.type == ADD_TODO;
}

function reducer(state: any, action: Action) {
  if (isAddTodoAction(action)) {
    // typesafe access to action.text is possible here
  } else {
    return ...
  }
}

@ Matthias247 itulah yang saya butuhkan, terima kasih!

@ Matthias247 Itu luar biasa, terima kasih telah menunjukkannya! Tidak tahu tentang fitur itu, itulah yang saya butuhkan :)

Saya menggunakan FSA dan saya merasa sangat nyaman untuk memiliki antarmuka Action generik dengan parameter tipe yang sesuai dengan tipe payload :

interface Action<T>{
  type: string;
  payload: T;
  error?: boolean;
  meta?: any;
}

Kemudian saya mengekspor jenis muatan di samping jenis tindakan menggunakan nama yang sama:

export const ADD_TODO = 'ADD_TODO';
export type ADD_TODO = {text: string};

Dengan cara ini ketika Anda mengimpor ADD_TODO , Anda sebenarnya mengimpor jenis tindakan dan jenis muatan:

import {ADD_TODO} from './actions';
import {handleActions} from 'redux-actions';

const reducer = handleActions({
  [ADD_TODO]: (state, action:Action<ADD_TODO>) => {
    // enabled type checking for action.payload
    // ...
  }
});

Saya memiliki masalah yang sama, dan menemukan solusinya di sini: http://www.bluewire-technologies.com/2015/redux-actions-for-typescript/ . Ini pada dasarnya adalah generalisasi dari solusi yang diusulkan oleh @Matthias247 , dan memiliki keuntungan yang jelas: tidak perlu mendefinisikan fungsi is*Action() untuk setiap tindakan.

Solusi ini tampaknya berfungsi, tetapi perlu 2 tambahan untuk Redux. Pertama, Anda memerlukan middleware untuk mengubah tindakan menjadi objek biasa (redux melakukan pemeriksaan ini dan membuat kesalahan). Ini bisa sesederhana:

// middleware for transforming Typed Actions into plain actions
const typedToPlain = (store: any) => (next: any) => (action: any) =>  {
    next(Object.assign({}, action));
};

Dan jika Anda menggunakan tslint, ia akan mengeluh tentang properti _brand yang tidak digunakan, jadi satu-satunya hal yang dapat Anda lakukan adalah menyertakannya di tslint abaikan komentar:

/* tslint:disable */
private _brand: NominalType;
/* tslint:enable */

BTW, saya tidak menggunakan NominalType karena sepertinya menambah boilerplate, dan hanya menggunakan tipe void langsung.

Bagi mereka yang tertarik dengan 'flux' lib dengan dukungan asli untuk TypeScript, Anda mungkin ingin memeriksa: https://github.com/AlexGalays/fluxx#typescript -store

@AlexGalays Kerja bagus sejauh ini! Tidakkah Anda berpikir untuk mengganti Redux dan menambahkan dukungan TypeScript ke dalamnya, sehingga komunitas mendapatkan semua fitur yang disediakan oleh Redux dan modul temannya, seperti DevTools, middleware, saga, dll?

UPD Oh, begitu, ini https://github.com/rackt/redux/issues/992#issuecomment -153662848 menjawab pertanyaan saya. Lalu mengapa fluxx dan bukan hanya redux-typescript ? Dan apakah itu kompatibel dengan alat berbasis Redux?

UPD2 Dengan melihat lebih dalam, satu-satunya tambahan untuk Redux adalah typings dan tsconfig.json -- tidak bisakah kita mendapatkan repo hanya dengan ini, dan menggunakan kembali serta mendelegasikan pemeliharaan Redux ke pengembang Redux, untuk menghindari semua callbacks dan Dispatch in the middle of dispatch dan Store.log diimplementasikan di perpustakaan berorientasi TypeScript?

@sompylasar Saya bertanya pada diri sendiri pertanyaan yang sama di beberapa titik: "Mengapa tidak menggunakan redux saja, itu terlihat cukup dekat dengan apa yang saya inginkan dari wadah negara"; Tapi 1) sedikit kompetisi selalu bagus (Berbicara secara umum. fluxx jelas dilenyapkan oleh kompetisi redux), 2) Saya ingin membuatnya tetap sederhana dan mudah dipelajari untuk rekan kerja saya. Itulah yang saya dapatkan dengan fluxx yang tidak saya dapatkan dengan redux: Hanya satu ketergantungan npm untuk semua kebutuhan saya (namun itu bahkan lebih kecil daripada redux, Anda dapat memeriksanya!), Lebih sedikit boilerplate (tindakannya mengirim sendiri dan sangat cepat untuk tulis yang baru) dan jaminan bahwa definisi TypeScript saya tetap mutakhir (saya tidak percaya pada definisi eksternal kecuali untuk hal-hal seperti reaksi di mana Anda dapat cukup yakin berbagai orang akan tetap memperbaruinya)

Di sisi lain saya tidak membutuhkan banyak hal redux dan berbagai ekstensi menyediakannya.

Omong-omong, router abyssa saya juga segera mendapatkan definisi TypeScript dan saya merasa bekerja dengan kombinasi abyssa/fluxx/immupdate sangat menyenangkan.

lebih sedikit boilerplate (tindakannya mengirim sendiri dan sangat cepat untuk menulis yang baru)

FWIW, dari http://redux.js.org/docs/recipes/ReducingBoilerplate.html :

Tindakan adalah objek biasa yang menjelaskan apa yang terjadi di aplikasi, dan berfungsi sebagai satu-satunya cara untuk menggambarkan niat untuk mengubah data. Penting bahwa tindakan yang menjadi objek yang harus Anda kirim bukanlah boilerplate, tetapi salah satu pilihan desain dasar Redux.

Ada kerangka kerja yang mengklaim mirip dengan Flux, tetapi tanpa konsep objek tindakan. Dalam hal dapat diprediksi, ini adalah langkah mundur dari Flux atau Redux. Jika tidak ada tindakan objek biasa yang dapat diserialisasi, tidak mungkin untuk merekam dan memutar ulang sesi pengguna, atau menerapkan pemuatan ulang panas dengan perjalanan waktu. Jika Anda lebih suka mengubah data secara langsung, Anda tidak perlu Redux.

Salah satu proyek pesaing menarik yang ramah TypeScript adalah https://github.com/ngrx/store
Ini tidak sepenuhnya kompatibel dengan Redux tetapi orang suka menggunakannya dengan Angular.

@gaearon Saya tahu mengapa Anda melakukannya, saya tidak mengatakan "redux bisa memiliki lebih sedikit boilerplate". Saya baru saja memilih serangkaian pengorbanan yang berbeda :p

https://github.com/ngrx/store menggunakan TypeScript tetapi tidak aman untuk mengetik sama sekali. Anda dapat mengirimkan apa saja, dan Anda tidak memiliki petunjuk ketik dalam fungsi pembaruan.

Saya baru saja memilih serangkaian pengorbanan yang berbeda :p

:+1:

Anda dapat mengirimkan apa saja, dan Anda tidak memiliki petunjuk ketik dalam fungsi pembaruan.

Terima kasih, sepertinya Anda benar. Apakah Anda ingin membicarakan hal ini dengan mereka? Saya bertanya-tanya apakah ini disengaja.

Tolong bantu tinjau pengetikan resmi baru di https://github.com/reactjs/redux/pull/1413.

Berikut adalah kombinasi dari ide-ide di atas type-guards dan nama tipe yang sama dengan string const :

Keuntungannya adalah Anda hanya memerlukan satu fungsi typeguard yang generik dan dapat digunakan di semua reduksi. Tidak yakin apakah itu berfungsi, belum menggunakannya dalam proyek dunia nyata :-).

type Action<TPayload> = {
    type:string,
    payload:TPayload
}

const FOO_ACTION = 'FOO_ACTION';
type FOO_ACTION = { foo:string };
const BAR_ACTION = 'BAR_ACTION';
type BAR_ACTION = { bar:number };

// This is the only type-guard function needed since it is generic
function is<TPayload>(action: Action<any>, actionName:string): action is Action<TPayload> {
    return action.type == actionName;
}

function reducer(state:string, action:Action<any>) {
    if(is<FOO_ACTION>(action, FOO_ACTION)) {
        // Type-safe access to payload
        const x:string = action.payload.foo;
        //...
        return state;
    }
    else if(is<BAR_ACTION>(action, BAR_ACTION)) {
        // Type-safe access to payload
        const x:number = action.payload.bar;
        // ...
        return state;
    }
}

Pendekatan terbaru saya memungkinkan untuk menghilangkan boilerplate untuk menentukan tipe di sepanjang string tipe tindakan:

interface ActionCreator<P> {
  type: string;
  (payload: P): Action<P>;
}

function actionCreator<P>(type: string): ActionCreator<P> {
  return Object.assign(
    (payload: P) => ({type, payload}),
    {type}
  );
}

export function isType<P>(action: Action<any>, 
                          actionCreator: ActionCreator<P>): action is Action<P> {
  return action.type === actionCreator.type;
}

Dengan cara ini saya dapat mengatur tindakan dengan satu pernyataan:

const fooAction = actionCreator<{foo: string }>('FOO_ACTION');

Dan gunakan di reduksi:

const reducer = (state: State, action: Action<any>): State => {
  if (isType(action, fooAction)) {
    // action payload type is inferred to `{foo: string}`
  }

  return state;
}

Saya juga menambahkan pemeriksaan ekstra ke fungsi actionCreator untuk memastikan bahwa jenis tindakan itu unik.

@aikoven Ini sepertinya pendekatan yang menarik. Jadi pada dasarnya fungsi pembuat tindakan bertanggung jawab untuk membawa konstanta string. Jika Anda ingin menggunakan redux-saga atau yang serupa dan memerlukan konstanta string untuk melakukan take(), Anda dapat mengimpor pembuat tindakan foo dan kemudian menggunakan foo.type untuk mendapatkan konstanta string. Satu-satunya hal yang saya tidak mengerti adalah bagaimana Anda melakukan bagian ini:

Saya juga menambahkan pemeriksaan ekstra ke fungsi actionCreator untuk memastikan bahwa jenis tindakan itu unik.

@aikoven

Sunting: String vs. string adalah masalah. Tidak yakin apakah itu bisa diperbaiki.

Mengimpor pembuat aksi ke peredam tampaknya agak tidak intuitif bagi saya. Sangat menyenangkan untuk tidak harus mendefinisikan string konstan sama sekali.

Bagaimana kalau mengetik string konstan dengan generik?

interface ActionType<TPayload> extends String {}

type Action<TPayload> = {
    type: ActionType<TPayload>,
    payload: TPayload
}

interface ActionCreator<P> {
    (payload: P): Action<P>;
}

function actionCreator<TPayload>(type: ActionType<TPayload>): ActionCreator<TPayload> {
    return (payload) => ({
        type,
        payload
    });
}

export function isType<TPayload>(
    action: Action<any>, 
    type: ActionType<TPayload>
): action is Action<TPayload> {
    return action.type === type;
}

Properti tipe dalam tindakan masih berupa string, tetapi diketik menjadi generik dari tipe yang ingin saya jaga.

// In actions.ts
export const FOO: ActionType<{ foo: string }> = 'FOO';

export const createFoo = actionCreator(FOO);

Menggunakannya sangat jelas:

// In reducer.ts
const reducer = (state: State, action: Action<any>): State => {
    if (isType(action, FOO)) {

        let foo:string = action.payload.foo;
    }

    return state;
}

@jonaskello

Jika Anda ingin menggunakan redux-saga atau yang serupa dan memerlukan konstanta string untuk melakukan take(), Anda dapat mengimpor pembuat tindakan foo dan kemudian menggunakan foo.type untuk mendapatkan konstanta string.

Tepat.

Satu-satunya hal yang saya tidak mengerti adalah bagaimana Anda melakukan bagian ini:

Saya juga menambahkan pemeriksaan ekstra ke fungsi actionCreator untuk memastikan bahwa jenis tindakan itu unik.

Sederhana:

const actionTypes = {};

export function actionCreator<P>(type: string): ActionCreator<P> {
  if (actionTypes[type])
    throw new Error(`Duplicate action type: ${type}`);

  actionTypes[type] = true;

  return Object.assign(
  // ...

@geon Itu pintar, meskipun masih melibatkan boilerplate untuk menentukan tipe tindakan. Saya tidak melihat ada kekurangan dalam mengimpor pembuat tindakan ke reduksi. Faktanya, semua pembuat tindakan saya adalah fungsi murni sederhana dan mereka merangkum string jenis tindakan dan jenis muatan tindakan, tidak lebih. Dan keduanya digunakan dalam reduksi, jadi menurut saya baik-baik saja.

@aikoven Terima kasih, itu yang saya kira maksud Anda :-). Btw, bagaimana Anda menangani tindakan yang tidak memiliki muatan? Hanya mengatur jenis muatan ke sembarang sepertinya bukan solusi yang bagus ...

@geon @aikoven @jonaskello Saya tidak yakin apakah solusi Anda menyelesaikan masalah pengetikan nominal yang saya sebutkan di atas. Pada dasarnya pertanyaannya adalah apakah pemeriksaan tipe akan membedakan tindakan dengan bentuk yang sama? Masalahnya adalah ada banyak kasus ketika tindakan dengan tujuan yang sama sekali berbeda memiliki bentuk muatan yang sama.

@igorbt Fungsi type-guard melihat string action.type untuk mengetahui jenis tindakan yang dimiliki. Jadi bentuknya seharusnya tidak masalah.

@jonaskello anda benar! Saya disesatkan oleh kasus penggunaan untuk pengetikan nominal yang diilustrasikan di sini yang tidak cukup relevan untuk Redux. Jadi, terima kasih telah mengarahkan saya ke arah yang benar tentang cara menyederhanakan berbagai hal. Saya tidak memiliki tindakan FSA di aplikasi saya, dan saya juga memiliki banyak tindakan yang hanya mengetik tanpa data, jadi saya mengadaptasi sedikit dari solusi @geon , jadi itu solusi yang lebih umum IMHO:

interface ActionType<TAction> extends String {}

interface Action {
    type: string;
}

interface FooAction extends Action {
    foo: string;
}

interface BarAction extends Action {
    bar: string;
}

function isType<T extends Action>(
    action: Action,
    type: ActionType<T>
): action is T
function isType<T extends Action>(
    action: Action,
    type: string
): action is T
{
    return action.type === type;
}

const FOO: ActionType<FooAction> = 'FOO';
const BAR: ActionType<BarAction> = 'BAR';
const BAZ = 'BAZ';

export const reducer = (state: any, action: Action): any => {
    if (isType(action, FOO)) {

        let foo = action.foo;
    }
    if (isType(action, BAR)) {

        let bar = action.bar;
        action.foo; // error
    }
    if (isType(action, BAZ)) {
        action.type; // BAZ
        action.foo; // error
    }
    return state;
}

@jonaskello

Btw, bagaimana Anda menangani tindakan yang tidak memiliki muatan? Hanya mengatur jenis muatan ke sembarang sepertinya bukan solusi yang bagus ...

Saya menggunakan parameter tipe void untuk mereka, meskipun sampai ini diselesaikan saya harus membuat argumen payload dari pembuat tindakan yang dihasilkan opsional:

interface ActionCreator<P> {
  type: string;
  (payload?: P): Action<P>;  // payload is optional
}

const fooAction = actionCreator<void>('FOO_ACTION');

const action = fooAction();

Namun, ini jelas tidak begitu aman untuk tipe. Pilihan lain adalah melewatkan void 0 sebagai parameter, tetapi saya memilih singkatnya daripada keamanan di sini, karena saya memiliki banyak tindakan tanpa muatan.

@igorbt , solusi elegan. Menghilangkan peretasan pengetikan nominal, kelas yang tidak dibutuhkan, dan risiko tergoda untuk menggunakan instanceof.

Sebagai ide, dimungkinkan untuk menggunakan nama yang sama untuk konstanta ActionType seperti untuk antarmuka tindakan tanpa konflik. Konstanta dan antarmuka mungkin akan ditentukan di tempat yang sama jika aplikasi bersifat modular.

Saya juga berpikir apakah mungkin di masa depan untuk mengganti string dengan Simbol. Seperti berdiri, kita perlu menghasilkan beberapa string unik untuk setiap jenis tindakan, sambil tetap memiliki sesuatu yang dapat dibaca manusia.

@aikoven Terima kasih, melakukannya dengan cara yang sama sekarang melihat proposal untuk mengabaikan kekosongan disetujui

@igorbt @use-strict Saya mulai menggunakan solusi yang diusulkan @geon tetapi memiliki batasan bahwa string tindakan tidak lagi dapat diperlakukan sebagai string . Misalnya. kode ini tidak akan berfungsi:

const FOO: ActionType<FooAction> = 'FOO';
const myString:string = FOO;

Masalahnya terletak pada bahwa di JS ada perbedaan antara String dan string an TS hanya dapat memperpanjang String bukan string . Ini akan menjadi masalah jika Anda ingin mengirim string action.type Anda ke API lain seperti redux-saga. Karena API itu akan mengharapkan string tetapi Anda akan mengirim String . Tentu saja Anda dapat mengetik-cast dalam kasus-kasus itu tetapi pada akhirnya saya menggunakan solusi yang diusulkan @aikoven karena menghindari masalah ini dan juga menghemat satu baris kode untuk setiap tindakan karena string dimasukkan ke dalam fungsi pembuat tindakan itu sendiri.

@use-strict Penggunaan Symbol untuk tindakan telah dibahas berkali-kali dan itu bukan ide yang baik karena Symbol tidak dapat serial. Jadi jika nanti Anda ingin mengirim tindakan Anda ke tempat lain (penyimpanan lokal, http, dll.) itu tidak akan berfungsi. Jadi kita harus menggunakan string. Solusi yang disarankan @aikoven akan bekerja dengan cara yang hampir sama dengan Simbol. Anda merujuk ke fungsi pembuat tindakan di mana saja alih-alih string itu sendiri. Jadi fungsi pencipta tindakan itu sendiri menjadi simbol. Jika mau, Anda dapat memodifikasi fungsi actionCreator() dalam solusinya untuk menghasilkan string unik secara otomatis karena Anda tidak akan pernah merujuk ke string secara langsung dalam kode.

Salah satu bagian dari solusi @aikoven yang sulit saya pahami adalah ini:

interface ActionCreator<P> {
  type: string;
  (payload: P): Action<P>;
}

Untuk memperjelas: Sepertinya tipe ini menentukan objek tetapi sebenarnya menentukan satu fungsi. Dia menggunakan trik yang berfungsi di JS adalah objek. Jadi, Anda dapat melampirkan properti ke suatu fungsi. Jadi tipe ini menentukan fungsi tipe (payload: P) => Action<P> tetapi juga mengatakan bahwa fungsi ini dapat memiliki properti terlampir yang disebut "tipe". Saya menemukan sintaks TS ini cukup funky tetapi berhasil.

Sebenarnya saya pikir ide melampirkan string ke fungsi pembuat tindakan memiliki kelebihan di luar TS. Berikut adalah contoh ES6 sederhana untuk melakukan hal yang sama yang mungkin lebih mudah dipahami (tidak mengujinya tetapi saya pikir itu berhasil :-)). Perhatikan bahwa kita tidak perlu lagi mendeklarasikan konstanta string terpisah karena string dimasukkan ke dalam fungsi pembuat tindakan:

// In actions.js
export function fooAction(foo:string) {
    return {
        foo: foo;
    }
}
fooAction.type = 'FOO';

// In reducer.js
import {fooAction} from './actions';

function(state, action) {
    if(action.type === fooAction.type) {
        // Do foo action stuff
    }
}

Mungkin ini bisa ditambahkan sebagai resep pengurangan boilerplate di dokumen.

@aikoven @jonaskello Saya semakin menyukai solusi Anda. Butuh beberapa waktu untuk memahami kelebihannya, tetapi sekarang saya akan mencobanya. Saya juga akan beralih ke FSA.
@jonaskello saya pikir dalam contoh ES6 Anda, Anda juga harus menggunakan pabrik actionCreator jika tidak maka tidak akan berfungsi.

Setelah bermain dengan saran yang bagus di sini (terutama @jonaskello ) saya menemukan dua kelemahan kecil yang ingin saya atasi:

  • Agak canggung menggunakan pembuat aksi di reduksi. Terasa aneh dengan unta-casing, tidak ada jenis highlighting dan hanya memiliki mereka di sana pada umumnya.
  • Kebutuhan akan pencipta tindakan di tempat pertama. Saya memiliki sebagian besar aplikasi yang digerakkan oleh server di mana banyak tindakan yang dipakai di server dan definisi tipe dihasilkan.

Inilah saran saya:

export class ConnectAction {
    static type = "Connect";
    host: string;
}

// Variant1: less boilerplate to create
export const connect1 = common.createAction(ConnectAction)

// Variatn2: less boilerplate to call
export const connect2 = common.createActionFunc(ConnectAction, (host: string) => ({ host: host}))

Memanggil pembuat tindakan:

connect1({ host: 'localhost'}) 
connect2('localhost');

Peredam:

const reducer = (state: any = {}, action: Action) => {
    if (is(action, ConnectAction)) {
        console.log('We are currently connecting to: ' + action.host);
        return state;
    } else {
        return state;
    }
}

Hal-hal umum/umum:

export interface ActionClass<TAction extends Action> {
    type: string;
    new (): TAction;
}

export interface Action {
    type?: string;
}

export function createAction<TAction extends Action>(actionClass: ActionClass<TAction>): (payload: TAction) => TAction {
    return (arg: TAction) => {
        arg.type = actionClass.type;
        return arg;
    };
}

export function createActionFunc<TAction extends Action, TFactory extends (...args: any[]) => TAction>(
    actionClass: ActionClass<TAction>,
    actionFactory: TFactory): TFactory {
    return <any>((...args: any[]): any => {
        var action = actionFactory(args);
        action.type = actionClass.type;
        return action;
    });
}

export function is<P extends Action>(action: Action, actionClass: ActionClass<P>): action is P {
    return action.type === actionClass.type;
}

Kekurangan dari solusi ini adalah:

  • Penggunaan kelas yang tidak konvensional - mereka tidak pernah dipakai.
  • Harus membuat properti tipe di antarmuka Tindakan opsional (ini sangat kecil).
  • Tidak praktis untuk menambahkan properti opsional ke tindakan karena mereka adalah kelas (harus menambahkan antarmuka tambahan).

Meskipun kelemahan baru, solusi ini berfungsi lebih baik untuk kasus penggunaan saya saat ini. Saya juga tidak menggunakan gaya Tindakan Standar Flux tetapi saya yakin seseorang dapat membuat varian seperti itu dengan saran saya.

Perhatikan bahwa kami telah menambahkan beberapa file pengetikan di 3.5.x.
Jika itu tidak membantu, jangan ragu untuk mengangkat masalah untuk mendiskusikan bagaimana mereka dapat ditingkatkan.

@Cooke lihat https://github.com/reactjs/redux/issues/992#issuecomment -191219795. Saya tidak melihat ada yang salah dengan memiliki pembuat tindakan di reduksi karena mereka hanya merangkum string jenis tindakan dan bentuk muatan. Tentu saja ada juga logika instantiasi tindakan, tetapi tidak ada perbedaan dengan kelas: mereka juga berisi info tipenya sendiri bersama dengan logika konstruksi.

@geon Sehubungan dengan masalah "String vs. string", bagaimana dengan:

export interface _ActionType<T>{}
export type ActionType<T> = string & _ActionType<T>

// important to use _ActionType here
export function isAction<T extends Action>(action: Action, type: _ActionType<T>): action is T;
export function isAction<T extends Action>(action: Action, type: string): action is T {
    return action.type === type
}

Anda dapat menggunakan ini seperti sebelumnya tetapi ini akan menjadi string biasa dan bukan String.

Pendekatan ini bekerja dengan baik...

1) Dapatkan tindakan dan muatan yang diketik untuk dikirim.
2) Redux mendapatkan objek polos yang bersih
3) Reducer mendapatkan tipe tindakan yang diketik, dan muatan yang diketik.

Pengaturan pertama enum..

export enum ActionTypes { TYPE_SELECTED }

create your commands...

export class ActionSelectedCommand  {
  constructor(public actionitem: any) {
  }
}

Buat fungsi Anda untuk dipanggil saat mengirim (perhatikan saya membuat objek standar yang dalam arti tertentu membuat serial objek perintah saya).

Jadi setiap tindakan akan selalu memiliki jenis dan muatan.

export function ActionSelected(actionItem): any {
  return {
    type: ActionTypes.TYPE_SELECTED,
    payload: JSON.stringify(new ActionSelectedCommand(actionItem))
  };
}

Kemudian pada peredam Anda ...

  if (!state)
    state = new NewStore();

  let type: actions.ActionTypes = <actions.ActionTypes>action.type;  -- constant action type :)

  switch (type) {
    case actions.ActionTypes.TYPE_SELECTED: {  -- more constact action type
      var warmAndFuzzyPayLoad = <actions.ActionSelectedCommand>JSON.parse(action.payload)
      -- Then you deserialize back to your action command with all your parameters type set.
      return state;
    }
    default:
      return state;
  }

Lalu kirim...

store.dispatch(actions.ActionSelected("Tight Actions"));

Sejauh ini tampaknya bekerja dengan baik.

Ini memaksa untuk menjaga perintah tindakan sangat sederhana karena setiap rekursi atau referensi diri akan meledakkan ini.

Saya suka gagasan menjaga jenis toko redux gratis. Hal ini memungkinkan kemampuan untuk memiliki fleksibilitas masuk dan keluar dari bus.

Bersulang

Saya pikir jenis serikat pekerja yang

Dukungan tindakan impian saya akan menjadi sesuatu seperti elm atau scala.js di mana peredam didukung oleh pencocokan pola. Tapi sepertinya TypeScript tidak akan memiliki pencocokan pola dalam waktu dekat :(

Versi lain dengan objek biasa di peredam, terutama berdasarkan @Cooke.
Ini memungkinkan untuk memasukkan bidang muatan tindakan dan menghilangkan definisi Tindakan eksplisit.
ActionTypes di sini digunakan sebagai pembuat tindakan generik.

export interface Action {
    type?: string;
}
export class ActionType<T extends Action>{
    type: string;
    constructor(type: string) {
        this.type=type;
    }
    new(t: T):T {
        t.type=this.type;
        return t;
    }
}

export function isAction<T extends Action>(action: Action, type: ActionType<T>|string): action is T {
    if (typeof type === 'string') {
        return action.type==type;
    } else if (type instanceof ActionType) {
        return action.type === type.type;
    }
    return false;
}

// here come action creators
const FOO: ActionType<{foo: string, foo2: string}> = new ActionType<any>('FOO');
interface BarAction extends Action {
    bar: string;
}
const BAR: ActionType<BarAction> = new ActionType<BarAction>('BAR');

di peredam:

export default function reducer(state = [], action:Action) {
    if (isAction(action, FOO)) {
        action.foo // have access
        return state;
    } else if (isAction(action, BAR)) {
        action.bar;
        return state;
    } else
        return state;
}

buat aksi baru seperti ini

FOO.new({foo: 'foo'}) // error, foo2 is required
FOO.new({foo: 'foo', foo2: 'foo'}) // OK

// dispatch it like this
dispatch(FOO.new({foo: 'foo', foo2: 'foo'}));

Anda memiliki jenis muatan aman dan tidak harus menggunakan pembuat tugas yang ditunjuk.
Anda masih dapat membuat tindakan dengan cara lama. Kelemahan utama adalah tipe itu tidak diperlukan dalam antarmuka Action.

Ini adalah versi baru berdasarkan proposal dari @aikoven , @jonaskello dan terinspirasi dari redux-actions .
Tujuannya adalah untuk mengatasi skenario khusus ini:

  1. Ketik keamanan untuk status, tindakan, dan muatan
  2. Izinkan berbagi tipe status aman dan variabel lokal untuk semua penangan di dalam fungsi peredam
  3. Pertahankan sintaks fungsi pembuat tindakan standar untuk memungkinkan logika, thunks, dll.
  4. Kurangi boilerplate switch-case di reduksi

Ini dia, beri tahu saya jika Anda melihat kekurangan utama!

[EDIT: Memfaktorkan ulang dan mengganti nama fungsi HandleActions sesuai komentar

// FSA Compliant action
interface Action<P> {
    type: string,
    payload?: P,
    error?: boolean,
    meta?: any
}

interface Todo {
    text: string
}

type HandlerMap<T> = {
    [type: string]: { (action: Action<any>): T }
}

// Handle actions "inspired" by redux-actions
// Only pass the action parameter to handlers, allowing to share type-safe state in the reducer
export const DEFAULT = "DEFAULT";
    export function handleAction<T>(
    action: Action<any>,
    handlers: HandlerMap<T>,
    defaultState: T): T {
    // As improved by <strong i="18">@alexfoxgill</strong>
    const handler = handlers[action.type] || handlers[DEFAULT];
    // Execute the function if exists passing the action and return its value.
    // Otherwise return the Default state. 
    return handler ? handler(action) : defaultState;
}

    // If no handlers matched the type, execute the DEFAULT handler 
    if (handlers[DEFAULT]) {
        return handlers[DEFAULT](action);
    }

    // If DEFAULT not declared, return the state from the parameter
    return defaultState;
}

export const ADD_TODO = "ADD_TODO";
// Declare the type for the payload
export type ADD_TODO = Action<string>
// Standard action creator pattern with type safe return value
export const addTodo = (text: string): ADD_TODO => {
    // Allows for some complex logic, async behavior, thunks, etc.

    return {
        type: ADD_TODO,
        payload: text,
        meta: {
            foo: "Bar"
        }
    }
}

export const DELETE_TODO = "DELETE_TODO";
export type DELETE_TODO = Action<number>
export const deleteTodo = (index: number): DELETE_TODO => {
    // Some complex logic, async behavior, thunks, etc.

    return {
        type: DELETE_TODO,
        payload: index,
    }
}

// Standard pattern for creating reducers
// Type safety for state and return value, shared for all handlers
export const todoReducer = function (
    state: Todo[] = [],
    action: Action<any>): Todo[] {
    // Local variables can be declared and shared by all handlers
    let foo = action.meta && action.meta.foo;

    // Type safety for return value of state
    return handleActions<Todo[]>(action, {
        // Less boilerplate by removing the 'return' and 'break' statements
        // Payload is a string 
        [ADD_TODO]: ({payload}: ADD_TODO) => [...state, { text: payload }],
        // Payload is a number
        [DELETE_TODO]: ({payload}: DELETE_TODO) =>
            [...state.slice(0, payload), ...state.slice(payload + 1)],
        // For more complex actions, expand function
        [DEFAULT]: ({payload}) => {
            // Can handle other operations that don't depend on type
            if (foo) {
                console.log(foo);
            };
            return state;
        }
    },
        // In case DEFAULT is not implemented, return the default state 
        state);
}

// Dispatch in the context of a connected component
addTodo("Foo");
deleteTodo(0);

@Agalla81 Saya suka desain ini. Saya bingung tentang implementasi handleActions - untuk apa loop itu? Bukankah yang berikut ini akan berfungsi:

type HandlerMap<T> = {
    [type: string]: { (action: Action<any>): T }
}

export function handleAction<T>(action: Action<any>, handlers: HandlerMap<T>, defaultState: T): T {    
    const handler = handlers[action.type] || handlers[DEFAULT];
    return handler ? handler(action) : defaultState;
}

Anda juga dapat mendefinisikan fungsi pembantu untuk pola ini seperti:

type HandlerMapWithState<T> = {
    [type: string]: { (action: Action<any>): (state: T) => T }
}

export function createReducer<T>(defaultState: T, handlers: HandlerMapWithState<T>) {
    return (state: T = defaultState, action: Action<any>): T => {
        const handler = handlers[action.type] || handlers[DEFAULT];
        return handler ? handler(action)(state) : state;
    };
}

Yang memungkinkan Anda membuat peredam dengan sintaks ini:

const reducer = createReducer(0, {
    INCREMENT: (a: INCREMENT) => s => s + a.payload.incBy,
    DECREMENT: (a: DECREMENT) => s => s - a.payload.decBy
});

@alexfoxgill Anda benar, loop tidak diperlukan. Ini telah diperbarui berdasarkan saran Anda untuk implementasi handleAction, terima kasih telah memperhatikan.

Saya suka pendekatan createReducer tetapi mungkin bertabrakan dengan salah satu item yang ingin saya tangani:
"2. Izinkan berbagi status aman tipe dan _variabel lokal untuk semua penangan di dalam fungsi peredam_".

  • Jenis Status aman memang dibagi dengan fungsi panah "ganda" "=> s =>".
  • Tetapi bagaimana variabel lokal yang dibagikan dapat ditentukan saat menggunakan createReducer?
    Dalam beberapa skenario, saya merasa berguna untuk mendefinisikannya jika ada logika umum yang dapat digunakan kembali oleh beberapa penangan tindakan di dalam peredam.

@Agalla81 Saya kira itu poin yang adil, meskipun saya belum menemukan waktu ketika saya membutuhkan logika umum dalam definisi peredam itu sendiri (biasanya ada di fungsi lain)

@alexfoxgill inilah contoh dari dokumentasi Redux yang menunjukkan skenario kasus seperti itu, dengan mendeklarasikan variabel lokal "posting" yang dapat digunakan kembali dalam penangan tindakan yang berbeda.
Sumber: http://redux.js.org/docs/recipes/reducers/UpdatingNormalizedData.html

// reducers/posts.js
function addComment(state, action) {
    const {payload} = action;
    const {postId, commentId} = payload;

    // Look up the correct post, to simplify the rest of the code
    const post = state[postId];

    return {
        ...state,
        // Update our Post object with a new "comments" array
        [postId] : {
             ...post,
             comments : post.comments.concat(commentId)             
        }
    };
}

Baru saja menerbitkan paket yang mengimplementasikan pendekatan saya di atas:
https://github.com/aikoven/redux-typescript-actions

Saya menemukan utas ini ketika mencoba menggunakan kelas konkret untuk tindakan seperti @nickknw untuk menegakkan keamanan tipe dan redux secara wajar meneriaki saya karena tidak menggunakan objek biasa.

Saya menyadari bahwa dengan TypeScript 2.0, firasat @frankwallis telah menjadi kenyataan, jadi saya pikir saya akan memperbarui dengan contoh untuk siapa pun yang berakhir di sini.

/* define the waxing actions and create a type alias which can be any of them */
interface WaxOn
{
    type: "WaxOn";
    id: number;
    waxBrand: string;
}

function createWaxOnAction(id: number, waxBrand: string): WaxOn
{
    return { type: "WaxOn", id, waxBrand };
}

interface WaxOff
{
    type: "WaxOff";
    id: number;
    spongeBrand: string;
}

function createWaxOffAction(id: number, spongeBrand: string): WaxOff
{
    return { type: "WaxOff", id, spongeBrand };
}

type AnyWaxingAction = WaxOn | WaxOff;

/* define the dodging actions and create a type alias which can be any of them */
interface Bob
{
    type: "Bob";
    id: number;
    bobDirection: number[];
}

function createBobAction(id: number, bobDirection: number[]): Bob
{
    return { type: "Bob", id, bobDirection };
}

interface Weave
{
    type: "Weave";
    id: number;
    weaveDirection: number[];
}

function createWeaveAction(id: number, weaveDirection: number[]): Weave
{
    return { type: "Weave", id, weaveDirection };
}

type AnyDodgeAction = Bob| Weave;

type AnyActionAtAll = AnyWaxingAction | AnyDodgeAction;
// action must implement one of the Action interfaces or tsc will yell at you
const reducer = (state: State = {}, action: AnyActionAtAll) =>
{
    switch(action.type)
    {
        case "WaxOn":
            // Will only let you access id and waxBrand
        case "WaxOff":
            // Will only let you access id and spongeBrand
        case "Bob":
            // Will only let you access id and bobDirection
        case "Weave":
            // Will only let you access id and weaveDirection
        default:
            // Make sure there aren't any actions not being handled here
            // that are included in the union
            const _exhaustiveCheck: never = action;
            return state;
    }
}

Jika Anda secara tidak sengaja menyebutkan dua tindakan dengan tipe string yang sama, TypeScript tidak akan selalu meneriaki Anda, tetapi dalam kasus sakelar untuk string itu, TypeScript hanya akan memungkinkan Anda mengakses anggota yang _keduanya_ antarmukanya memiliki kesamaan.

@awesson Anda mungkin ingin menegaskan bahwa tindakan bertipe never dalam kasus default dalam pernyataan switch. Dengan cara ini Anda dapat yakin bahwa semua tindakan ditangani dan Anda tidak lupa untuk menambahkan lebih banyak case s ke sakelar jika Anda membuat tindakan baru. Lihat halaman ini di bawah bagian "Pemeriksaan Lengkap" untuk info lebih lanjut.

Terima kasih @jonaskello. Itu poin yang bagus.

Saya memperbarui contoh saya, tetapi sebenarnya mungkin lebih baik melihat contoh yang Anda tautkan. (Saya tidak tahu bagaimana saya melewatkannya.)

Satu catatan tentang pemeriksaan lengkap adalah bahwa jika Anda baru mulai menulis tindakan untuk modul baru mengikuti organisasi ini dan sejauh ini Anda hanya memiliki 1 tindakan, alias tipe tidak akan menjadi gabungan dan pemeriksaan lengkap akan selalu gagal.

Saya telah mencari solusi yang berbeda untuk dukungan pengetikan penuh (status dan tindakan), dan saya telah menemukan https://Gist.github.com/japsu/d33f2b210f41de0e24ae47cf8db6a5df + solusi @awesson menjadi kombinasi yang sangat bagus

Setelah bermain-main dengan alternatif yang berbeda, saya menyelesaikan dengan pendekatan berikut https://github.com/Cooke/redux-ts-simple

Anda dapat menggunakan tipe gabungan tetapi Anda juga dapat menggunakan objek tindakan yang diketik besar.

// Before

{
    type: "UserLoaded",
    payload: {
        user: { id: 4 }
    }
}

// After

{
    type: "UserLoaded",
    userLoaded: {
        user: { id: 4 }
    }
}

Dalam kasus pertama, ada banyak objek tindakan yang berbeda sehingga Anda memerlukan tipe gabungan.

interface UserLoaded {
    type: "UserLoaded",        // redundant
    payload: {
        user: User;
    };
}

type MyAction = UserLoaded | OtherAction

Dalam kasus kedua, hanya ada satu jenis tindakan dengan banyak kunci muatan.

interface UserLoaded {
    user: User;
}

interface MyAction {
    type: string;
    userLoaded?: UserLoaded;
    otherAction?: OtherAction;
}

Anda dapat mengurangi dengan mencocokkan type dengan string.

function userById(state = {}, action: MyAction) {
    if (action.type == "UserLoaded") {
        // action.userLoaded.user
    }
}

Anda juga dapat memeriksa keberadaan kunci payload.

function userById(state = {}, action: MyAction) {
    if (action.userLoaded) {
        // action.userLoaded.user
    }
}

Jika Anda menggunakan kehadiran, Anda tidak lagi bergantung pada type untuk pengurangan aktual sehingga Anda tidak perlu khawatir jika seseorang menggunakan nama yang sama di bagian lain dari proyek Anda. Anda juga dapat menumpuk muatan tindakan yang terkait dengan widget atau domain tertentu di dalam objek tindakan induk yang menyertakan konteks.

enum Field {
    Title,
    Body,
}

interface TextChange {
    field: Field;
    newText: string;
}

interface Submit {

}

interface NewPostForm {
    pageId: string;
    textChange?: TextChange;
    submit?: Submit;
}

interface MyAction {
    type: string;
    newPostForm?: NewPostForm;
}

Jika Anda benar-benar menyukai tindakan penyatuan dari sub modul, Anda masih dapat menggunakan pewarisan antarmuka.

interface UserLoaded {
    user: User;
}

interface UserAction {
    userLoaded?: UserLoaded;
}

interface MyAction extends UserAction {
    type: string;
}

Jadi ya, ini berfungsi baik dengan serialisasi JSON, alat redux yang ada, dan ini adalah keseimbangan yang bagus antara merangkum beberapa bagian sambil tetap memungkinkan tindakan lintas sektoral.

Saya kira semua orang telah menyebutkan sebagian besar solusi. Dua sen saya tentang topik ini, saya hanya ingin menekankan tentang pendekatan @aikoven . Meskipun saya baru saja menemukan topik ini, pendekatan lib saya sangat dekat dengannya.

Satu- satunya perbedaan pendekatan saya dibandingkan dengan @aikoven adalah kami melacak tindakan kami dengan ClassAction.is(..) / ClassAction.type alih-alih isType(..) dalam reduksi atau epos/efek samping.

Di sini bagaimana saya melakukannya, saya mencoba untuk menghapus semua boilerplate sebanyak mungkin:

fitur-x.actions.ts

import { defineAction, defineSymbolAction } from 'redux-typed-actions';
import { ItemX } from './feature-x.types';

// For this action we don't have any payload
export const FeatureXLoadAction = defineAction('[Feature X] Load');

// Let's have a payload, this action will carry a payload with an array of ItemX type
export const FeatureXLoadSuccessAction = defineAction<ItemX[]>('[Feature X] Load Success');

// Let's have a symbol action
export const FeatureXDummySymbolAction = defineSymbolAction<ItemX[]>('[Feature X] Dummy Started');

fitur-x.component.ts

import { ItemX } from './feature-x.types';
import { FeatureXLoadAction, FeatureXLoadSuccessAction } from '../feature-x.actions';
...
store.dispatch(FeatureXLoadAction.get());

// or in epics or side effects
const payload: ItemX[] = [{ title: 'item 1' }, { title: 'item 2' }];
store.dispatch(FeatureXLoadSuccessAction.get(payload));
// And a more strict check for payload param
store.dispatch(FeatureXLoadSuccessAction.strictGet(payload));

fitur-x.reducers.ts

import { PlainAction } from 'redux-typed-actions';
import { ItemX } from './feature-x.types';
import { FeatureXLoadAction, FeatureXLoadSuccessAction } from '../feature-x.actions';

export interface ItemXState {
  items: ItemX[];
  loading: boolean;
}

...
export function reducer(state: ItemXState = InitialState, action: PlainAction): ItemXState {
  if (FeatureXLoadAction.is(action)) { // Visually helps developer to keep track of actions
    // Within this branch our action variable has the right typings
    return {
      ...state,
      loading: true,
    }

  } else if (FeatureXLoadSuccessAction.is(action)) {
    return {
      ...state,
      loading: false,
      items: action.payload, // <- Here we are checking types strongly :)
    }

  } else {
    return state;
  }
}

feature-x.epics.ts (terkait

...
class epics {
   loadEpic(): Epic<PlainAction, AppState> {
    return (action$, _store) => action$
      .ofType(FeatureXLoadAction.type)
      .switchMap(() =>
        this.service.getRepositories()
          .map(repos => StoreService.transformToItem(repos))
          .delay(2000)
          .map(repos => FeatureXLoadSuccessAction.strictGet(repos))
          .catch((error) => Observable.of(FeatureXLoadFailedAction.strictGet(`Oops something went wrong:\n\r${error._body}`))));
}

Berikut contoh langsung

@jonaskello saya suka komentar Anda tentang simbol. Anda menyebutkan:
If you want, you could modify the actionCreator() function in his solution to generate unique strings automatically since you will never refer to the strings directly in the code.
jika saya tidak salah devtools akan menampilkan string ini? kita masih perlu menghasilkan string unik yang masuk akal terkait dengan tindakan nyata dan bukan hanya uid.

Jadi saya pikir apa yang @aikoven usulkan untuk tidak memiliki masalah ini tidak terlalu buruk.

export function actionCreator<P>(type: string): ActionCreator<P> {
 if (actionTypes[type])
    throw new Error('Duplicate action type: ${type}');
...

@nasreddineskandrani Atau solusi yang lebih baik adalah dengan memeriksa process.env.NODE_ENV dan jika development peringatkan pengembang dan jika itu produksi, buat saja hash acak dan lampirkan ke nilai jenis tindakan akhir untuk dibuat itu unik. Dengan cara ini tidak akan pernah gagal dalam prod di seluruh basis kode yang besar.

@aminpaks saya pikir devtool dengan nama tindakan nyata di prod berguna.
https://medium.com/@zalmoxis/using -redux-devtools-in-production-4c5b56c5600f
contoh: What if we want the end-users to help the debugging

@nasreddineskandrani Saya tidak pernah mengatakan kita harus menghapus jenis tindakan awal. Maksud saya mari kita tambahkan hash di akhir sehingga kita akan memiliki tindakan yang unik.

salah satu dev di utas ini menyebutkan ini:
.... I have a mostly server driven application where many actions are instantiated on the server and the type definitions are generated.
dengan strategi tipe dinamis, pertimbangkan bahwa lib Anda membutuhkan hash yang sama persis di 2 server berbeda untuk tindakan tertentu dan bukan yang acak.

Saya baru-baru ini menambahkan contoh React menggunakan tindakan yang diketik redux untuk menunjukkan bagaimana strategi ini dapat membantu meningkatkan produktivitas dan akan menghapus semua boilerplate.

Lihat di sini .

Apakah halaman ini membantu?
0 / 5 - 0 peringkat