Obtenir les clés d'une interface Typescript sous forme de tableau de chaînes


104

J'ai beaucoup de tables dans Lovefield et leurs interfaces respectives pour les colonnes qu'elles ont.
Exemple:

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

J'aimerais avoir les noms de propriété de cette interface dans un tableau comme celui-ci:

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

je ne peux pas créer un objet / tableau basé IMyTabledirectement sur l'interface, ce qui devrait faire l'affaire car j'obtiendrais les noms d'interface des tables de manière dynamique. Par conséquent, je dois parcourir ces propriétés dans l'interface et en extraire un tableau.

Comment obtenir ce résultat?

Réponses:


52

À partir de TypeScript 2.3 (ou devrais-je dire 2.4 , comme dans 2.3 cette fonctionnalité contient un bogue qui a été corrigé dans typescript@2.4-dev ), vous pouvez créer un transformateur personnalisé pour réaliser ce que vous voulez faire.

En fait, j'ai déjà créé un tel transformateur personnalisé, qui permet ce qui suit.

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']

Malheureusement, les transformateurs personnalisés ne sont actuellement pas si faciles à utiliser. Vous devez les utiliser avec l' API de transformation TypeScript au lieu d'exécuter la commande tsc. Il y a un problème concernant la prise en charge d'un plugin pour les transformateurs personnalisés.


Merci pour votre réponse, j'ai déjà vu et installé ce transformateur personnalisé hier, mais comme il utilise du typographie 2.4, cela ne me sert à rien pour le moment.
Tushar Shukla

16
Salut, cette bibliothèque répond exactement à mes besoins, cependant, je reçois ts_transformer_keys_1.keys is not a functionquand je suis les étapes exactes de la documentation. y a-t-il une solution de contournement?
Hasitha Shan

Soigné! Pensez-vous qu'il peut être étendu pour prendre un paramètre de type dynamique (note 2 dans le readme)?
kenshin le

@HasithaShan regarde de près dans la documentation - vous devez utiliser l'API du compilateur TypeScript pour que le package fonctionne
Yaroslav Bai

2
Malheureusement, le paquet est cassé, quoi que je fasse, je reçois toujoursts_transformer_keys_1.keys is not a function
fr1sk

17

Ce qui suit vous oblige à lister les clés vous-même, mais au moins TypeScript appliquera IUserProfileet IUserProfileKeysaura exactement les mêmes clés (a Required<T>été ajouté dans TypeScript 2.8 ):

export interface IUserProfile  {
  id: string;
  name: string;
};
type KeysEnum<T> = { [P in keyof Required<T>]: true };
const IUserProfileKeys: KeysEnum<IUserProfile> = {
  id: true,
  name: true,
};

Truc assez cool. Désormais, il est facile d'implémenter toutes les clés de IUserProfileet il serait facile de les extraire du const IUserProfileKeys. C'est exactement ce que je cherchais. Pas besoin de convertir toutes mes interfaces en classes maintenant.
Anddo le

13

J'ai eu un problème similaire: j'avais une liste géante de propriétés que je voulais avoir à la fois une interface et un objet en dehors.

REMARQUE: je ne voulais pas écrire (taper avec le clavier) les propriétés deux fois! Juste SEC.


Une chose à noter ici est que les interfaces sont des types imposés au moment de la compilation, tandis que les objets sont principalement exécutés. ( Source )

Comme @derek l'a mentionné dans une autre réponse , le dénominateur commun de l' interface et de l' objet peut être une classe qui sert à la fois un type et une valeur .

Donc, TL; DR, le morceau de code suivant devrait satisfaire les besoins:

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,
};

( Voici le code ci-dessus dans Typescript Playground pour jouer plus)

PS. Si vous ne voulez pas affecter de valeurs initiales aux propriétés de la classe et rester avec le type, vous pouvez faire l'astuce du constructeur:

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"] 

Constructor Trick dans TypeScript Playground .


Le propsArrayseul est accessible lors de l' initialisation des clés cependant.
denkquer le

