Ottieni le chiavi di un'interfaccia Typescript come array di stringhe


103

Ho molte tabelle in Lovefield e le rispettive interfacce per le colonne che hanno.
Esempio:

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

Mi piacerebbe avere i nomi delle proprietà di questa interfaccia in un array come questo:

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

non posso creare un oggetto / array basato IMyTabledirettamente sull'interfaccia che dovrebbe fare il trucco perché otterrei dinamicamente i nomi dell'interfaccia delle tabelle. Quindi ho bisogno di iterare su queste proprietà nell'interfaccia e ricavarne un array.

Come ottengo questo risultato?

Risposte:


52

A partire da TypeScript 2.3 (o dovrei dire 2.4 , poiché in 2.3 questa funzione contiene un bug che è stato corretto in typescript@2.4-dev ), puoi creare un trasformatore personalizzato per ottenere ciò che vuoi fare.

In realtà, ho già creato un trasformatore personalizzato, che consente quanto segue.

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

Sfortunatamente, i trasformatori personalizzati attualmente non sono così facili da usare. Devi usarli con l' API di trasformazione TypeScript invece di eseguire il comando tsc. Si è verificato un problema durante la richiesta di un supporto plug-in per trasformatori personalizzati.


Grazie per la tua risposta, ho già visto e installato questo trasformatore personalizzato ieri, ma poiché utilizza il dattiloscritto 2.4, questo non mi serve al momento.
Tushar Shukla

16
Ciao, questa libreria soddisfa esattamente anche le mie esigenze, tuttavia, ottengo ts_transformer_keys_1.keys is not a functionquando seguo i passaggi esatti nella documentazione. c'è una soluzione alternativa a questo?
Hasitha Shan

Neat! Pensi che possa essere esteso per prendere un parametro di tipo dinamico (nota 2 nel file readme)?
kenshin

@HasithaShan esamina da vicino i documenti: devi usare l'API del compilatore TypeScript per far funzionare il pacchetto
Yaroslav Bai

2
Sfortunatamente, il pacchetto è rotto, qualunque cosa faccia la ricevo semprets_transformer_keys_1.keys is not a function
dal

17

Quanto segue richiede di elencare le chiavi da solo, ma almeno TypeScript applicherà IUserProfilee IUserProfileKeysavrà le stesse identiche chiavi (è Required<T>stato aggiunto in 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,
};

Trucco piuttosto interessante. Ora è facile imporre l'implementazione di tutte le chiavi di IUserProfilee sarebbe facile estrarle dal const IUserProfileKeys. Questo è esattamente quello che stavo cercando. Non c'è bisogno di convertire tutte le mie interfacce in classi ora.
Anddo

13

Ho avuto un problema simile che avevo un enorme elenco di proprietà che volevo avere sia un'interfaccia che un oggetto da essa.

NOTA: non volevo scrivere (digitare con la tastiera) le proprietà due volte! Solo ASCIUTTO.


Una cosa da notare qui è che le interfacce sono tipi applicati in fase di compilazione, mentre gli oggetti sono per lo più in fase di esecuzione. ( Fonte )

Come @derek ha menzionato in un'altra risposta , il denominatore comune di interfaccia e oggetto può essere una classe che serve sia un tipo che un valore .

Quindi, TL; DR, il seguente pezzo di codice dovrebbe soddisfare le esigenze:

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

( Ecco il codice sopra in Typescript Playground per giocare di più)

PS. Se non vuoi assegnare valori iniziali alle proprietà nella classe e rimanere con il tipo, puoi eseguire il trucco del costruttore:

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

Trucco del costruttore in TypeScript Playground .


Tuttavia, propsArrayè accessibile solo quando hai inizializzato le chiavi.
denkquer

