Problem med TypeScript og spread-operator
Jeg forstår ikke, hvorfor denne kode:
interface TotoInt {
name: string;
lastName: string | null;
}
interface TotoInt2 {
name: string;
lastName: string;
}
const toto: TotoInt = {
name: 'toto',
lastName: Math.random() < 0.5 ? null : "abc",
};
if (toto.lastName) {
const toto2: TotoInt2 = {
...toto,
};
}
producerer følgende output:
Jeg ville have forventet, at TypeScript forstod, at ved at tjekke if (toto.lastName), ville toto.lastName være garanteret ikke-null, hvilket dermed tillader brugen af TotoInt2.
Hvis jeg gør det på denne måde i stedet (med operatoren for ikke-null påstand !), klager TypeScript ikke:
// if (toto.lastName) {
// const toto2: TotoInt2 = {
// ...toto,
// };
// }
const toto2: TotoInt2 = {
name: toto.name,
lastName: toto.lastName!,
};
Er dette et problem med den måde, TypeScript (den version jeg bruger er 4.8.3) håndterer spread-operatoren på? Er der ingen måde at undgå den fulde rekonstruktion af et objektliteral med ! non-nullable operatoren for at få koden til at acceptere brugen af TotoInt2?
Objektet er ret simpelt til demonstrationsformål, men jeg arbejder med et stort objekt, som jeg ideelt set kunne sende ind i en funktion, der ville tjekke for null-værdier, og som jeg dermed ikke skulle rekonstruere helt med en ny objektlitteral og ! ikke-nullbare operatorer.
Svar #1
Indsnævring sker kun under særlige omstændigheder.
Hvis du udfører en typekontrol på egenskaben prop af et objekt obj, såsom (typeof obj.prop === "string"), kan du med rette forvente, at hvis obj.prop indsnævres, så vil obj også blive indsnævret. Det vil sige, hvis typekontrolløren nu ved, at obj.prop er en string i modsætning til string | null, så burde den også vide, at obj er en objekttype med en prop-egenskab af typen string frem for string | null. Men generelt sker dette ikke: hvis du udfører en typekontrol på obj.prop, vil normalt kun obj.prop blive indsnævret. Selve typen af obj vil stædigt forblive bred.
( En undtagelse fra dette er, når obj er af en diskrimineret unionstype, og prop er en diskriminant-egenskab. Men TotoInt er slet ikke en unionstype, og slet ikke en diskrimineret en, så at tjekke en egenskab af et TotoInt-objekt vil kun muligvis indsnævre den egenskab og ikke det overordnede objekt. )
Der er et forslag på microsoft/TypeScript#42384 om at videreføre indskrænkninger af egenskaber til deres overordnede objekter. Men indtil videre er dette ikke en del af sproget.
Derfor skal du arbejde udenom det. Den nemmeste løsning er at kopiere den indsnævrede egenskab eksplicit, da den egenskab er korrekt indsnævret:
if (toto.lastName) {
const toto2: TotoInt2 = {
...toto,
lastName: toto.lastName // <-- copy over the checked prop again
}; // okay
}
Hvis du ofte oplever dette problem, kunne du skrive en hjælperfunktion som brugerdefineret typebeskytter, som du kalder i stedet for at udføre typekontrollen. Det er en slags gør-det-selv implementering af microsoft/TypeScript#42384, og er accordingly klodset:
function hasPropType<T, K extends keyof T, V extends T[K]>(
obj: T, prop: K, guard: (v: T[K]) => v is V): obj is T & { [P in K]: V } {
return guard(obj[prop]);
}
Idéen er, at du tjekker egenskaben af nøgletypen K på et objekt af typen T med en typebeskyttelsesfunktion af typen (v: T[K]) => v er V, hvor V er en snævrere type end den kendte egenskabstype T[K]. Og dette vil tjene til at indsnævre obj, hvis beskyttelsen returnerer sand.
For hasPropType, vil du gerne tjekke, om toto.lastName ikke er null, så du kan skrive følgende typeguard-funktion:
function isNonNullish<T>(x: T): x is NonNullable<T> {
return x !== undefined && x !== null;
}
(Hvis du bruger TS5.5 eller nyere, kan du undlade x is NonNullable<T>-typeannotationen, fordi den nu kan udledes fra funktionen. For TS5.4 eller ældre skal du skrive dette manuelt.) Nu ser tjekket sådan ud:
if (hasPropType(toto, "lastName", isNonNullish)) {
//const toto: TotoInt & { lastName: string; }
const toto2: TotoInt2 = { ...toto }; // okay
}
Du kan se, at toto bliver indsnævret fra TotoInt til TotoInt & {lastName: string}, hvilket kan tildeles TotoInt2.
Ja, det er klodset, men du vil måske have denne version, hvis du har en grund til, at det at kopiere ejendommen flere gange har dårlige bivirkninger.
Playground-link til kode
