generics - TypeScript Partial<T>-typ utan undefined


Hur skapar man en kinda-Partial<T>-typ, som inte tillåter undefined-värden?

Här är ett exempel:

interface MyType {
  foo: string
  bar?: number
}

const merge = (value1: MyType, value2: KindaPartial<MyType>): MyType => {
  return {...value1, ...value2};
}



const value = {
  foo: 'foo',
  bar: 42
}

merge(value, {});                          // should work
merge(value, { foo: 'bar' });              // should work
merge(value, { bar: undefined });          // should work
merge(value, { bar: 666 });                // should work
merge(value, { foo: '', bar: undefined }); // should work
merge(value, { foo: '', bar: 666 });       // should work

// now the problematic case:
merge(value, { foo: undefined }); // this should throw an error
                                  // because MyType["foo"] is of type string

Typen jag letar efter bör:

  • acceptera endast nycklar som finns på den generiska typen (precis som en vanlig Partial<T>)
  • acceptera en delmängd av nycklarna av den generiska typen
  • men acceptera inte undefined om den generiska typen inte accepterar undefined för den nyckeln

Är detta möjligt?

REDIGERA: Jag skapade också ett ärende i TypeScript-förvaret, eftersom detta är konstigt och borde ge ett felmeddelande vid något tillfälle: https://github.com/Microsoft/TypeScript/issues/29701

Svar #1

TS 4.4 UPPDATERING:

TS4.4 kommer att ha en --exactOptionalPropertyTypes-kompilatorflagg för att ge dig det beteende du letar efter direkt med Partial, så länge du medvetet lägger till undefined där du vill tillåta det:

interface MyType {
  foo: string
  bar?: number | undefined // <-- you want this
}

const merge = (value1: MyType, value2: Partial<MyType>): MyType => {
  return { ...value1, ...value2 };
}


const value = {
  foo: 'foo',
  bar: 42
}

merge(value, {});                          // okay
merge(value, { foo: 'bar' });              // okay
merge(value, { bar: undefined });          // okay
merge(value, { bar: 666 });                // okay
merge(value, { foo: '', bar: undefined }); // okay
merge(value, { foo: '', bar: 666 });       // okay

// now the problematic case:
merge(value, { foo: undefined }); // error!
// ----------> ~~~
// Type 'undefined' is not assignable to type 'string'

Playground-länk till kod <-- notera, för tillfället behöver du själv slå på --exactOptionalPropertyTypes i TS Config-fliken; av någon anledning är URL:en trasig

——--

PRE-TS4.4 SVAR:

Det är en känd begränsning (se microsoft/TypeScript#13195) att TypeScript inte korrekt skiljer mellan objekt-egenskaper (och funktionsparametrar) som saknas från de som finns men är undefined. Det faktum att Partial<T> tillåter undefined-egenskaper är en följd av det. Det rätta att göra är att vänta tills detta problem åtgärdas (detta kan bli mer sannolikt om du går till det ärendet på GitHub och ger en 👍 eller en kommentar med ett övertygande användningsfall).

Om du inte vill vänta kan du kanske använda följande hackiga sätt för att få något liknande detta beteende:

type VerifyKindaPartial<T, KP> = 
  Partial<T> & {[K in keyof KP]-?: K extends keyof T ? T[K] : never}; 

const merge = <KP>(value1: MyType, value2: KP & VerifyKindaPartial<MyType, KP>): MyType => {
  return { ...value1, ...value2 };
}

Så du kan inte skriva KindaPartial<T> direkt. Men du kan skriva en typ VerifyKindaPartial<T, KP> som tar en typ T och en kandidat-typ KP som du vill kontrollera mot din avsedda KindaPartial<T>. Om kandidaten matchar returnerar den något som matchar KP. Annars returnerar den något som inte gör det.

Sedan gör du merge() till en generisk funktion som härleder KP från typen av värdet som skickas in i value2. Om KP & VerifyKindaPartial<MyType, KP> matchar KP (vilket betyder att KP matchar KindaPartial<MyType>), kommer koden att kompileras. Annars, om KP & VerifyKindaPartial<MyType, KP> inte matchar KP (vilket betyder att KP inte matchar KindaPartial<MyType>), kommer det att uppstå ett fel. (Felet kanske inte är särskilt intuitivt, dock).

Låt oss se:

merge(value, {});                          // works
merge(value, { foo: 'bar' });              // works
merge(value, { bar: undefined });          // works
merge(value, { bar: 666 });                // works
merge(value, { foo: '', bar: undefined }); // works
merge(value, { foo: '', bar: 666 });       // works

merge(value, { foo: undefined }); // error!
//             ~~~ <-- undefined is not assignable to never
//  the expected type comes from property 'foo', 

Det har det beteende du vill ha... även om felet du får är lite konstigt (idealiskt sett skulle det säga att undefined inte kan tilldelas string, men problemet är att kompilatorn vet att den passerade typen är undefined, och den vill att typen ska vara string, så kompilatorn korsar dessa till undefined & string vilket är never. Nåväl.

Hur som helst finns det förmodligen förbehåll här; generiska funktioner fungerar bra när de anropas direkt men de komponerar inte bra eftersom TypeScripts stöd för högre-ordningens typer inte är så bra. Jag vet inte om detta faktiskt kommer att fungera för ditt användningsfall, men det är det bästa jag kan göra med språket som det är just nu.

Lekplatslänk till kod

Svar #2

I detta fall bör Pick fungera.

interface MyType {
  foo: string
  bar?: number
}

const merge = <K extends keyof MyType>(value1: MyType, value2: Pick<MyType, K>): MyType => {
  return {...value1, ...value2};
}

merge(value, {});                          // ok
merge(value, { foo: 'bar' });              // ok
merge(value, { bar: undefined });          // ok
merge(value, { bar: 666 });                // ok
merge(value, { foo: '', bar: undefined }); // ok
merge(value, { foo: '', bar: 666 });       // ok

merge(value, { foo: undefined });          // ng

Lekplatslänk

Relaterade artiklar