typescript - Varför har Object.values för en Partial Record undefined-typer?


När man extraherar Object.values från ett Partial Record är värdena en union mellan vad jag skulle förvänta mig och undefined.

const example: Partial<Record<string, number>> = {}
const values = Object.values(example)

// strangely, the typing for values is as follows:
// const values: (number | undefined)[]

Istället för (number | undefined)[] skulle jag förvänta mig att det är number[]. Varje värde är i praktiken definierat, och, om jag inte missar något, kommer det alltid att vara det.

Code Sandbox som visar problemet (med React-stomme) finns här

Det verkar sannolikt vara en artefakt av typesystemet, men jag skulle vilja förstå vad som händer och om det finns något sätt att undvika detta beteende.

Svar #1

I allt som följer kommer jag att anta att vi alla använder --strict-sviten av kompilatoralternativ, särskilt --strictNullChecks-kompilatoralternativet. Om du inte har --strictNullChecks aktiverat gäller de flesta av följande diskussioner som involverar typen undefined inte, men du skulle förmodligen inte ha problemet som tas upp i denna fråga från början.


Utility-typen Partial<T> implementeras som type Partial<T>={[K in keyof T]?: T[K]} och använder den valfria mappade typmodifieraren för att göra alla resulterande egenskaper valfria.

Utilitytypen Record<K, V> är en mappad typ som implementeras som type Record<K, V>={[P in K]: V}; och när du gör det med string som nyckel resulterar det i en strängindexsignatur som type Dictionary<V>={[k: string]: V}; där vilken egenskap som helst vars nyckel är av typen string måste ha ett värde av typen V.


Jag skulle verkligen inte rekommendera att blanda valfria mappade typmodifierare (Partial<T>) med indexsignaturer (Record<string, V>). Båda gör, eh, intressanta saker med undefined-typen som standard, och det finns kompilatoralternativ för att ändra detta standardbeteende till andra, eh, intressanta beteenden som vissa personer föredrar och andra inte gör. Och oavsett vilka inställningar du använder, kommer kombinationen av båda dessa sannolikt att göra dig olycklig, åtminstone i TypeScript 4.8.


Först, valfria egenskaper: i --strict-läge gör TypeScript egentligen ingen skillnad mellan saknade egenskaper och de som är närvarande-men-undefined. Det är ofta rimligt att behandla dem som samma sak, eftersom läsning av någon av dem ger dig undefined. Men bör du tillåtas att skriva undefined till värdet? Eller bör du kunna läsa undefined efter att du har använt in-operatorn för att kontrollera om egenskapen finns? Detta beror på vad du tycker att "valfri" betyder. Som standard säger TypeScript ja, om en egenskap är valfri bör du kunna skriva undefined där. Och därför är {x?: string} och {x?: string | undefined} identiska typer.

Gillar du inte det? Tja, det gjorde inte många andra heller... microsoft/TypeScript#13195, "Skilj mellan saknad och undefined", var öppet länge och fick hundratals upp-röster. Och så med TypeScript 4.4 introducerades kompilatoralternativet --exactOptionalPropertyTypes. Om du aktiverar det, kommer du inte längre kunna skriva undefined till en valfri egenskap om undefined inte uttryckligen ingår i egenskapens typ. Hurra!

Men det är inte en del av --strict-sviten av kompilatoralternativ. Kanske om den här funktionen hade introducerats med --strictNullChecks i TypeScript 2.0 skulle den ha varit det. Men mycket verklig kod förväntar sig för närvarande att valfria egenskaper accepterar undefined, och det skulle bryta om detta infördes. Och det kan vara irriterande. Den enkla kopieringen av en egenskap från ett objekt till ett annat av samma typ, som a.x=b.x, är plötsligt inte acceptabel om egenskapen är valfri. Du behöver if ("x" in b) {a.x=b.x} else {delete a.x} nu. Det är en extra hinder att ta sig igenom och om du inte verkligen bryr dig om skillnaden mellan saknas/undefined så kommer du inte att bli nöjd med det.