Non capisco cosa intendi per "inizializzato" @denkquer. Nel primo esempio, propsArrayè disponibile prima del tableInstancese questo è ciò che intendi, quindi chiaramente prima dell'inizializzazione dell'istanza. Tuttavia, se ti riferisci ai valori falsi assegnati in MyTableClass, quelli sono lì solo per indicare brevemente il "tipo" delle proprietà. Se non li vuoi, puoi usare il trucco del costruttore nell'esempio PS.
Aidin

1
Nella mia comprensione un valore viene inizializzato quando ha un valore. Il tuo "trucco del costruttore" è fuorviante perché non puoi semplicemente sostituire il MyTableClasscon quest'ultimo e aspettarti di ricevere le chiavi nelle propsArrayvariabili e i tipi non inizializzati vengono rimossi in fase di esecuzione. Devi sempre fornire loro una sorta di valore predefinito. Ho scoperto che inizializzarli con undefinedè l'approccio migliore.
denkquer

@denkquer mi dispiace, il mio trucco da Costruttore mancava readonlye ?sui parametri. L'ho appena aggiornato. Grazie per avermelo fatto notare. Ora penso che funzioni nel modo in cui hai detto che è fuorviante e non può funzionare. :)
Aidin

@Aidin grazie per la tua soluzione. Mi chiedo anche se posso evitare l'inizializzazione dei parametri. Se uso il trucco del costruttore non riesco più a creare un'interfaccia che estenda MyTableClass .. il tuo trucco del costruttore nel collegamento del campo giochi dattiloscritto è vuoto però
Flion

10

Questo dovrebbe funzionare

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

o

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

11
Non che sia sbagliato, ma per essere chiari qui stai solo "imponendo i valori dell'array" per essere corretti. Lo sviluppatore deve ancora scriverli due volte, manualmente.
Aidin

Mentre quello che ha detto Aidin potrebbe essere vero, per alcuni casi, questo era esattamente quello che stavo cercando, per il mio caso. Grazie.
Daniel

4
Ciò non impedirà duplicati di chiavi o chiavi mancanti. Mi piacevar IMyTable: Array<keyof IMyTable> = ["id", "createdAt", "id"];
ford04

Per me era anche quello che cercavo perché volevo accettare facoltativamente le chiavi ma nient'altro che le chiavi definite nell'interfaccia. Non mi aspettavo che fosse predefinito con il codice sopra. Immagino che avremmo ancora bisogno di un metodo TS comune per questo. Grazie in ogni caso per il codice sopra!
Nicoes il

8

Forse è troppo tardi, ma nella versione 2.1 del dattiloscritto puoi usare in key ofquesto modo:

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


Grazie per la risposta ma non sono sicuro se aiuta qualcuno a utilizzare i tipi creati staticamente dall'interfaccia. IMHO, possiamo usare interfacce / tipi in modo intercambiabile nella maggior parte dei casi. Inoltre, ciò richiederebbe la creazione manuale di tipi per più interfacce. Tuttavia, la soluzione sembra buona se qualcuno ha solo bisogno di estrarre i tipi da un'interfaccia.
Tushar Shukla

6

Invece di definire IMyTablecome interfaccia, prova a definirlo come una classe. Nel dattiloscritto puoi usare una classe come un'interfaccia.

Quindi, per il tuo esempio, definisci / genera la tua classe in questo modo:

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

Usalo come interfaccia:

export class SomeTable implements IMyTable {
    ...
}

Ottieni chiavi:

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

5

Dovrai creare una classe che implementa la tua interfaccia, istanziarla e quindi usarla Object.keys(yourObject)per ottenere le proprietà.

export class YourClass implements IMyTable {
    ...
}

poi

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

Non funziona nel mio caso, dovrei elencare quelle proprietà dell'interfaccia ma non è quello che voglio? Il nome dell'interfaccia arriva dinamicamente e quindi devo determinarne le proprietà
Tushar Shukla,

Questo produce un errore (v2.8.3): Cannot extend an interface […]. Did you mean 'implements'?Tuttavia, l'utilizzo implementsinvece richiede la copia manuale dell'interfaccia, che è esattamente quello che non voglio.
jacob

