reactjs - Compilatorul Typescript nu aplică genericele


Întrebare

Am un model de reducer cu o acțiune care acceptă un payload care se așteaptă la structura { id: string, newValues: Partial<A_or_B> }. În codul de mai jos, respectarea tipului A_or_B nu este impusă atunci când declar newValues direct; totuși, dacă declar newValues din timp și ii atribui proprietăți, constrângerile sunt aplicate. Cum pot face să fie respectate constrângerile pentru A_or_B chiar și atunci când newValues este declarat direct?

Cod problematic

import { useContext } from "react";
import { AppDispatchContext } from "../src/AppProvider";
import { ActionTypes, A_or_B } from "./interfaces";

export default function App() {
  const dispatch = useContext(AppDispatchContext);

  const x: Partial<A_or_B> = {};
  // Property 'color' does not exist on type
  // 'Partial<A> | Partial<B>'.
  // Property 'color' does not exist on type 'Partial<A>'.
  x.color = "red";

  dispatch({
    type: ActionTypes.POINT_UPDATED,
    // why can I assign a color to Partial<A>|Partial<B> if 'color'
    // only exists on B?
    payload: { id: "x", newValues: { color: "red" } }
  });
}

Link CodeSandbox

Link CodeSandbox Aici

Răspuns #1

Aceasta este o problemă comună pe care majoritatea oamenilor o experimentează cu tipurile union în TypeScript. Unii le tratează ca și cum ar fi tipuri de intersecție, în timp ce câțiva doresc ca acestea să se comporte mai mult ca tipuri mutual exclusive.

Totuși, TypeScript are două interpretări diferite ale tipurilor de uniune, în funcție de cazul tău de utilizare.

Proprietăți

Când încerci să accesați proprietatea unui tip de uniune, poți accesa doar proprietățile care sunt comune tuturor membrilor uniunii. O altă modalitate de a spune acest lucru este că ai acces doar la intersecția proprietăților membrilor uniunii.

Poate fi greu de pronunțat, dar documentația TypeScript oferă o analogie frumoasă pentru a explica acest lucru:

De exemplu, dacă am avea o cameră plină cu persoane înalte care poartă pălării și o altă cameră cu vorbitori de spaniolă care poartă pălării, după ce am combina acele camere, singurul lucru pe care îl știm despre fiecare persoană este că trebuie să poarte o pălărie.

Deci, în cazul accesului la proprietăți, TypeScript permite doar unui subset de proprietăți să fie accesibile (fără alte indicații suplimentare din partea utilizatorului), formând astfel o intersecție.

Membru (cunoscut și ca inferență de tip)

Când vine vorba de inferența tipurilor, compilatorul este destul de liberal cu ceea ce permite să se potrivească unui tip union. Practic, spune dacă vrei să faci parte din acest grup, trebuie doar să arăți ca unul dintre membrii acestui grup. În acest caz, nu trebuie să ai o proprietate care să corespundă proprietății fiecărui membru al uniunii.


Poate că soluția problemei tale este să folosești pur și simplu tipul de intersecție pentru x:

const x: Partial<A&B> = {};
x.color = "red"; // works!

...
newValues: x; // also works

Aceasta rezolvă problema atât a accesului la proprietăți, cât și a apartenenței

Răspunsul #2

Constrângerile sunt, de fapt, aplicate în payload-ul tău. Poți verifica acest lucru adăugând o proprietate în payload-ul tău care nu există în tipul A sau B.

Problema reală aici este inferarea tipului.

  const x: Partial<A_or_B> = {};
  // Property 'color' does not exist on type
  // 'Partial<A> | Partial<B>'.
  // Property 'color' does not exist on type 'Partial<A>'.
  x.color = "red";

x primește o valoare validă de tip uniune, dar TypeScript nu poate determina dacă este de tip A sau B, așa că x.color nu poate fi verificat deoarece TS consideră că există posibilitatea să fie de tip B.

payload: { id: "x", newValues: { color: "red" } }

newValues este setat explicit ca un obiect care conține doar color, ceea ce înseamnă că Typescript știe că acesta trebuie să fie de tipul A.

Exemplu