typer - Typescript märker inte att en Partial<Record<...>>-nyckel kanske inte finns om det finns obligatoriska nycklar
Efter att ha definierat en typ för att beskriva ett objekt med obligatoriska och valfria nycklar i stil med
type ItemWithOptionals<OPTIONAL extends string, REQUIRED extends string> =
Partial<Record<OPTIONAL, string>> & Record<REQUIRED, string>;
kompilatorn drar inte slutsatsen att en åtkomst med en nyckel av typen OPTIONAL kan returnera undefined, som i
function wantString(text: string): void { ... }
function f2<O extends string, R extends string>(opt: O, rec: ItemWithOptionals<O, R>): void {
const x = rec[opt];
wantString(x);
}
Det verkar vara uppenbart att rec[opt] kan returnera undefined, men anropet till wantString(x) är inte markerat som ett fel. Och typassistenten för x visar inte heller undefined (playground).
Varför härleder inte kompilatorn att undefined är en del av typen för x? Kan typen av ItemWithOptionals förbättras så att kompilatorn märker detta?
Svar #1
Detta är en begränsning av TypeScript. Se microsoft/TypeScript#33181 för mer information.
Det finns en kompromiss mellan noggrannhet och bekvämlighet när det gäller att analysera generiska typer som ItemWithOptionals<O, R>[O], typen av rec[opt]. Antingen kan kompilatorn skjuta upp analysen och behålla typen generisk och korrekt men mestadels oexponerad, i vilket fall typen i huvudsak är ogenomskinlig och inte lätt kan användas, eller så kan den ivrig analysera typen genom att vidga de generiska typparametrarna till deras begränsningar, vilket gör den till en specifik typ som enkelt kan användas, men som kanske inte är korrekt.
Generellt, när du läser en egenskap som rec[opt] där typerna är generiska, kommer kompilatorn oftast att göra den eventuellt felaktiga men bekväma utvidgningen. I det här fallet är rec av typen ItemWithOptionals<O, R>, där O och R båda är begränsade till string.
Uppsvidgningen resulterar i ItemWithOptionals<string, string>, vilket utvärderas till Partial<Record<string, string>> & Record<string, string>, som i sin tur utvärderas till Record<string, string | undefined> & Record<string, string>, vilket är ekvivalent med bara Record<string, string> (eftersom alla egenskaper är snittet (string | undefined) & string som utvärderas till string), eller {[k: string]: string}. Och indexering i den typen med opt av typen O, utvidgad till string, ger ett värde av typen string:
const y: string = rec[opt]; // no error
Oj. Genom att bredda både O och R till string har kompilatorn felaktigt utplånat skillnaden mellan de valfria egenskaperna och de obligatoriska egenskaperna.
Som en lösning skulle jag säga att du kan undvika detta genom att göra en mer noggrann utvidgning innan indexeringen. Om ett värde kan tilldelas en intersection XXX & YYY, då kan det tilldelas varje XXX och YYY separat. Den relevanta delen av rect för den raden är Partial<Record<O, string>>-delen, inte Record<R, string>-delen. Så låt oss först utvidga till den relevanta delen och se vad som händer:
const r: Partial<Record<O, string>> = rec; // okay
const x = r[opt];
// const x: Partial<Record<O, string>>[O]
wantString(x); // error!
// ------> ~
// string | undefined not assignable to string
Ser bra ut. Vi har breddat rec till r, vilket lyckas. Sedan ses typen av r[opt] som Partial<Record<O, string>>[O]. När kompilatorn breddar det genom att sätta in string för O, får den sedan string | undefined istället för string. Och du får felet du förväntade dig.
Playground-länk till kod
