Comportament inconsistent al TypeScript cu tipuri union și obiect


Să avem următorul fragment:

interface A {
  isEmpty(x: any[] | string | object): boolean;
  isEmptyObject(x: object): boolean;
}

const a: A = <A>{};

a.isEmptyObject({});
a.isEmptyObject({a: 1});

a.isEmpty([]);
a.isEmpty('');
a.isEmpty({});
a.isEmpty({a: 1});

Când încerc să îl compilez, se blochează cu asta:

a.ts(14,12): error TS2345: Argument of type '{ a: number; }' is not assignable to parameter of type 'string | object | any[]'.
  Object literal may only specify known properties, and 'a' does not exist in type 'string | object | any[]'.

Cum este posibil acest lucru, când tipul union (string | object | any[]) conține tipul object pentru care funcționează (apelul isEmptyObject cu {a: 1} este în regulă)?

Întrebarea principală: Este aceasta o eroare în TypeScript sau cum ar trebui să scrii că acel input poate fi un array de orice valoare sau string sau obiect (dar nu orice primitiv, nu vreau any sau Object)?

Testat cu TypeScript 2.3.4.

Răspuns #1

Aceasta nu este o eroare, ci o caracteristică pentru a suporta API-uri care utilizează obiecte ca argumente. Luați, de exemplu, funcția ajax din jQuery. Aceasta preia un obiect ca și argument.

interface JQueryAjaxParam {
    url: string;
    data?: any;
}

...

$.ajax({
    url: "",
    datta: "" // notice the typo here
});

Vrei să ai o eroare de compilare în acest caz, chiar dacă {url:"",datta:""} implementează interfața param. Pentru a depăși acest lucru, au adăugat verificări de proprietăți în exces pentru literaluri de obiect ca parametri direcți. Acest lucru este benefic, oferă mai multă încredere în verificatorul de tipuri și cauzează erori false doar atunci când oamenii încearcă să facă teste rapide ca al tău. Trebuie să ții cont de această regulă, acesta este prețul siguranței suplimentare.

ACTUALIZARE: TypeScript 2.4 introduce suplimentar tipuri slabe

Deci atunci există funcții cu această semnătură:

function isEmptyObject(x : object) : void;

Aici, intenția este clară, această funcție își propune să accepte orice obiect cu orice chei. O funcție care acceptă doar obiecte goale are puțin folos. Așa că au adăugat o excepție: atunci când parametrul este exact de tipul object, atunci verificarea anterioară este omisă.

Deci ce să faci? Nimic. În utilizările reale, funcția ta va trece verificarea de tipuri fără probleme. Poți să încerci:

interface A {
  isEmpty(x: any[] | string | object): boolean;
  isEmptyObject(x: object): boolean;
}

const a: A = <A>{};
const param = {a: 1};

a.isEmptyObject({});
a.isEmptyObject(param);

a.isEmpty([]);
a.isEmpty('');
a.isEmpty({});
a.isEmpty(param);

ACTUALIZARE

Exemplul tău este un caz de utilizare real, vrei să funcționeze chiar și cu litere. După Nitzan, și eu sugerez să folosești supraincarcare a funcțiilor pentru asta.

interface A {
    isEmpty(x: any[]): boolean;
    isEmpty(x: string): boolean;
    isEmpty(x: object): boolean;
    isEmptyObject(x: object): boolean;
}

const a: A = <A>{};
a.isEmptyObject({b: 6});
a.isEmptyObject({a: 1});

a.isEmpty([]);
a.isEmpty('');
a.isEmpty({});
a.isEmpty({a: 1});

Răspunsul #2

Poți să eviți cu ușurință această problemă adăugând o altă semnătură:

interface A {
    isEmpty(x: any[]): boolean;
    isEmpty(x: string | object): boolean;
    isEmptyObject(x: object): boolean;
}

a.isEmpty({ a: 1 }); // fine now

(cod în teren de joacă)

Editează

Nu cred că este un bug, dacă ceva este un bug, atunci mesajul de eroare este cel care nu este foarte util.

Ceea ce cred că se întâmplă este că poți folosi object într-o uniune cu orice tipuri primitive fără probleme (în cazul tău), dar odată ce introduci un tip non-primar, vei primi eroarea, de exemplu:

isEmpty(x: string | object | Map<string, string>): boolean;

Va rezulta în aceeași eroare.
Uniunea dintre object (care este orice nu este primitiv) și un alt tip non-primar probabil rezultă într-un tip care nu poate fi potrivit și compilatorul se încurcă.

Ești binevenit să deschizi o nouă problemă în acest caz, dacă o faci te rog să împărtășești linkul aici, aș dori să văd ce spun.