Ruft die Schlüssel einer Typescript-Schnittstelle als Array von Zeichenfolgen ab

96

Ich habe viele Tabellen in Lovefield und ihre jeweiligen Schnittstellen für die Spalten, die sie haben.
Beispiel:

export interface IMyTable {
  id: number;
  title: string;
  createdAt: Date;
  isDeleted: boolean;
}

Ich möchte die Eigenschaftsnamen dieser Schnittstelle in einem Array wie diesem haben:

const IMyTable = ["id", "title", "createdAt", "isDeleted"];

Ich kann kein Objekt / Array basierend auf der Schnittstelle IMyTabledirekt erstellen, was den Trick tun sollte, da ich die Schnittstellennamen der Tabellen dynamisch erhalten würde. Daher muss ich diese Eigenschaften in der Schnittstelle durchlaufen und ein Array daraus erstellen.

Wie erreiche ich dieses Ergebnis?

Tushar Shukla
quelle

Antworten:

49

Ab TypeScript 2.3 (oder sollte ich 2.4 sagen , da diese Funktion in 2.3 einen Fehler enthält, der in [email protected] behoben wurde ) können Sie einen benutzerdefinierten Transformator erstellen, um das zu erreichen, was Sie tun möchten.

Eigentlich habe ich schon einen solchen kundenspezifischen Transformator erstellt, der Folgendes ermöglicht.

https://github.com/kimamula/ts-transformer-keys

import { keys } from 'ts-transformer-keys';

interface Props {
  id: string;
  name: string;
  age: number;
}
const keysOfProps = keys<Props>();

console.log(keysOfProps); // ['id', 'name', 'age']

Leider sind kundenspezifische Transformatoren derzeit nicht so einfach zu bedienen. Sie müssen sie mit der TypeScript- Transformations-API verwenden, anstatt den Befehl tsc auszuführen. Es gibt ein Problem , bei dem eine Plugin-Unterstützung für benutzerdefinierte Transformatoren angefordert wird.

Kimamula
quelle
Vielen Dank für Ihre Antwort. Ich habe diesen benutzerdefinierten Transformator bereits gestern gesehen und installiert. Da hier jedoch Typoskript 2.4 verwendet wird, ist dies für mich derzeit nicht von Nutzen.
Tushar Shukla
16
Hallo, diese Bibliothek erfüllt auch genau meine Anforderungen, die ich jedoch erhalte, ts_transformer_keys_1.keys is not a functionwenn ich die genauen Schritte in der Dokumentation befolge. Gibt es eine Problemumgehung?
Hasitha Shan
Ordentlich! Denken Sie, dass es erweitert werden kann, um einen dynamischen Typparameter zu verwenden (Anmerkung 2 in der Readme-Datei)?
Kenshin
@ HasithaShan schauen Sie sich die Dokumente genau an - Sie müssen die TypeScript-Compiler-API verwenden, damit das Paket funktioniert
Yaroslav Bai
2
Leider ist das Paket kaputt, was auch immer ich tue, ich ts_transformer_keys_1.keys is not a function
bekomme
15

Im Folgenden müssen Sie die Schlüssel selbst auflisten, aber mindestens TypeScript erzwingt IUserProfileund verfügt IUserProfileKeysüber genau dieselben Schlüssel ( Required<T>wurde in TypeScript 2.8 hinzugefügt ):

export interface IUserProfile  {
  id: string;
  name: string;
};
type KeysEnum<T> = { [P in keyof Required<T>]: true };
const IUserProfileKeys: KeysEnum<IUserProfile> = {
  id: true,
  name: true,
};
Maciek Wawro
quelle
Ziemlich cooler Trick. Jetzt ist es einfach, die Implementierung aller Schlüssel von zu erzwingen, IUserProfileund es wäre einfach, sie aus der Konstante zu extrahieren IUserProfileKeys. Genau das habe ich gesucht. Sie müssen jetzt nicht alle meine Schnittstellen in Klassen konvertieren.
Anddo
9

Ich hatte ein ähnliches Problem, dass ich eine riesige Liste von Eigenschaften hatte, für die ich sowohl eine Schnittstelle als auch ein Objekt haben wollte.

HINWEIS: Ich wollte die Eigenschaften nicht zweimal schreiben (mit Tastatur eingeben)! Einfach trocken.


Hierbei ist zu beachten, dass Schnittstellen zur Kompilierungszeit erzwungene Typen sind, während Objekte meistens zur Laufzeit ausgeführt werden. ( Quelle )

Wie @derek in einer anderen Antwort erwähnt hat , kann der gemeinsame Nenner von Schnittstelle und Objekt eine Klasse sein, die sowohl einem Typ als auch einem Wert dient .

