Tipul de intersecție Typescript și semnătura funcției nu aruncă eroarea așteptată
Am declarat următoarele tipuri:
type ExampleA = {
a: string;
}
type ExampleB = {
b: number;
}
type ExampleC = {
c: boolean;
}
type Examples = ExampleA &
ExampleB &
ExampleC;
Apoi folosesc tipul după cum urmează:
function foo(pattern: { [key: string]: string }) {
console.log(pattern);
}
const bar: Examples = { a: 'foo', b: 1, c: false };
foo(bar);
Compilatorul TypeScript nu generează nicio eroare la invocarea metodei foo(bar), chiar dacă variabila bar:Examples nu corespunde semnăturii funcției foo.
Link către terenul de joacă
De ce TypeScript nu aruncă nicio eroare? Este aceasta o eroare în compilator?
Răspuns #1
Motivul pentru care acest lucru funcționează este capacitatea unui tip de intersecție de a fi atribuit tipurilor sale de bază.
Ca tip de intersecție, Examples poate fi atribuit lui ExampleA. ExampleA poate fi atribuit lui { [key: string]: string }. Prin urmare, Examples trebuie să poată fi atribuit tipului parametrului funcției
Acest lucru poate fi arătat în acest cod:
const bar: Examples = { a: 'foo', b: 1, c: false };
const bar2: ExampleA = bar;
const bar3: { [key: string]: string } = bar2;
foo(bar3); //This works
foo(bar2); //Since the assignment bar3 = bar2 works, this must work, too
foo(bar); //Since the assignment bar2 = bar works, this must work, too
Versiune de teren de joacă
ACTUALIZARE
Comportamentul are consecințe atunci când vrei să menții principiul „când A poate fi atribuit lui B și B poate fi atribuit lui C, atunci A trebuie să poată fi atribuit lui C”. Sistemul de tipuri nu are altă opțiune decât să permită acest tip de atribuiri. Totuși, există de fapt o altă problemă în transmiterea valorii ca parametru către foo.
Poți atribui o valoare unei variabile de un tip care împărtășește doar o parte din membrii valorii atribuite. Deci această atribuire funcționează bine:
let item: { a: string, b: number } = { a: "Hello World!", b: 1 };
let partiallyMatchingItem: { a: string } = item;
Nu este absolut nicio problemă că partiallyMatchingItem are mai multe proprietăți decât cele declarate efectiv în tip. Garanția este o garanție minimă.
Atribuirea unui tip mapat, totuși, nu funcționează, deoarece membrul suplimentar al lui item de tip number:
let item = { a: "Hello World!", b: 1 };
let mappedTypeItem: { [key: string]: string } = item; //Error
Deci, garanția de data aceasta nu este o garanție minimă, ci o garanție absolută. Și asta este destul de ridicol, dacă te gândești cât de ușor poți să o ocolești (intenționat sau din greșeală):
let item = { a: "Hello World!", b: 1 };
let partiallyMatchingItem: { a: string } = item;
let mappedTypeItem: { [key: string]: string } = partiallyMatchingItem;
Sau pur și simplu:
let item = { a: "Hello World!", b: 1 };
let mappedTypeItem: { [key: string]: string } = item as { a: string };
Aceasta este o eroare care așteaptă să se întâmple, mai ales atunci când parcurgi proprietățile lui mappedTypeItem și presupui că valorile tuturor proprietăților sunt un string.
Având în vedere cât de comune sunt atribuțiile de tip structural în TypeScript, această garanție absolută nu se încadrează în sistemul de garanții minime oferite în general de sistemul de tipuri.
O soluție curată ar fi să faceți ca valorile tipurilor „regulate” să nu poată fi atribuite tipurilor mapate (dacă este necesară compatibilitatea cu versiunile anterioare, ați putea să activați sau să dezactivați această opțiune cu un comutator în fișierul tsconfig.json). Cel puțin, probabil ar trebui să evitați acest tip de atribuiri, deoarece siguranța tipului oferită aici este destul de slabă.
Răspunsul #2
Dacă vrei cu adevărat să ai eroarea, poți declara Example ca interfață, nu ca tip de intersecție. Începând cu 2.2, interfața poate extinde tipul obiect (sau chiar tipul de intersecție)
type ExampleA = {
a: string;
}
type ExampleB = {
b: number;
}
type ExampleC = {
c: boolean;
}
interface Examples extends ExampleA, ExampleB, ExampleC {
}
function foo(pattern: { [key: string]: string }) {
console.log(pattern);
}
const bar: Examples = { a: 'foo', b: 1, c: false };
foo(bar); // error
Sau chiar în acest fel, pentru a ilustra mai bine diferența dintre tipurile de interfață și tipurile de intersecție:
type Examples = ExampleA & // the same as in question
ExampleB &
ExampleC;
interface IExamples extends Examples { // empty interface "collapses" the intersection
}
const bar1: Examples = { a: 'foo', b: 1, c: false };
foo(bar1); // no error
const bar2: IExamples = { a: 'foo', b: 1, c: false };
foo(bar2); // error
O altă modalitate de a construi un tip de obiect din intersecție, așa cum a sugerat Titian în comentariu, este să folosești un tip mapat care este aproape, dar nu chiar, identic cu parametrul său generic:
type Id<T> = { [P in keyof T]: T[P] }
const bar3: Id<ExampleA & ExampleB & ExampleC> = { a: 'foo', b: 1, c: false };
foo(bar3); // error
