typescript - Hvorfor får spredninger på type-vergende typer typetester til å bli hoppet over?


Spørsmål:

Hvorfor får jeg ikke en kompileringstid-feil når jeg glemmer å legge til nestede felter i et objekt av typen T, når jeg konstruerer nevnte objekt ved å bruke et objektspredning?

Eksempel:

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 spekulasjon på svaret:

Alt jeg kan forestille meg er at TypeScript tillater subtyper å fjerne egenskaper fra sine supertyper, noe som gjør det mulig at for en eller annen subtype T av User, kan profile-egenskapen kanskje ikke inneholde noen egenskaper. (Hvis det er tilfelle, var jeg ikke klar over at TypeScript tillot deg å gjøre dette...)


TypeScript-versjon 4.1.2

Lekeplass

Svar #1

Dette har alt å gjøre med hvordan spredningen løses med generiske typer (se PR) sammenlignet med vanlige typer. Hvis du skriver det resulterende objektet til en variabel, vil du straks merke forskjellen: for ikke-generiske typer blir den sammenslåtte typen utledet som:

{
    profile: {};
    userId: number;
}

som fører til at denne typen ikke kan tilordnes den annoterte returtypen User som har en påkrevd underegenskap username. Dette er akkurat det kompilatorfeilen TS 2322 sier til deg:

Egenskapen 'username' mangler i typen '{}' men er påkrevd i typen '{ username: string; }'

Nå er situasjonen med generics litt annerledes: typen blir faktisk utledet som et snitt av en subtype av User og en { profile: {}; }-type:

T & {
    userId: string;
    profile: {};
}

Kompilatoren er ok med det siden krysningen er en «utvidelse» av den annoterte returtypen som inneholder alle egenskapene i henhold til definisjonen av krysningen.


Om dette er en god oppførsel er debatterbart, da du kan gjøre følgende, og kompilatoren vil ikke være noe klokere:

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"

Siden returtypen er et kryss mellom den generiske typeparameteren og de sammenslåtte egenskapene, kan du fjerne annotasjonen av returtypen og la TypeScript utlede den. Uforenlige egenskapstyper vil bli korrekt utledet 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

Lekeplass

Relaterte artikler