typescript - Är det möjligt att härleda en objekttyp baserat på ett skickat objekt?
Jag har olika dokument som alla har en uppsättning gemensamma fält.
Jag försöker skapa en funktion som kommer att fylla i de gemensamma fälten samtidigt som de unika fälten tas som indata, vilket resulterar i ett komplett dokument.
Jag har detta fungerande, men just nu behöver jag manuellt specificera den slutliga typen. Jag är nyfiken på om det finns ett sätt i TypeScript att härleda den slutliga typen baserat på de fält som skickas in (eller ge ett fel om de inskickade fälten inte matchar någon av de kända dokumenttyperna).
Här är koden jag har just nu för detta:
type DocA = {
id: string,
createdAt: string,
onlyDocA: string
}
type DocB = {
id: string,
createdAt: string,
onlyDocB: string
}
type AllDocTypes = DocA | DocB
export const createBaseDocument = <DocType extends AllDocTypes>(
fields: Omit<DocType, 'id' | 'createdAt'>,
): DocType => {
const createdAt = new Date();
return {
id: generateId(),
createdAt: createdAt.toString(),
...fields,
} as DocType
}
function generateId(): string {
return 'id'
}
// testA should be of type `DocA`
const testA = createBaseDocument({ id: 'a', createdAt: '123', onlyDocA: 'hello'})
// testA should be of type `DocB`
const testB = createBaseDocument({ id: 'a', createdAt: '123', onlyDocB: 'hello'})
// testC should throw a type error
const testC = createBaseDocument({ id: 'a', createdAt: '123', onlyDocC: 'hello'})
// testD should throw a type error
const testD = createBaseDocument({ id: 'a', createdAt: '123', onlyDocA: 'hello', onlyDocB: 'hello'})
TypeScript-lekplats
Svar #1
Om du vill ha bättre kontroll över vad som går in i funktionen, kanske du vill använda funktionsöverlagring istället:
type DocA = {
id: string,
createdAt: string,
onlyDocA: string
}
type DocB = {
id: string,
createdAt: string,
onlyDocB: string
}
function createBaseDocument(fields: Omit<DocA, 'id'| 'createdAt'>): DocA;
function createBaseDocument(fields: Omit<DocB, 'id'| 'createdAt'>): DocB;
function createBaseDocument(fields: Omit<DocA, 'id'| 'createdAt'>|Omit<DocB, 'id'| 'createdAt'>): DocB|DocA{
const createdAt = new Date();
return {
id:generateId(),
createdAt: createdAt.toString(),
...fields
}
}
På det här sättet kommer createBaseDocument({onlyDocA:'a'}) och createBaseDocument({onlyDocB:'b'}) att fungera, men createBaseDocument({onlyDocA:'a',onlyDocB:'b'}) kommer att resultera i ett fel
Svar #2
För att utöka Lazars svar med en förklaring till varför:
Din metod misslyckas eftersom Omit<AllDocTypes, "id"|"createdAt"> inte gör det du förväntar dig att den ska göra. Du vill använda den för att lämna onlyDocA eller onlyDocB-egenskaper, men så här fungerar det inte. Vad du faktiskt får är ett tomt objekt {}:
type testOmit = Omit<AllDocTypes, "id"|"createdAt">; // {}
Typen {} är faktiskt en väldigt bred typ i detta sammanhang och betyder "ett objekt med ett godtyckligt antal egenskaper av vilken typ som helst". Det är därför du kan specificera vad som helst i funktionsanropet.
När det gäller varför detta händer, är det eftersom du använder det med en generisk parametertyp med en union. Kompilatorn vet inte vilken konkret typ som ersätts som ett argument, därför fungerar Omit-hjälpen med unionen av DocA | DocB.
Som du förmodligen vet kan endast delade egenskaper nås på unioner av objekt (att göra annars skulle inte vara säkert), därför, efter att ha tagit bort de delade egenskaperna "id" och "createdAt", återstår ett tomt objekt (onlyDocA och onlyDocB är inte delade).
När det gäller varför att kombinera generiska med DocBase (<T extends Onlys> (fields: T): DocBase & T) inte förhindrar tillägg av överflödiga egenskaper, beror detta på att i avsaknad av en explicit parameteranteckning, härleds T från det argument som skickas in, till exempel:
/**
function createBaseDocument<{
onlyDocA: string;
whatever: string;
}>(fields: {
onlyDocA: string;
whatever: string;
}): DocBase & {
onlyDocA: string;
whatever: string;
}*/
const z = createBaseDocument({ onlyDocA: "a", whatever: "whenever" }); // ok
För att lösningen ska fungera måste du vara tydlig:
const x = createBaseDocument<OnlyDocA>({ onlyDocA: 'a' }); //ok
const y = createBaseDocument<OnlyDocA>({ onlyDocA: 'a', onlyDocB: 'b' ,onlyDocX: 'x' }); //error
Lekplats
