Hur begränsar man nycklarna i ett TypeScript-objekt samtidigt som man härleder värdena?
Ofta, när jag har en union av strängliteraler i TypeScript, vill jag skapa en karta från strängarna till andra värden. Detta involverar att skapa en typ som inte låter värdena härledas, vilket kan vara irriterande när jag också vill få åtkomst till värdetyperna. Till exempel, här är ett grundläggande problem:
type UserPersona =
| "entrepreneur"
| "programmer"
| "designer"
| "product_manager"
| "marketing_sales"
| "customer_support"
| "operations_hr"
const userPersonaDisplayNames: { [key in UserPersona]: string } = {
entrepreneur: "Entrepreneur",
programmer: "Programmer",
designer: "Designer",
product_manager: "Product Manager",
marketing_sales: "Marketing & Sales",
customer_support: "Customer Support",
operations_hr: "Operations & HR",
}
Problemet här är att värdena för userPersonaDisplayNames är av typen string och inte deras bokstavliga värden.
När jag utelämnar typen är nycklarna och värdena bokstavliga typer vilket är bra, men nycklarna är inte längre begränsade till unionstypen.
const userPersonaDisplayNames = {
entrepreneur: "Entrepreneur",
programmer: "Programmer",
designer: "Designer",
product_manager: "Product Manager",
marketing_sales: "Marketing & Sales",
customer_support: "Customer Support",
operations_hr: "Operations & HR",
}
Ett knep jag har upptäckt är att använda extends för att påstå.
type Assert<A,B> = A extends B
type assertPersonaKeys = Assert<keyof userPersonaDisplayNames, UserPersona>
Det här fungerar, men det är ganska äckligt. Jag har en oanvänd typ liggande som TSLint klagar på.
Det verkar som att infer-nyckelordet är precis vad jag letar efter, men jag är inte säker på hur det fungerar och det fungerar inte i mitt fall. Idealiskt sett skulle jag kunna göra något sådant här:
const userPersonaDisplayNames: { [key in UserPersona]: infer } = {
entrepreneur: "Entrepreneur",
programmer: "Programmer",
designer: "Designer",
product_manager: "Product Manager",
marketing_sales: "Marketing & Sales",
customer_support: "Customer Support",
operations_hr: "Operations & HR",
}
Har du några idéer om hur man kan lösa det här problemet på ett smidigt sätt?
Svar #1
Använd en hjälpfunktion för att representera begränsningen du vill se och kalla sedan på den? Det finns en mycket liten mängd körningstidsoverhead, eftersom det anropar (t => t)(obj) istället för att bara använda obj.
const asUserPersonaDisplayNames = <
S extends string, // allow S to be inferred as a string literal
T extends Record<UserPersona, S> & // require keys from UserPersona
Record<Exclude<keyof T, UserPersona>, never> // disallow keys not from UserPersona
>(t: T) => t
const userPersonaDisplayNames = asUserPersonaDisplayNames({
entrepreneur: "Entrepreneur",
programmer: "Programmer",
designer: "Designer",
product_manager: "Product Manager",
marketing_sales: "Marketing & Sales",
customer_support: "Customer Support",
operations_hr: "Operations & HR",
}); // type has fully string-literal values
const missing = asUserPersonaDisplayNames({
entrepreneur: "Entrepreneur",
programmer: "Programmer",
product_manager: "Product Manager",
marketing_sales: "Marketing & Sales",
customer_support: "Customer Support",
operations_hr: "Operations & HR",
}) // error, property "designer" is missing
const extra = asUserPersonaDisplayNames({
candlestick_maker: "Rub a Dub Dub",
entrepreneur: "Entrepreneur",
programmer: "Programmer",
designer: "Designer",
product_manager: "Product Manager",
marketing_sales: "Marketing & Sales",
customer_support: "Customer Support",
operations_hr: "Operations & HR",
}); // error, property "candlestick_maker" is extra
const notString = asUserPersonaDisplayNames({
entrepreneur: 25,
programmer: "Programmer",
designer: "Designer",
product_manager: "Product Manager",
marketing_sales: "Marketing & Sales",
customer_support: "Customer Support",
operations_hr: "Operations & HR",
}); // error, number is not expected
Hoppas det hjälper; lycka till!
