javascript - Typescript oppslagstyper - korrekt begrense mengden egenskaper når man slår sammen med Partial


Jeg eksperimenterer med oppslagstyper og ønsker å lage en slags sikker-sammenføyningsverktøyfunksjon (en som tar en enhet av typen T og et objekt som inneholder en delmengde av nøklene til T for å oppdatere). Målet mitt er å la kompilatoren fortelle meg når jeg stavefeiler en egenskap eller prøver å legge til en ikke-eksisterende for T.

Så jeg har Person og bruker innebygd Partial i (v2.1) slik:

interface Person {
  name: string
  age: number
  active: boolean
}

function mergeAsNew<T>(a: T, b: Partial<T>): T {
  return Object.assign({}, a, b);
}

Nå anvender jeg dette på følgende data:

let p: Person = {
  name: 'john',
  age: 33,
  active: false
};

let newPropsOk = {
  name: 'john doe',
  active: true
};

let newPropsErr = {
  fullname: 'john doe',
  enabled: true
};

mergeAsNew(p, newPropsOk);
mergeAsNew(p, newPropsErr); // <----- I want tsc to yell at me here because of trying to assign non-existing props

Jeg ønsker at TS-kompilatoren skal rope til meg ved andre kjøring ettersom fullname og enabled ikke er props til Person. Dessverre kompilerer dette fint lokalt, men... når jeg gjør det samme i online TS Playground får jeg mer eller mindre det jeg forventer:

The type argument for type parameter 'T' cannot be inferred from the usage. Consider specifying the type arguments explicitly.
  Type argument candidate 'Person' is not a valid type argument because it is not a supertype of candidate '{ fullname: string; enabled: boolean; }'.
    Property 'name' is missing in type '{ fullname: string; enabled: boolean; }'.

Ser ut som lekeplassen bruker samme versjon som jeg har lokalt (2.1.4). Har noen en idé om hvorfor disse to kan være forskjellige?

Bonusspørsmål:

når jeg prøver følgende oppgave:

let x: Person = mergeAsNew(p, newPropsOk);

Jeg får følgende feil på x, men bare på lekeplassen (det fungerer helt fint lokalt):

Type '{ name: string; active: boolean; }' is not assignable to type 'Person'.
  Property 'age' is missing in type '{ name: string; active: boolean; }'.

Hvorfor er det slik? Burde det ikke være av Person-type, siden første mergeAsNew-argument er Person og alt annet er et delsett av Person-egenskaper (så det er maksimalt Person)?

REDIGER Her er min tsconfig.json:

{
  "compilerOptions": {
    "target": "ES6",
    "module": "commonjs",
    "noEmitOnError": true,
    "allowJs": false,
    "sourceMap": true,
    "strictNullChecks": true
  },
  "exclude": ["dist", "scripts"]
}

Svar #1

Når du vil uttrykke at én type bare har et delsett av egenskapene til en annen type, kan vanlig gammel extends også være til hjelp

interface Person {
    name: string
    age: number
    active: boolean
}


let p: Person = {
    name: 'john',
    age: 33,
    active: false
};

let newPropsOk = {
    name: 'john doe',
    active: true
};

let newPropsErr = {
  fullname: 'john doe',
  enabled: true
};

// a extends b and not the other way round
//because we don't want to allow b to have properties not in a
function mergeAsNew<T2, T1 extends T2>(a: T1, b: T2): T1 {
    return Object.assign({}, a, b);
}

mergeAsNew(p, newPropsOk); // ok
mergeAsNew(p, newPropsErr); 
// Argument of type 'Person' is not assignable 
// to parameter of type '{ fullname: string; enabled: boolean; }'.
//   Property 'fullname' is missing in type 'Person'.

PS har ingen anelse om hva som skjer med playground og kartlagte typer

Svar #2

Ok, det ser ut til at jeg var for fiksert på det i går. Jeg fikk litt søvn, og det ser ut til at det (sannsynligvis) er umulig å gjøre det jeg vil på den måten (ved å bruke Partial). Hvis vi legger denne merkelige playground-adferden til side en stund, her er hvorfor (i det minste tror jeg det):

For å oppsummere:

interface Person {
  name: string
  age: number
  active: boolean
}

function mergeAsNew<T>(a: T, b: Partial<T>): T {
  return Object.assign({}, a, b);
}

I denne koden kan Partial<Person> (men må ikke) inneholde noen av Person-egenskapene, så følgende er en helt gyldig Partial<Person>-type:

let newPropsErr = {
  fullname: 'john doe',
  enabled: true
};

Siden Typescript har strukturell typing bryr det seg ikke om ekstra egenskaper, det sjekker bare objektets form, og her passer formen til Partial<Person> helt fint.

Den eneste måten å på en eller annen måte sjekke argumentets form mot Partial<Person> for ekstra egenskaper er å sende det som en objektliteral (literalene sjekkes strengt for å samsvare med typen)

Nå til selve mergeAsNew-funksjonen. Jeg gjør Object.assign her som gjør akkurat det det er laget for - slår sammen props fra begge argumentene til et nytt objekt (alle sammen). Fordi typer kun er for TS-kompilering, finnes det ingen måte under kjøring å begrense og selektivt velge props fra b og bruke dem til å overstyre a-props.

Ser ut som @Artems svar som gjør det jeg trenger med extend og uten map-/oppslagstyper.

Uansett, jeg forstår fortsatt ikke hvorfor TS Playground fungerer som det gjør (det som først ga meg inntrykk av at alt det der var helt gjennomførbart).

Relaterte artikler