Så det är valfria egenskaper.


Nu om indexsignaturer: i --strict-läge gör TypeScript egentligen ingen skillnad mellan saknade egenskaper och de som är närvarande. Detta spelar egentligen ingen roll vid skrivning, men vid läsning märks det. Kompilatorn antar att om du läser från en nyckel som matchar indexsignaturen, så vet du att ett värde faktiskt finns där. Den tar inte hänsyn till möjligheten att det inte finns något egenskapsvärde och att du kommer få undefined. Om du ville göra säkrare läsningar kunde du manuellt lägga till | undefined till typen själv, vilket gör om {[x: string]: number} till {[x: string]: number | undefined}, men nu byter du problem med osäkra läsningar mot ett oönskat skrivproblem liknande det med valfria egenskaper. Som standard säger TypeScript att om du läser från en indexsignatur kommer du att få ett definierat värde (avreferera på egen risk).

Gillar du inte det? Nå, det gjorde många andra inte heller... microsoft/TypeScript#13778, "Option to include undefined in index signatures", var öppen under lång tid och fick hundratals upp-rop. Och så med TypeScript 4.1 introducerades --noUncheckedIndexedAccess kompileringsalternativet. Om du aktiverar det kommer du nu att få ett möjligt undefined-värde när du läser från en indexsignatur men du kan inte skriva ett där. Hurra!

Men det är inte en del av --strict-sviten av kompilatoralternativ. Kanske om den här funktionen hade introducerats med --strictNullChecks i TypeScript 2.0 skulle den ha varit det. Men mycket verklig kod förväntar sig för närvarande att indexsignatur-egenskaper finns, och det skulle bryta koden om detta infördes. Och det kan vara irriterande. Den enkla iterationen genom en array via index, som for (let i=0; i < arr.length; i++) { sum += arr[i]*arr[i] }, är plötsligt inte acceptabel. Du behöver nu for (let i=0; i < arr.length; i++) { const v=arr[i]; if (v !== undefined) { sum += v * v } }. Det är ett extra hinder att ta sig igenom, och om du inte använder indexsignaturer med godtyckliga nycklar kommer du inte att vara nöjd med det.

Så det är indexsignaturer.


Och du kombinerar dem. Tja, även med --exactOptionalPropertyTypes och --noUncheckedIndexedAccess aktiverade, kommer Partial<Record<string, number>> uttryckligen att inkludera undefined och därmed låta dig skriva undefined till dess egenskapsvärden:

// @exactOptionalPropertyTypes: true
// @noUncheckedIndexedAccess
type X = Partial<Record<string, number>>;
// type X = { [x: string]: number | undefined; }

const x: X = {};
x.hello = undefined; // no error!

Det här är... en bugg, antar jag? Eller en designbegränsning? Det är inrapporterat på microsoft/TypeScript#46969 "exactOptionalPropertyTypes: Partial av indexsignatur lägger till undefined" och markerat som "Behöver undersökas" från och med TypeScript 4.8.

Så oavsett vad du letar efter, vill du förmodligen inte använda Partial<{[k: string]: XXX}> alls.

Istället bör du tänka på vilket beteende du vill ha med tanke på de tillgängliga alternativen ovan och deras kompromisser. Om du planerar att endast iterera över egenskaperna i ditt objekt, skulle jag säga att du bara ska använda Record<string, number> (utlämna Partial) och behålla standardinställningarna för --strict. Om du planerar att indexera in i ditt objekts egenskaper med godtyckliga nycklar, bör du välja mellan standardinställningarna och att lägga till | undefined manuellt (med vetskapen att undefined-värden kan dyka upp när du itererar) eller att aktivera --noUncheckedIndexedAccess och få varje indexsignatur i din kodbas att börja oroa sig mycket för undefined. Det är upp till dig.

Playground-länk till kod

Relaterade artiklar