Typescript säger att array.pop() kan returnera undefined även när arrayen är garanterad att innehålla element
Varför varnar Typescript mig för att pop() kan returnera undefined även efter att jag har verifierat att jag tar bort ett element från en icke-tomt array?
type Person = {
name: string;
};
const people: Person[] = [{ name: 'Sandra' }];
if (people.length) {
const { name } = people.pop();
// TS Error: Property 'name' does not exist on type 'Person | undefined'.
}
Även om jag gör garantin tydligare, får jag samma fel:
if (people.length > 0 && people[people.length - 1] !== undefined){
const { name } = people.pop();
// TS Error: Property 'name' does not exist on type 'Person | undefined'.
}
Svar #1
Se microsoft/TypeScript#30406 för ett kanoniskt svar.
För frågan, "varför tror kompilatorn att pop() kan returnera undefined även när jag kontrollerar arrayens length", är det korta svaret "för att TypeScript:s standardbiblioteks anropssignatur för pop()-metoden på Array<T> returnerar T | undefined":
interface Array<T> {
// ... elided
pop(): T | undefined;
// ... elided
}
Så varje gång du anropar pop() på en array, kommer typen på returvärdet att inkludera undefined.
Den logiska nästa frågan är "varför gör de inte bättre anropssignaturer som returnerar T om length är icke noll och undefined om length är noll?" Svaret på det är "eftersom kontroll av length-egenskapen hos en allmän arraytyp inte ändrar den uppenbara typen av arrayen, så anropssignaturerna kan inte se skillnaden".
Du skulle till exempel kunna lägga till några samtalsignaturer på det här sättet:
interface Array<T> {
pop(this: { length: 0 }): undefined;
pop(this: { 0: T }): T;
}
Genom att använda this-parametrar kommer varje anropssignatur endast att väljas när arrayen som metoden anropas på matchar den angivna typen. Om arrayen har en length på 0, returnera undefined. Om arrayen har ett element av typen T på nyckeln 0, returnera T.
Detta fungerar bra för tuplet-typer vars längd är fast och vars elementindex är kända:
declare const u: [];
const a = u.pop(); // undefined
declare const v: [1, 2];
const b = v.pop(); // 1 | 2
Men naturligtvis är det en dålig idé att anropa pop() på en tupel och inte precis något du sannolikt skulle vilja göra. Om du har en array med variabel längd och anropar pop() på den väljs ingen av anropssignaturerna och du faller tillbaka på den inbyggda T | undefined, även om du försöker kontrollera length:
const w = Math.random() < 0.5 ? [] : ["a"] // string[]
if (w.length) {
w.pop().toUpperCase(); // error! Object is possibly undefined
}
Problemet är att length-egenskapen hos en Array<T> är av typen number, och det finns ingen typ som är "icke-noll number" för att begränsa w.length till. För att stödja den här typen av saker skulle du behöva något som negaterade typer, vilket inte ingår i TypeScript. Det är möjligt att någon med tillräckligt kompilatorarbete skulle kunna ge tillräcklig struktur åt arrayer i TypeScript så att en kontroll av sanningsvärdet för w.length skulle begränsa typen av arrayen till något du kan anropa pop() på utan att oroa dig för att få undefined.
Men det skulle lägga till en massa komplexitet för kompilatorn och språket för att stödja detta användningsfall. Fördelen är sannolikt inte värd kostnaden.
I avsaknad av detta är det så mycket enklare för dig att hoppa över length-kontrollen och bara kalla på pop() och kontrollera om det är undefined eller inte. Det är samma mängd arbete för dig, eftersom det bara flyttar kontrollen från före till efter anropet, och det gör saker mer raka för kompilatorn.
De andra svaren som publicerats här föreslår detta och andra lösningar, så jag kommer inte att gå in på dem. Min huvudpoäng är att språket helt enkelt inte är utrustat för att tillåta en length-kontroll att påverka beteendet hos pop(). Eller, som @RyanCavanaugh (dev-ledare för TS) kommenterade i microsoft/TypeScript#30406,
Detta är inget vi kan spåra
Åh ja!
Lekplatslänk till kod
Svar #2
Du kan göra det på det här sättet.
type Person = {
name: string;
};
const people: Person[] = [{ name: 'Sandra' }];
if (people.length) {
const nextPerson = people.pop() ;
if(nextPerson){
const {name} =nextPerson;
}
}
Lösning-2 (! efter pop)
type Person = {
name: string;
};
const people: Person[] = [{ name: 'Sandra' }];
if (people.length) {
const {name}=people.pop()!;
console.log(name);
}