@jacob scusa, avrebbe dovuto essere implementse ho aggiornato la mia risposta. Come ha affermato @basarat, le interfacce non esistono in fase di esecuzione, quindi l'unico modo è implementarle come classe.
Dan Def

Intendi invece che un'interfaccia usi una classe? Purtroppo non posso in quanto l'interfaccia proviene da una terza parte ( @types/react). Li ho copiati manualmente, ma non è certo a prova di futuro 😪 Sto cercando di associare dinamicamente metodi non del ciclo di vita (che sono già associati), ma non sono dichiarati su React.Component (la classe).
jacob

No, intendo creare una classe che implementa la tua interfaccia di terze parti e ottenere le proprietà di quella classe in fase di esecuzione.
Dan Def

5

Non esiste un modo semplice per creare un array di chiavi da un'interfaccia. I tipi vengono cancellati in fase di esecuzione e i tipi di oggetto (non ordinati, denominati) non possono essere convertiti in tipi di tupla (ordinati, senza nome) senza una sorta di hack.


Opzione 1: approccio manuale

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

(+) semplice (-) tipo restituito da array, nessuna scrittura manuale di tupla (+ -) con completamento automatico

Estensione: potremmo provare a essere fantasiosi e utilizzare tipi ricorsivi per generare una tupla. Questo ha funzionato per me solo per un paio di oggetti di scena (~ 5,6) fino a quando le prestazioni non si sono degradate in modo massiccio. Anche il tipo ricorsivo profondamente annidato non è ufficialmente supportato da TS: elenco questo esempio qui per completezza.


Opzione 2: generatore di codice basato sull'API del compilatore 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

Dopo aver compilato ed eseguito questo file con tsc && node dist/mybuildstep.js, ./src/generated/IMyTable-keys.tsviene generato un file con il seguente contenuto:

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

(+) soluzione automatica (+) il tipo esatto di tupla (-) richiede build-step


PS: ho scelto ts-morph, in quanto è una semplice alternativa all'API del compilatore TS originale.


4

Non posso. Le interfacce non esistono in fase di esecuzione.

soluzione alternativa

Crea una variabile del tipo e Object.keysusala 🌹


1
Vuoi dire in questo modo:var abc: IMyTable = {}; Object.keys(abc).forEach((key) => {console.log(key)});
Tushar Shukla

4
No, perché quell'oggetto non ha chiavi su di esso. Un'interfaccia è qualcosa che TypeScript utilizza ma evapora in JavaScript, quindi non ci sono informazioni rimaste per informare qualsiasi "riflessione" o "interspection". Tutto ciò che JavaScript sa è che esiste un oggetto letterale vuoto. La tua unica speranza è aspettare (o richiedere che ) TypeScript includa un modo per generare un array o un oggetto con tutte le chiavi dell'interfaccia nel codice sorgente. Oppure, come dice Dan Def, se puoi usare una classe, avrai le chiavi definite sotto forma di proprietà in ogni caso ..
Jesper

1
Si otterrebbe anche un errore da TypeScript su questa riga: var abc: IMyTable = {}perché l'oggetto letterale vuoto non è conforme alla forma di quell'interfaccia.
Jesper

16
Se questo non funziona, perché ci sono voti positivi su questa risposta?
dawez

1
motivo del
voto negativo

-1

Non puoi farlo. Le interfacce non esistono in fase di esecuzione (come @basarat detto ).

Ora sto lavorando con quanto segue:

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

Immagina di avere un sacco di modelli e di farlo per tutti ... è un tempo così costoso.
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
Questo codice non funzionerà poiché la sintassi del dattiloscritto non è disponibile in fase di esecuzione. Se controlli questo codice sul campo dattiloscritto, noterai che l'unica cosa che viene compilata in JavaScript è console.log(Tes.id)che ovviamente sarebbe l'errore `` Errore di riferimento non rilevato: Tes non è definito ''
Tushar Shukla
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.