Je ne comprends pas ce que vous entendez par @denkquer "initialisé". Dans le premier exemple, propsArrayest disponible avant le tableInstancesi c'est ce que vous voulez dire, donc clairement avant l'initialisation de l'instance. Cependant, si vous faites référence aux fausses valeurs assignées dans MyTableClass, celles-ci sont juste là pour impliquer brièvement le «type» des propriétés. Si vous ne les voulez pas, vous pouvez utiliser l'astuce du constructeur dans l'exemple PS.
Aidin

1
Dans ma compréhension, une valeur est initialisée lorsqu'elle a une valeur. Votre "astuce du constructeur" est trompeuse car vous ne pouvez pas simplement remplacer le MyTableClasspar le dernier et vous attendre à recevoir des clés dans les propsArrayvars non initialisés et les types sont supprimés au moment de l'exécution. Vous devez toujours leur fournir une sorte de valeur par défaut. J'ai trouvé que les initialiser avec undefinedest la meilleure approche.
denkquer le

@denkquer désolé, mon astuce de constructeur manquait readonlyet ?sur les paramètres. Je viens de mettre à jour cela. Merci de l'avoir signalé. Maintenant, je pense que cela fonctionne comme vous l'avez dit, c'est trompeur et ne peut pas fonctionner. :)
Aidin le

1
@Aidin merci pour votre solution. Je me demande également si je peux éviter l'initialisation des paramètres. Si j'utilise l'astuce du constructeur, je ne peux plus créer une interface qui étend MyTableClass .. votre astuce de constructeur dans le lien du terrain de jeu dactylographié est vide cependant
Flion

10

Cela devrait fonctionner

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

ou

var IMyTable: (keyof IMyTable)[] = ["id", "title", "createdAt", "isDeleted"];

11
Non pas que ce soit faux, mais pour être clair ici, vous devez simplement "appliquer les valeurs du tableau" pour être correct. Le développeur doit encore les noter deux fois, manuellement.
Aidin le

Bien que ce que Aidin ait dit puisse être vrai, dans certains cas, c'était exactement ce que je recherchais, pour mon cas. Je vous remercie.
Daniel

4
Cela n'empêchera pas les doublons de clés ou les clés manquantes. J'aimevar IMyTable: Array<keyof IMyTable> = ["id", "createdAt", "id"];
ford04

Pour moi, c'était aussi ce que je recherchais car je veux éventuellement accepter les clés mais rien de plus que les clés définies dans l'interface. Je ne m'attendais pas à ce que ce soit par défaut avec le code ci-dessus. Je suppose que nous aurions encore besoin d'un moyen TS commun pour cela. Merci en tout cas pour le code ci-dessus!
nicoes le

8

Il est peut-être trop tard, mais dans la version 2.1 de dactylographié, vous pouvez utiliser key ofcomme ceci:

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


Merci pour la réponse mais je ne sais pas si cela aide quelqu'un à utiliser des types créés statiquement à partir de l'interface. À mon humble avis, nous pouvons utiliser les interfaces / types de manière interchangeable dans la plupart des cas. De plus, cela nécessiterait la création manuelle de types pour plusieurs interfaces. Cependant, la solution semble bonne si quelqu'un a juste besoin d'extraire des types d'une interface.
Tushar Shukla le

6

Au lieu de définir IMyTablecomme dans l'interface, essayez de le définir comme une classe. En typographie, vous pouvez utiliser une classe comme une interface.

Donc, pour votre exemple, définissez / générez votre classe comme ceci:

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

Utilisez-le comme interface:

export class SomeTable implements IMyTable {
    ...
}

Obtenez les clés:

const keys = Object.keys(new IMyTable());

5

Vous devrez créer une classe qui implémente votre interface, l'instancier puis utiliser Object.keys(yourObject) pour obtenir les propriétés.

export class YourClass implements IMyTable {
    ...
}

puis

let yourObject:YourClass = new YourClass();
Object.keys(yourObject).forEach((...) => { ... });

Cela ne fonctionne pas dans mon cas, je devrais lister ces propriétés de l'interface mais ce n'est pas ce que je veux? Le nom de l'interface vient dynamiquement et ensuite je dois déterminer ses propriétés
Tushar Shukla

Cela produit une erreur (v2.8.3): Cannot extend an interface […]. Did you mean 'implements'?Cependant, utiliser à la implementsplace nécessite de copier manuellement l'interface, ce qui est exactement ce que je ne veux pas.
Jacob