Also, TL; DR, der folgende Code sollte die Anforderungen erfüllen:

class MyTableClass {
    // list the propeties here, ONLY WRITTEN ONCE
    id = "";
    title = "";
    isDeleted = false;
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

// This is the pure interface version, to be used/exported
interface IMyTable extends MyTableClass { };

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

// Props type as an array, to be exported
type MyTablePropsArray = Array<keyof IMyTable>;

// Props array itself!
const propsArray: MyTablePropsArray =
    Object.keys(new MyTableClass()) as MyTablePropsArray;

console.log(propsArray); // prints out  ["id", "title", "isDeleted"]


// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

// Example of creating a pure instance as an object
const tableInstance: MyTableClass = { // works properly!
    id: "3",
    title: "hi",
    isDeleted: false,
};

( Hier ist der obige Code in Typescript Playground, um mehr zu spielen)

PS. Wenn Sie den Eigenschaften in der Klasse keine Anfangswerte zuweisen und beim Typ bleiben möchten, können Sie den Konstruktortrick ausführen:

class MyTableClass {
    // list the propeties here, ONLY WRITTEN ONCE
    constructor(
        readonly id?: string,
        readonly title?: string,
        readonly isDeleted?: boolean,
    ) {}
}

console.log(Object.keys(new MyTableClass()));  // prints out  ["id", "title", "isDeleted"] 

Konstruktor-Trick auf dem TypeScript-Spielplatz .

Hilfe bei der
quelle
Der propsArrayZugriff ist jedoch nur möglich, wenn Sie die Schlüssel initialisiert haben.
denkquer
Ich verstehe nicht, was du mit "initialisiert" @denkquer meinst. Im ersten Beispiel propsArraysteht es vor dem tableInstanceWenn Sie das meinen, also deutlich vor der Initialisierung der Instanz. Wenn Sie sich jedoch auf die in zugewiesenen gefälschten Werte beziehen MyTableClass, sind diese nur dazu da, kurz den "Typ" der Eigenschaften zu implizieren. Wenn Sie sie nicht möchten, können Sie den Konstruktortrick im PS-Beispiel verwenden.
Aidin
Nach meinem Verständnis wird ein Wert initialisiert, wenn er einen Wert hat. Ihr "Konstruktortrick" ist irreführend, da Sie den Trick nicht einfach durch den MyTableClassletzteren ersetzen können und erwarten, dass Sie Schlüssel erhalten, propsArraywenn nicht initialisierte Variablen und Typen zur Laufzeit entfernt werden. Sie müssen ihnen immer einen Standardwert geben. Ich habe festgestellt, dass das Initialisieren mit undefineddem besten Ansatz ist.
denkquer
@denkquer sorry, mein Konstruktor-Trick fehlte readonlyund ?auf den Parametern. Ich habe das gerade aktualisiert. Vielen Dank für den Hinweis. Jetzt denke ich, dass es so funktioniert, wie Sie es gesagt haben, irreführend ist und nicht funktionieren kann. :)
Aidin
8

Das sollte funktionieren

var IMyTable: Array<keyof IMyTable> = ["id", "title", "createdAt", "isDeleted"];

oder

var IMyTable: (keyof IMyTable)[] = ["id", "title", "createdAt", "isDeleted"];
Damathryx
quelle
10
Nicht, dass es falsch wäre, aber um hier klar zu sein, "erzwingen Sie nur die Werte des Arrays", um korrekt zu sein. Der Entwickler muss sie noch zweimal manuell aufschreiben.
Aidin
Während das, was Aidin sagte, in einigen Fällen wahr sein könnte, war dies genau das, wonach ich für meinen Fall gesucht hatte. Danke dir.
Daniel
4
Dies verhindert nicht, dass Schlüssel dupliziert werden oder Schlüssel fehlen. Gefällt var IMyTable: Array<keyof IMyTable> = ["id", "createdAt", "id"];
mir
Für mich war es auch das, wonach ich gesucht habe, weil ich optional die Schlüssel akzeptieren möchte, aber nicht mehr als die in der Benutzeroberfläche definierten Schlüssel. Ich habe nicht erwartet, dass dies mit dem obigen Code Standard ist. Ich denke, wir würden dafür immer noch einen gemeinsamen TS-Weg brauchen. Danke auf jeden Fall für den obigen Code!
Nicoes
6

IMyTableVersuchen Sie, es als Klasse zu definieren, anstatt es wie in der Schnittstelle zu definieren. In Typoskript können Sie eine Klasse wie eine Schnittstelle verwenden.

Definieren / generieren Sie Ihre Klasse für Ihr Beispiel folgendermaßen:

