typescript - Hämta JavaScript-gränssnittets nycklar som sträng (som en Enum)


Min begäran är faktiskt väldigt enkel men jag har nästan inte hittat något av den sorten online. Föreställ dig detta, du har ett gränssnitt som så:

interface MyInterface: {
  propertyA: number;
  propertyB: string;
  propertyC: boolean;
}

Nu vill du använda namnet på ett fält för att dynamiskt komma åt dess värde, så du gör något som object['propertyA'], där object är av typen MyInterface.

Att däremot skicka 'propertyA' som en rå sträng är opraktiskt och farligt eftersom om du byter namn på den eller ändrar gränssnittet så fungerar inte koden. (Och den plockas inte upp av TypeScript, men jag är inte säker på om det är möjligt ändå).

Idealiskt skulle jag vilja göra något som detta MyInterface.propertyA, jag blev ledsen över att detta verkar inte existera, jag ser ingen anledning till varför det inte borde.

Och nu, innan du säger att det är vad object.propertyA är till för, har jag förenklat konceptet för att göra det lätt att förstå, men användningsfallet är inte exakt detta. Jag kan skicka fält från ett gränssnitt som strängar från React-komponenter inom JSX för att använda en återanvändbar metod, eller så kan jag kontrollera om en sträng matchar en delmängd av nycklar i ett gränssnitt för att visa felmeddelanden därefter.

Att autogenerera en enum från gränssnittet/klassen är inte en dålig lösning även om det inte är idealiskt eftersom det fortfarande medför en overhead, men så länge det inte behöver uppdateras varje gång gränssnittet ändras och det inte beror på typen av gränssnittets egenskaper så är det inte dåligt.

Svar #1

Gränssnitt existerar inte vid körning, så det finns inget sätt att få egenskapsnamnen för ett gränssnitt som värden vid körning. Vad du kan göra, däremot, är att gå åt andra hållet: Ha ett "modell"-objekt:

const myInterfaceModel = {
    propertyA: 42,
    propertyB: "x",
    propertyC: true,
};

Definiera sedan gränssnittet med det objektet:

type MyInterface = typeof myInterfaceModel;

Det definierar MyInterface precis som det är i din fråga.

Then you can create an object that has the property names as strings, like this:

const myInterfaceKeys = Object.fromEntries(
    Object.keys(myInterfaceModel).map((key) => [key, key])
);

Nu, om vi bara gjorde det, skulle myInterfaceKeys vara (i praktiken) Record<string, string>, vilket inte är användbart för utvecklarupplevelsen. Men vi kan göra bättre genom att använda en mappad typ på det:

type KeyNames<T> = {
    [Key in keyof T]: Key;
};
// ...
const myInterfaceKeys = Object.fromEntries(
    Object.keys(myInterfaceModel).map((key) => [key, key])
) as KeyNames<MyInterface>;

Nu matchar dess typ dess körningstidvärde, och vi får användbar automatisk ifyllning:

Exempel på användning (bara en grundläggande funktion, men skulle lika gärna kunna vara en React-komponent):

function example(obj: MyInterface, key: keyof MyInterface) {
    console.log(obj[key]);
}

declare let exampleObject: MyInterface;

example(exampleObject, myInterfaceKeys.propertyA);

Här är allt tillsammans (playground-länk):

type KeyNames<T> = {
    [Key in keyof T]: Key;
};

const myInterfaceModel = {
    propertyA: 42,
    propertyB: "x",
    propertyC: true,
};
type MyInterface = typeof myInterfaceModel;
//   ^? type MyInterface = { propertyA: number, propertyB: string; propertyC : boolean }

const myInterfaceKeys = Object.fromEntries(
    Object.keys(myInterfaceModel).map((key) => [key, key])
) as KeyNames<MyInterface>;

myInterfaceKeys
//             ^−−−− Type a period after this to see the autocompletion

Vanligtvis skulle jag bara använda strängliteraler (example(exampleObject, "propertyA")) eftersom IDE:n kommer att erbjuda lämpliga val, och eftersom keyof MyInterface är restriktivt, om du tar bort en egenskap skulle all kod som refererade till den med en strängliteral sluta kompileras. Men om du vill ha det objektet med nycklarna på det, kan du göra ovanstående för att uppnå det. (Jag provade också just nu, och om du använder ditt objekt med nycklar, fungerar att byta namn på en egenskap överallt, medan strängliteralerna inte gjorde det.)

Svar #2

Letar du efter något som detta?

enum Direction {
  Up = "u",
  Down = "d",
  Left = "l",
  Right = "r",
}

interface MyDirection  {
    [Direction.Up]:string;
    [Direction.Down]:number;
}

const obj:MyDirection = {
    [Direction.Up]: 'up',
    [Direction.Down]:12,

}


console.log(obj[Direction.Down], Direction.Down)