typescript - Varför får spridningar på typ-skyddade typer typkontroller att hoppas över?
Fråga:
Varför får jag inte ett kompileringstidfel när jag glömmer att lägga till nästlade fält till ett objekt av typen T, när jag konstruerar nämnda objekt med hjälp av en objektsprej?
Exempel:
interface User {
userId: number;
profile: {
username: string
}
}
function updateUsername(user: User): User {
return {
...user,
profile: {
// Error (as expected)
}
}
}
function updateUsernameGeneric<T extends User>(user: T): T {
return {
...user,
profile: {
// No error (unexpected... why?)
}
}
}
Min egen spekulation om svaret:
Allt jag kan föreställa mig är att TypeScript tillåter subtyper att ta bort egenskaper från sina supertyper, vilket gör det möjligt att för någon subtyp T av User kan profile-egenskapen inte innehålla några egenskaper. (Om så är fallet, var jag inte medveten om att TypeScript tillät dig att göra detta...)
TypeScript Version 4.1.2
Lekplats
Svar #1
Detta har allt att göra med hur spridningen hanteras med generiska typer (se PR) jämfört med normala typer. Om du skriver det resulterande objektet till en variabel kommer du omedelbart att märka skillnaden: för icke-generiska typer härleds den sammanslagna typen som:
{
profile: {};
userId: number;
}
vilket leder till att denna typ inte kan tilldelas den annoterade returtypen User som har en obligatorisk username-underegenskap. Detta är exakt vad kompilatorfelet TS 2322 berättar för dig:
Egenskapen 'username' saknas i typen '{}' men krävs i typen '{ username: string; }'
Nu är situationen med generics lite annorlunda: typen härleds faktiskt som en korsning av en subtyp av User och en { profile: {}; }-typ:
T & {
userId: string;
profile: {};
}
Kompilatorn är okej med det eftersom snittet är en "förlängning" av den annoterade returtypen som innehåller alla egenskaper enligt definitionen av snittet.
Om detta är ett bra beteende är diskutabelt eftersom du kan göra följande, och kompilatorn kommer inte att vara klokare:
function updateUsernameGeneric<T extends User>(user: T): T {
const newUser = {
...user,
userId: "234",
profile: {
// No error (unexpected... why?)
}
}
return newUser;
}
updateUsernameGeneric({ profile: { username: "John" }, userId: 123 }).userId //a-ok, "number"
Eftersom returtypen är en korsning av den generiska typparametern och de sammanslagna egenskaperna, kan du ta bort annoteringen av returtypen och låta TypeScript härleda den. Oförenliga egenskaps typer kommer att korrekt härledas som never:
function updateUsernameGenericFixed<T extends User>(user: T) {
const newUser = {
...user,
userId: "234",
profile: {
// No error (unexpected... why?)
}
}
return newUser;
}
updateUsernameGenericFixed({ profile: { username: "John" }, userId: 123 }).userId //never
Lekplats