export class IMyTable {
    constructor(
        public id = '',
        public title = '',
        public createdAt: Date = null,
        public isDeleted = false
    )
}

Verwenden Sie es als Schnittstelle:

export class SomeTable implements IMyTable {
    ...
}

Schlüssel erhalten:

const keys = Object.keys(new IMyTable());
Derek
quelle
5

Sie müssen eine Klasse erstellen, die Ihre Schnittstelle implementiert, instanziieren und dann verwenden Object.keys(yourObject), um die Eigenschaften abzurufen.

export class YourClass implements IMyTable {
    ...
}

dann

let yourObject:YourClass = new YourClass();
Object.keys(yourObject).forEach((...) => { ... });
Dan Def
quelle
Funktioniert in meinem Fall nicht, ich müsste diese Eigenschaften der Schnittstelle auflisten, aber das ist nicht das, was ich will? Der Name der Schnittstelle kommt dynamisch und dann muss ich ihre Eigenschaften bestimmen
Tushar Shukla
Dies führt zu einem Fehler (v2.8.3): Für die Cannot extend an interface […]. Did you mean 'implements'?Verwendung implementsmuss jedoch die Schnittstelle manuell kopiert werden, was genau das ist, was ich nicht möchte.
Jacob
@jacob sorry, es hätte sein sollen implementsund ich habe meine Antwort aktualisiert. Wie @basarat angegeben hat, existieren Schnittstellen zur Laufzeit nicht. Die einzige Möglichkeit besteht darin, sie als Klasse zu implementieren.
Dan Def
Sie meinen, anstelle einer Schnittstelle eine Klasse verwenden? Leider kann ich nicht, da die Schnittstelle von einem Drittanbieter stammt ( @types/react). Ich habe sie manuell kopiert, aber das ist kaum zukunftssicher. Trying Ich versuche, Nicht-Lebenszyklus-Methoden (die bereits gebunden sind) dynamisch zu binden, aber sie sind nicht in React.Component (der Klasse) deklariert.
Jacob
Nein, ich meine, erstellen Sie eine Klasse, die Ihre Schnittstelle von Drittanbietern implementiert, und rufen Sie zur Laufzeit die Eigenschaften dieser Klasse ab.
Dan Def
4

Kippen. Schnittstellen existieren zur Laufzeit nicht.

Problemumgehung

Erstellen Sie eine Variable des Typs und verwenden Sie Object.keyssie 🌹

Basarat
quelle
1
Meinen Sie so:var abc: IMyTable = {}; Object.keys(abc).forEach((key) => {console.log(key)});
Tushar Shukla
4
Nein, weil dieses Objekt keine Schlüssel hat. Eine Schnittstelle wird von TypeScript verwendet, verdunstet jedoch im JavaScript, sodass keine Informationen mehr übrig sind, die über "Reflexion" oder "Intersektion" informieren. Alles, was JavaScript weiß, ist, dass es ein leeres Objektliteral gibt. Ihre einzige Hoffnung besteht darin, darauf zu warten (oder dies anzufordern ). TypeScript bietet eine Möglichkeit, ein Array oder Objekt mit allen Schlüsseln in der Schnittstelle in den Quellcode zu generieren. Oder, wie Dan Def sagt, wenn Sie eine Klasse verwenden können, werden die Schlüssel in jedem Fall in Form von Eigenschaften definiert.
Jesper
1
In dieser Zeile wird auch ein Fehler von TypeScript angezeigt: var abc: IMyTable = {}Da das leere Objektliteral nicht der Form dieser Schnittstelle entspricht.
Jesper
16
Wenn dies nicht funktioniert, warum gibt es zu dieser Antwort positive Stimmen?
Dawez
1
Downvote Grund: Keine Erwähnung, dass es nicht für nullbare Werte
funktioniert
4

Vielleicht ist es zu spät, aber in Version 2.1 des Typoskripts können Sie Folgendes verwenden key of:

interface Person {
    name: string;
    age: number;
    location: string;
}

type K1 = keyof Person; // "name" | "age" | "location"
type K2 = keyof Person[];  // "length" | "push" | "pop" | "concat" | ...
type K3 = keyof { [x: string]: Person };  // string

Doc: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#keyof-and-lookup-types

Nathan Gabriel
quelle
Vielen Dank für die Antwort, aber nicht sicher, ob es jemandem hilft, statisch erstellte Typen über die Benutzeroberfläche zu verwenden. IMHO können wir Schnittstellen / Typen in den meisten Fällen austauschbar verwenden. Außerdem würde dies die manuelle Erstellung von Typen für mehrere Schnittstellen erfordern. Die Lösung sieht jedoch gut aus, wenn jemand nur Typen aus einer Schnittstelle herausholen muss.
Tushar Shukla
3