@jacob désolé, ça aurait dû être implementset j'ai mis à jour ma réponse. Comme @basarat l'a indiqué, les interfaces n'existent pas au moment de l'exécution, le seul moyen est donc de l'implémenter en tant que classe.
Dan Def

Vous voulez dire qu'au lieu d'une interface, utilisez une classe? Malheureusement, je ne peux pas car l'interface provient d'un tiers ( @types/react). Je les ai copiés manuellement, mais ce n'est guère à l'épreuve du temps 😪 J'essaie de lier dynamiquement des méthodes non liées au cycle de vie (qui sont déjà liées), mais elles ne sont pas déclarées sur React.Component (la classe).
Jacob

Non, je veux dire créer une classe qui implémente votre interface tierce et obtenir les propriétés de cette classe au moment de l'exécution.
Dan Def

5

Il n'y a pas de moyen simple de créer un tableau de clés à partir d'une interface. Les types sont effacés au moment de l'exécution et les types d'objets (non ordonnés, nommés) ne peuvent pas être convertis en types tuple (ordonnés, sans nom) sans une sorte de piratage.


Option 1: approche manuelle

// 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")[]

(+) type de retour de tableau simple (-), pas d'écriture manuelle de tuple (+ -) avec auto-complétion

Extension: Nous pourrions essayer d'être sophistiqués et utiliser des types récursifs pour générer un tuple. Cela n'a fonctionné pour moi que pour quelques accessoires (~ 5,6) jusqu'à ce que les performances se dégradent massivement. Les types récursifs profondément imbriqués ne sont pas non plus officiellement pris en charge par TS - je liste cet exemple ici par souci d'exhaustivité.


Option 2: Générateur de code basé sur l'API du compilateur TS ( 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

Après avoir compilé et exécuté ce fichier avec tsc && node dist/mybuildstep.js, un fichier ./src/generated/IMyTable-keys.tsavec le contenu suivant est généré:

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

(+) solution automatique (+) type de tuple exact (-) nécessite une étape de construction


PS: J'ai choisi ts-morph, car c'est une alternative simple à l'API du compilateur TS d'origine.


4

Je ne peux pas. Les interfaces n'existent pas au moment de l'exécution.

solution de contournement

Créez une variable du type et utilisez Object.keys-la 🌹


1
Voulez-vous dire comme ça:var abc: IMyTable = {}; Object.keys(abc).forEach((key) => {console.log(key)});
Tushar Shukla

4
Non, car cet objet n'a pas de clé dessus. Une interface est quelque chose que TypeScript utilise mais s'évapore dans le JavaScript, il n'y a donc plus d'informations pour informer une "réflexion" ou une "interspection". Tout ce que JavaScript sait, c'est qu'il existe un littéral d'objet vide. Votre seul espoir est d'attendre (ou de demander que ) TypeScript inclut un moyen de générer un tableau ou un objet avec toutes les clés de l'interface dans le code source. Ou, comme le dit Dan Def, si vous pouvez utiliser une classe, vous aurez les clés définies sous la forme de propriétés dans chaque instance ..
Jesper

1
Vous obtiendrez également une erreur de TypeScript sur cette ligne: var abc: IMyTable = {}parce que le littéral d'objet vide n'est pas conforme à la forme de cette interface.
Jesper

16
Si cela ne fonctionne pas, pourquoi y a-t-il des votes positifs sur cette réponse?
dawez

1
raison du
vote négatif

-1

Vous ne pouvez pas le faire. Les interfaces n'existent pas au moment de l'exécution (comme @basarat l'a dit).

Maintenant, je travaille avec:

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;
}

Imaginez simplement avoir beaucoup de modèles, et faites-le pour tout le monde ... c'est tellement cher.
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);

1
Ce code ne fonctionnera pas car la syntaxe dactylographiée n'est pas disponible au moment de l'exécution. Si vous vérifiez ce code sur le terrain de jeu dactylographié, vous remarquerez que la seule chose qui compile en JavaScript est console.log(Tes.id)bien sûr l'erreur `` Uncaught ReferenceError: Tes is not defined ''
Tushar Shukla
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.