javascript - Typescript opslags-typer - korrekt indsnævring af egenskabssæt ved sammenlægning med Partial
Jeg leger med opslagstyper og vil gerne bygge en slags sikker-fletnings-utilityfunktion (en der tager en entitet af typen T og et objekt, der indeholder et delmængde af nøglerne i T til opdatering). Mit mål er at lade compiler fortælle mig, når jeg stavefejler et property eller forsøger at tilføje en ikke-eksisterende for T.
Så jeg har Person og bruger indbyggede Partial i (v2.1) sådan her:
interface Person {
name: string
age: number
active: boolean
}
function mergeAsNew<T>(a: T, b: Partial<T>): T {
return Object.assign({}, a, b);
}
Nu 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 vil gerne have, at TS-kompilatoren råber af mig ved anden kørsel, da fullname og enabled ikke er egenskaber af Person. Desværre kompilerer dette fint lokalt, men... når jeg gør det samme i online TS Playground, får jeg mere 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; }'.
Det ser ud til, at legepladsen bruger den samme version som jeg gør lokalt (2.1.4). Har nogen en idé om, hvorfor de to kan være forskellige?
Bonus spørgsmål:
når jeg prøver følgende opgave:
let x: Person = mergeAsNew(p, newPropsOk);
Jeg får følgende fejl på x, men kun på playgrounden (det er 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 sådan? Bør det ikke være af Person-typen, da det første mergeAsNew-argument er Person og alt andet er en delmængde af Person-egenskaber (så det er højst 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 udtrykke, at en type kun har et delmængde af egenskaberne fra en anden type, kan almindelig gammel extends også hjælpe
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 ingen idé om, hvad der foregår med playground og mapped types
Svar #2
Ok, det ser ud til, at jeg var for fokuseret på det i går. Jeg fik noget søvn, og det ser ud til, at det (formentlig) er umuligt at gøre, hvad jeg vil på den måde (ved at bruge Partial). Hvis vi lægger denne mærkelige playground-adfærd til side et øjeblik, er her hvorfor (i det mindste tror jeg det):
For at opsummere:
interface Person {
name: string
age: number
active: boolean
}
function mergeAsNew<T>(a: T, b: Partial<T>): T {
return Object.assign({}, a, b);
}
I denne kode kan Partial<Person> (men behøver ikke) indeholde nogen af Person-egenskaberne, så følgende er en helt gyldig Partial<Person>-type:
let newPropsErr = {
fullname: 'john doe',
enabled: true
};
Da Typescript har strukturel typning, bekymrer det sig ikke om ekstra egenskaber, det tjekker kun objektets form, og her passer formen fint til Partial<Person>.
Den eneste måde at på en eller anden måde kontrollere argumentets form mod Partial<Person> for ekstra egenskaber er at videregive det som en objektliteral (literals kontrolleres strengt for at matche typen)
Nu til selve mergeAsNew-funktionen. Jeg bruger Object.assign her, som gør præcis det, den er designet til - samler props fra begge argumenter i et nyt objekt (alle sammen). Fordi typer kun er til TS-kompilering, er der ingen måde i runtime at begrænse og selektivt vælge props fra b og anvende dem til at overskrive a-props.
Ser ud til at @Artems svar gør det, jeg har brug for med extend og uden mapped/lookup-typer.
Under alle omstændigheder forstår jeg stadig ikke, hvorfor TS Playground fungerer, som det gør (hvad der oprindeligt gav mig indtryk af, at alt det der er helt muligt at gøre).
