typescript - Hur man villkorsmässigt lägger till en nyckel till ett objekt?
Tänk på dessa typer:
type A = {
a: string;
b?: string;
}
type B = {
a: number;
b?: number;
}
Jag vill konvertera ett objekt av typen A till B genom att skriva över vissa nycklar och lägga till nycklar villkorligt beroende på om det ursprungliga objektet har dem:
const a: A = {
a: '1',
b: '2'
}
const b: B = {
...a,
a: 1,
... a.b && {b: Number(a.b)}
}
// expected:
// const b: B = {
// a: 1,
// b: 2
// }
TypeScript ger detta fel:
Type '{ b?: string | number | undefined; a: number; }' is not assignable to type 'B'.
Types of property 'b' are incompatible.
Type 'string | number | undefined' is not assignable to type 'number | undefined'.
Type 'string' is not assignable to type 'number | undefined'.
Varför härleder det b på det här sättet? Finns det ett sätt att kringgå det?
Svar #1
Det är en kombination av två mindre designbegränsningar och en större designbegränsning i TypeScript, och du skulle ha det bättre med att refaktorera eller använda en typförklaring för att gå framåt.
Först är microsoft/TypeScript#30506. Generellt sett kommer kontroll av en egenskap hos ett objekt att snäva in den uppenbara typen av den egenskapen men kommer inte snäva in den uppenbara typen av själva objektet. Det enda undantaget är om objektet är av en diskriminerad unionstyp och du kontrollerar dess diskriminant-egenskap. I ditt fall är A inte en diskriminerad union (det är inte en union alls), så detta händer inte. Observera:
type A = {
a: string;
b?: string;
}
declare const a: A;
if (a.b) {
a.b.toUpperCase(); // okay
const doesNotNarrowParentObject: { b: string } = a; // error
}
Det finns en nyare öppen förfrågan på microsoft/TypeScript#42384 för att åtgärda denna begränsning. Men för tillfället hindrar detta ändå din a.b-kontroll från att ha någon inverkan på den observerade typen av a när du sprider den till b.
Du kan skriva din egen anpassade typvaktfunktion som kontrollerar a.b och begränsar typen av a:
function isBString(a: A): a is { a: string, b: string } {
return !!a.b;
}
if (isBString(a)) {
a.b.toUpperCase(); // okay
const alsoOkay: { b: string } = a; // okay now
}
Nästa problem är att kompilatorn inte ser ett objekt-vars-egenskap-är-en-union som ekvivalent med en union-av-objekt:
type EquivalentA =
{ a: string, b: string } |
{ a: string, b?: undefined }
var a: A;
var a: EquivalentA; // error!
// Subsequent variable declarations must have the same type.
Alla typer av begränsande beteenden där kompilatorn ser a som "antingen något med ett b med värdet string, eller något med ett b med värdet undefined" skulle förlita sig på denna typ av ekvivalens. Kompilatorn förstår denna ekvivalens i vissa konkreta fall tack vare smartare stöd för kontroll av unionstyper som introducerades i TS 3.5, men det sker inte på typnivå.
Även om vi ändrar A till EquivalentA och a.b-kontrollen till isBString(a), har du fortfarande felet.
const stillBadB: B = {
...a,
a: 1,
...isBString(a) && { b: Number(a.b) }
} // error!
Och det är det stora problemet: grundläggande begränsningar i kontrollflödesanalys.
Kompilatorn kontrollerar vissa vanligt använda syntaktiska strukturer och försöker begränsa de uppenbara typerna av värden baserat på dessa. Detta fungerar bra med strukturer som if-satser, eller logiska operatorer som || eller &&. Men omfattningen av dessa inskränkningar är begränsad. För if-satser skulle detta vara sant/falskt kodblock, medan det för logiska operatorer är uttrycket till höger om operatorn. När man lämnar dessa omfång har all kontrollflödesinskränkning glömts bort.
Du kan inte "registrera" resultaten av styrflödesförträngning i en variabel eller annat uttryck och använda dem senare. Det finns helt enkelt ingen mekanism som tillåter detta att hända. (Se microsoft/TypeScript#12184 för ett förslag om att tillåta detta; det är markerat som "Revisit" Uppdatering för TS4.4, detta problem åtgärdades med en ny styrflödesanalysfunktion men denna fix hjälper inte den nuvarande koden, så jag kommer inte att gå in på det). Se microsoft/TypeScript#37224, som efterfrågar stöd för detta på nya objektliteraler.
Det verkar som att du förväntar dig koden
const b: B = {
...a,
a: 1,
...isBString(a) && { b: Number(a.b) }
}
att fungera eftersom kompilatorn bör utföra något som liknar följande analys:
- Typen av
aär{ a: string, b: string } | {a: string, b?: undefined}. - Om
aär{a: string, b: string}, då (förutom eventuella konstigheter med falska""-värden), kommer{...a, a: 1, ...isBString(a) && {b: Number(a.b) }att vara en{a: number, b: number}. - Om
aär{a: string, b?: undefined}, då ``{...a, a: 1, ...isBString(a) && {b: Number(a.b)}}kommer att vara{a: number, b?: undefined}` - Därför är detta uttryck en union
{a: number, b: number} | {a: number, b?: undefined}som kan tilldelasB.
Men detta händer inte. Kompilatorn tittar inte på samma kodblock flera gånger, och föreställer sig att något värde har begränsats till varje möjligt medlem i unionen i tur och ordning, och samlar sedan resultatet i en ny union. Det vill säga, den utför inte det jag kallar distributiv kontrollflödesanalys; se microsoft/TypeScript#25051.
Detta skulle nästan säkert aldrig kunna hända automatiskt, eftersom det skulle vara enormt dyrt för kompilatorn att simulera att varje värde av en unionstyp är av varje möjlig insnävning överallt. Du kan inte ens be kompilatorn att göra det uttryckligen (det var vad microsoft/TypeScript#25051 handlade om).
Det enda sättet att få kontrollflödesanalys att ske flera gånger är att ge den flera kodblock:
const b: B = isBString(a) ? {
...a,
a: 1,
...true && { b: Number(a.b) }
} : {
...a,
a: 1,
// ...false && { b: Number(a.b) } // comment this out
// because the compiler knows it's bogus
}
Vilket, vid det här laget, verkligen är för fult och för långt ifrån din ursprungliga kod för att vara troligt.
Du kan, som det andra svaret nämnde, använda ett helt annat arbetsflöde. Eller så kan du använda en typassertion någonstans för att göra kompilatorn nöjd. Till exempel:
const b: B = {
...(a as Omit<A, "b">),
a: 1,
...a.b && { b: Number(a.b) }
} // okay
Här ber vi kompilatorn att låtsas som att a inte ens har en b-egenskap när vi sprider ut den i det nya objektliteralet. Nu överväger inte kompilatorn ens möjligheten att det resulterande b kan vara av typen string, och det kompileras utan fel.
Eller ännu enklare:
const b = {
...a,
a: 1,
...a.b && { b: Number(a.b) }
} as B
I fall som detta, där kompilatorn inte kan verifiera typens säkerhet för något som du är säker på är säkert, är ett typpåstående rimligt. Detta flyttar ansvaret för sådan säkerhet från kompilatorn till dig, så var försiktig.
Lekplatslänk till kod
Svar #2
Det ser ut som att du redigerade din fråga och på så sätt löste ditt eget problem! :) Min kod är densamma som din förutom det sista testet.
type A = {
a: string;
b?: string;
};
type B = {
a: number;
b?: number;
};
/* With more generic object types:
type A = {
[id: string]: string;
};
type B = {
[id: string]: number;
};
*/
const a: A = {
a: '1',
b: '2'
}
const b: B = {
...a,
a: 1,
...(a.b && { b: Number(a.b) })
}
console.assert(b.a === 1, 'b.a');
console.assert(b.b === 2, 'b.b');
console.log(b);
Körde som tsc temp.ts && node temp.js och resultat:
{ a: 1, b: 2 }
