typescript - Predicatul de tip inferat nu funcționează la filtrarea obiectelor dacă tipul este setat în mod explicit
Poate cineva să explice de ce predicatele de tip inferate nu funcționează pe un șir de obiecte dacă tipul este setat explicit?
Conform acestui anunț, din versiunea 5.5 înainte, TypeScript poate deduce noul tip atunci când un array este filtrat.
De exemplu:
const arr0 = [ 1, 2, 3, undefined, 4, 5, undefined];
// ^? const arr0: (number | undefined)[]
const numsOnly0 = arr0.filter(n => typeof n === "number");
// ^? const numsOnly0: number[]
În acest caz simplu, funcționează de asemenea dacă array-ul este tipizat în mod explicit:
type NumOrUndefined = number | undefined;
const arr1:NumOrUndefined[] = [ 1, 2, 3, undefined, 4, 5, undefined];
// ^? const arr1: NumOrUndefined[]
const numsOnly1 = arr1.filter(n => typeof n === "number");
// ^? const numsOnly0: number[]
TypeScript știe că, după filtrare, tipul anterior NumOrUndefined[] nu este suficient de precis și deduce number[].
Am fost entuziasmat să văd această nouă funcționalitate în acțiune într-o bază de cod reală. Ceea ce am descoperit este că această inferență nu pare să funcționeze pentru obiectele care sunt tipizate explicit.
De exemplu:
type OriginalType = { id: number | undefined; name: string | undefined }
const objArr1: OriginalType[] = [
// ^? const objArr1: OriginalType[]
{
id: 1,
name: "foo"
},
{
id: undefined,
name: "foo"
},
{
id: 3,
name: undefined
},
{
id: undefined,
name: undefined
},
]
// type is not inferred
const definedOnly1 = objArr1.filter(obj => typeof obj.id === "number" && typeof obj.name === "string");
// ^? const definedOnly1: OriginalType[]
Fără tipizarea explicită, funcționează:
const objArr0= [
// ^? objArr0: (({id: number; name: string;} | {id: undefined; name: string;} | {id: number; name: undefined;} | {id: undefined; name: undefined;})[]
{
id: 1,
name: "foo"
},
{
id: undefined,
name: "foo"
},
{
id: 3,
name: undefined
},
{
id: undefined,
name: undefined
},
]
// new type is inferred properly
const definedOnly0 = objArr0.filter(obj => typeof obj.id === "number" && typeof obj.name === "string");
// ^? definedOnly0: {id: number; name: string;}[]
O modalitate de a „ajuta” compilatorul TypeScript pare să fie folosirea satisfies în locul tipării explicite. Deși o consider nu foarte convenabilă, deoarece acest lucru nu funcționează, de exemplu, pentru parametrii funcțiilor tipați. Dar doar de dragul completitudinii, iată exemplul:
const objArr2 = [
// ^? same as objArr0
{
id: 1,
name: "foo"
},
{
id: undefined,
name: "foo"
},
{
id: 3,
name: undefined
},
{
id: undefined,
name: undefined
},
] satisfies OriginalType[];
// original type is used on original object, but also inferred on filtered object properly
const definedOnly2 = objArr2.filter(obj => typeof obj.id === "number" && typeof obj.name === "string");
// ^? same as definedOnly0
Poate cineva să explice dacă acesta este comportamentul intenționat și de ce?
Teren de joacă
Răspuns #1
Nu există nicio problemă aici cu predicatele de tip inferate; problema este că TypeScript nu restrânge tipul aparent al unui obiect atunci când restrângi una dintre proprietățile sale (decât dacă tipul obiectului este o reuniune discriminată și proprietatea pe care o restrângi este o proprietate discriminantă). Aceasta este o funcționalitate lipsă în TypeScript, solicitată la microsoft/TypeScript#42384.
Deci, dacă începi cu un obiect de tip { id: number | undefined; name: string | undefined } și îți verifici proprietățile id și name, proprietățile vor fi restrânse, dar obiectul în sine va rămâne încă larg:
type OriginalType = { id: number | undefined; name: string | undefined }
function foo(o: OriginalType) {
if ((typeof o.id === "number") && (typeof o.name === "string")) {
let narrowed: { id: number, name: string };
narrowed = { id: o.id, name: o.name }; // okay, properties of o narrowed
narrowed = o; // error, o was not narrowed!
}
}
Astfel, verificarea celor două proprietăți nu este văzută deloc ca un tip de protecție asupra obiectului și, prin urmare, nu există niciun predicat de tip de dedus.
Pe de altă parte, când îți împarți tipul Original într-o reuniune prin distribuirea reuniunilor de proprietăți peste tot tipul:
type UnionType =
{ id: number, name: string } | { id: number, name: undefined } |
{ id: undefined, name: string } | { id: undefined, name: undefined }
atunci această uniune este văzută ca o uniune discriminată (deoarece undefined este un discriminant valid, vezi notele de lansare TypeScript 3.2), iar verificările proprietăților duc la restrângerea obiectului:
function bar(o: UnionType) {
if ((typeof o.id === "number") && (typeof o.name === "string")) {
let narrowed: { id: number, name: string };
narrowed = { id: o.id, name: o.name }; // okay, properties of o narrowed
narrowed = o; // okay, o also narrowed
}
}
Deci asta este explicația. Dacă vrei să vezi acest lucru schimbat, nu ar strica să dai un 👍 la microsoft/TypeScript#42384. Probabil nu ar ajuta prea mult nici asta, dar dacă destui oameni o fac, atunci echipa TypeScript ar putea considera că cererea este suficient de mare pentru a merita investigată.
Link de teren de joacă către cod