Es gibt keine einfache Möglichkeit, ein Schlüsselarray über eine Schnittstelle zu erstellen. Typen werden zur Laufzeit gelöscht und Objekttypen (ungeordnet, benannt) können nicht ohne irgendeine Art von Hack in Tupeltypen (geordnet, unbenannt) konvertiert werden.


Option 1: Manueller Ansatz

// Record type ensures, we have no double or missing keys, values can be neglected
function createKeys(keyRecord: Record<keyof IMyTable, any>): (keyof IMyTable)[] {
  return Object.keys(keyRecord) as any
}

const keys = createKeys({ isDeleted: 1, createdAt: 1, title: 1, id: 1 })
// const keys: ("id" | "title" | "createdAt" | "isDeleted")[]

(+) einfacher (-) Array-Rückgabetyp, kein Tupel (+ -) manuelles Schreiben mit automatischer Vervollständigung

Erweiterung: Wir könnten versuchen, ausgefallen zu sein und rekursive Typen zu verwenden, um ein Tupel zu generieren. Dies funktionierte nur für ein paar Requisiten (~ 5,6), bis sich die Leistung massiv verschlechterte. Tief verschachtelte rekursive Typen werden von TS auch nicht offiziell unterstützt . Der Vollständigkeit halber liste ich dieses Beispiel hier auf.


Option 2: Codegenerator basierend auf der TS-Compiler-API ( ts-morph )

// ./src/mybuildstep.ts
import {Project, VariableDeclarationKind, InterfaceDeclaration } from "ts-morph";

const project = new Project();
// source file with IMyTable interface
const sourceFile = project.addSourceFileAtPath("./src/IMyTable.ts"); 
// target file to write the keys string array to
const destFile = project.createSourceFile("./src/generated/IMyTable-keys.ts", "", {
  overwrite: true // overwrite if exists
}); 

function createKeys(node: InterfaceDeclaration) {
  const allKeys = node.getProperties().map(p => p.getName());
  destFile.addVariableStatement({
    declarationKind: VariableDeclarationKind.Const,
    declarations: [{
        name: "keys",
        initializer: writer =>
          writer.write(`${JSON.stringify(allKeys)} as const`)
    }]
  });
}

createKeys(sourceFile.getInterface("IMyTable")!);
destFile.saveSync(); // flush all changes and write to disk

Nachdem wir diese Datei kompiliert und ausgeführt haben tsc && node dist/mybuildstep.js, wird eine Datei ./src/generated/IMyTable-keys.tsmit folgendem Inhalt generiert:

// ./src/generated/IMyTable-keys.ts
const keys = ["id","title","createdAt","isDeleted"] as const;

(+) automatische Lösung (+) exakter Tupeltyp (-) erfordert Build-Schritt


PS: Ich habe mich entschieden ts-morph, da es eine einfache Alternative zur ursprünglichen TS-Compiler-API ist.

ford04
quelle
-1

Du kannst es nicht tun. Zur Laufzeit gibt es keine Schnittstellen (wie @basarat sagte).

Jetzt arbeite ich mit Folgendem:

const IMyTable_id = 'id';
const IMyTable_title = 'title';
const IMyTable_createdAt = 'createdAt';
const IMyTable_isDeleted = 'isDeleted';

export const IMyTable_keys = [
  IMyTable_id,
  IMyTable_title,
  IMyTable_createdAt,
  IMyTable_isDeleted,
];

export interface IMyTable {
  [IMyTable_id]: number;
  [IMyTable_title]: string;
  [IMyTable_createdAt]: Date;
  [IMyTable_isDeleted]: boolean;
}
Eduardo Cuomo
quelle
Stellen Sie sich vor, Sie haben nur viele Modelle und tun es für alle ... es ist so teure Zeit.
William Cuervo
-6
// declarations.d.ts
export interface IMyTable {
      id: number;
      title: string;
      createdAt: Date;
      isDeleted: boolean
}
declare var Tes: IMyTable;
// call in annother page
console.log(Tes.id);
Brillian Andrie Nugroho Wiguno
quelle
1
Dieser Code funktioniert nicht, da die Typoskript-Syntax zur Laufzeit nicht verfügbar ist. Wenn Sie diesen Code auf einem Typoskript-Spielplatz überprüfen, werden Sie feststellen, dass das einzige, was mit JavaScript kompiliert wird console.log(Tes.id), natürlich der Fehler "Nicht erfasster Referenzfehler: Tes ist nicht definiert" ist
Tushar Shukla