Obtenga claves de una interfaz de TypeScript como matriz de cadenas

103

Tengo muchas tablas en Lovefield y sus respectivas interfaces para las columnas que tienen.
Ejemplo:

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

Me gustaría tener los nombres de propiedad de esta interfaz en una matriz como esta:

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

no puedo crear un objeto / matriz basado en la interfaz IMyTabledirectamente, lo que debería funcionar porque obtendría los nombres de la interfaz de las tablas de forma dinámica. Por lo tanto, necesito iterar sobre estas propiedades en la interfaz y obtener una matriz.

¿Cómo consigo este resultado?

Tushar Shukla
fuente

Respuestas:

52

A partir de TypeScript 2.3 (o debería decir 2.4 , ya que en 2.3 esta función contiene un error que se ha corregido en [email protected] ), puede crear un transformador personalizado para lograr lo que desea hacer.

En realidad, ya he creado un transformador personalizado de este tipo, que permite lo siguiente.

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

Desafortunadamente, los transformadores personalizados actualmente no son tan fáciles de usar. Debe usarlos con la API de transformación de TypeScript en lugar de ejecutar el comando tsc. Existe un problema al solicitar un complemento de soporte para transformadores personalizados.

kimamula
fuente
Gracias por su respuesta, ya vi e instalé este transformador personalizado ayer, pero como usa el mecanografiado 2.4, esto no me sirve de nada a partir de ahora.
Tushar Shukla
16
Hola, esta biblioteca también cumple exactamente mis requisitos, sin embargo, la obtengo ts_transformer_keys_1.keys is not a functioncuando sigo los pasos exactos en la documentación. ¿Hay alguna solución a esto?
Hasitha Shan
¡Ordenado! ¿Crees que se puede ampliar para tomar un parámetro de tipo dinámico (nota 2 en el archivo Léame)?
kenshin
@HasithaShan mira de cerca los documentos: debe usar la API del compilador de TypeScript para que el paquete funcione
Yaroslav Bai
2
Desafortunadamente, el paquete está roto, haga lo que haga, siempre lo recibots_transformer_keys_1.keys is not a function
fr1sk
17

Lo siguiente requiere que enumere las claves por su cuenta, pero al menos TypeScript se aplicará IUserProfiley IUserProfileKeystendrá exactamente las mismas claves ( Required<T>se agregó en 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,
};
Maciek Wawro
fuente
Truco bastante bueno. Ahora es fácil hacer cumplir la implementación de todas las claves de IUserProfiley sería fácil extraerlas de const IUserProfileKeys. Esto es exactamente lo que estaba buscando. No es necesario convertir todas mis interfaces en clases ahora.
Anddo
13

Tuve un problema similar: tenía una lista gigante de propiedades para las que quería tener una interfaz y un objeto fuera de ella.

NOTA: ¡No quería escribir (escribir con el teclado) las propiedades dos veces! Solo SECO.


Una cosa a tener en cuenta aquí es que las interfaces son tipos obligatorios en tiempo de compilación, mientras que los objetos son principalmente en tiempo de ejecución. ( Fuente )

Como @derek mencionó en otra respuesta , el denominador común de interfaz y objeto puede ser una clase que sirve tanto para un tipo como para un valor .

Entonces, TL; DR, el siguiente fragmento de código debería satisfacer las necesidades:

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

( Aquí está el código anterior en Typecript Playground para jugar más)

PD. Si no desea asignar valores iniciales a las propiedades en la clase y permanecer con el tipo, puede hacer el truco del constructor:

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

Truco de constructor en TypeScript Playground .

Ayuda en
fuente
Sin propsArrayembargo, solo se puede acceder a él cuando inicializó las claves.
denkquer
No entiendo lo que quieres decir con @denkquer "inicializado". En el primer ejemplo, propsArrayestá disponible antes de tableInstancesi eso es lo que quiere decir, tan claramente antes de la inicialización de la instancia. Sin embargo, si se refiere a los valores falsos asignados en MyTableClass, estos están ahí para implicar brevemente el "tipo" de las propiedades. Si no los quiere, puede seguir el truco del constructor en el ejemplo de PS.
Aidin
1
Según tengo entendido, un valor se inicializa cuando tiene algún valor. Su "truco de constructor" es engañoso porque no puede simplemente reemplazar el MyTableClasscon el último y esperar recibir claves en el propsArrayya que las vars y tipos no inicializados se eliminan en tiempo de ejecución. Siempre debe proporcionarles algún tipo de valor predeterminado. Descubrí que inicializarlos con undefinedes el mejor enfoque.
Denkquer
@denkquer lo siento, faltaba mi truco de Constructor readonlyy ?en los parámetros. Acabo de actualizar eso. Gracias por señalar. Ahora creo que funciona de la manera que dijo, es engañoso y no puede funcionar. :)
Aidin
1
@Aidin gracias por tu solución. También me pregunto si puedo evitar la inicialización de los parámetros. Si uso el truco del constructor, ya no puedo crear una interfaz que extienda MyTableClass ... aunque su truco de constructor en el enlace del patio de juegos mecanografiado está vacío
Flion
10

Esto debería funcionar

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

o

var IMyTable: (keyof IMyTable)[] = ["id", "title", "createdAt", "isDeleted"];
Damathryx
fuente
11
No es que esté mal, pero para que quede claro, solo está "haciendo cumplir los valores de la matriz" para que sean correctos. El desarrollador aún necesita anotarlos dos veces, manualmente.
Aidin
Si bien lo que dijo Aidin podría ser cierto, en algunos casos, esto era exactamente lo que estaba buscando, para mi caso. Gracias.
Daniel
4
Esto no evitará la duplicación de claves o la falta de claves. Me gustavar IMyTable: Array<keyof IMyTable> = ["id", "createdAt", "id"];
ford04
Para mí también era lo que estaba buscando porque quiero aceptar opcionalmente las claves pero nada más que las claves definidas en la interfaz. No esperaba que esto fuera predeterminado con el código anterior. Supongo que todavía necesitaríamos una forma TS común para eso. ¡Gracias en cualquier caso por el código anterior!
nicoes
8

Tal vez sea demasiado tarde, pero en la versión 2.1 de mecanografiado puede usar key ofasí:

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

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

Nathan Gabriel
fuente
Gracias por la respuesta, pero no estoy seguro de si ayuda a alguien a usar tipos creados estáticamente desde la interfaz. En mi humilde opinión, podemos usar interfaces / tipos indistintamente en la mayoría de los casos. Además, esto requeriría la creación manual de tipos para múltiples interfaces. Sin embargo, la solución se ve bien si alguien solo necesita obtener tipos de una interfaz.
Tushar Shukla
6

En lugar de definir IMyTablecomo en interfaz, intente definirlo como una clase. En mecanografiado puedes usar una clase como una interfaz.

Entonces, para su ejemplo, defina / genere su clase de esta manera:

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

Úselo como interfaz:

export class SomeTable implements IMyTable {
    ...
}

Obtener claves:

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

Necesitará crear una clase que implemente su interfaz, instanciarla y luego usarla Object.keys(yourObject)para obtener las propiedades.

export class YourClass implements IMyTable {
    ...
}

luego

let yourObject:YourClass = new YourClass();
Object.keys(yourObject).forEach((...) => { ... });
Dan Def
fuente
No funciona en mi caso, tendría que enumerar esas propiedades de la interfaz, pero eso no es lo que quiero. El nombre de la interfaz viene dinámicamente y luego tengo que determinar sus propiedades
Tushar Shukla
Esto produce un error (v2.8.3): Cannot extend an interface […]. Did you mean 'implements'?Sin embargo, usar en su implementslugar requiere copiar manualmente la interfaz, que es exactamente lo que no quiero.
jacob
@jacob lo siento, debería haber sido implementsy he actualizado mi respuesta. Como ha dicho @basarat, las interfaces no existen en tiempo de ejecución, por lo que la única forma es implementarlas como una clase.
Dan Def
¿Quiere decir en lugar de una interfaz usar una clase? Desafortunadamente, no puedo, ya que la interfaz proviene de un tercero ( @types/react). Los copié manualmente, pero eso no es a prueba de futuro 😪 Estoy tratando de vincular dinámicamente métodos que no son del ciclo de vida (que ya están vinculados), pero no están declarados en React.Component (la clase).
jacob
No, me refiero a crear una clase que implemente su interfaz de terceros y obtenga las propiedades de esa clase en tiempo de ejecución.
Dan Def
5

No existe una forma sencilla de crear una matriz de claves desde una interfaz. Los tipos se borran en tiempo de ejecución y los tipos de objetos (sin ordenar, con nombre) no se pueden convertir a tipos de tupla (ordenados, sin nombre) sin algún tipo de truco.


Opción 1: Aproximación manual

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

(+) tipo de retorno de matriz simple (-), sin tupla (+ -) escritura manual con autocompletado

Extensión: Podríamos intentar ser elegantes y usar tipos recursivos para generar una tupla. Esto solo funcionó para mí para un par de accesorios (~ 5,6) hasta que el rendimiento se degradó enormemente. El tipo recursivo profundamente anidado tampoco es compatible oficialmente con TS: enumero este ejemplo aquí para completarlo.


Opción 2: generador de código basado en la API del compilador de 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

Después de compilar y ejecutar este archivo con tsc && node dist/mybuildstep.js, ./src/generated/IMyTable-keys.tsse genera un archivo con el siguiente contenido:

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

(+) solución automática (+) tipo de tupla exacta (-) requiere paso de compilación


PD: He elegido ts-morph, ya que es una alternativa simple a la API del compilador TS original.

ford04
fuente
4

Hipocresía. Las interfaces no existen en tiempo de ejecución.

solución alterna

Crea una variable del tipo y Object.keysúsala 🌹

basarat
fuente
1
¿Quieres decir así:var abc: IMyTable = {}; Object.keys(abc).forEach((key) => {console.log(key)});
Tushar Shukla
4
No, porque ese objeto no tiene claves. Una interfaz es algo que TypeScript usa pero se evapora en JavaScript, por lo que no queda información para informar ningún "reflejo" o "interspección". Todo lo que JavaScript sabe es que hay un objeto literal vacío. Su única esperanza es esperar (o solicitar que ) TypeScript incluya una forma de generar una matriz u objeto con todas las claves de la interfaz en el código fuente. O, como dice Dan Def, si puede usar una clase, tendrá las claves definidas en forma de propiedades en cada instancia ..
Jesper
1
También obtendría un error de TypeScript en esta línea: var abc: IMyTable = {}porque el literal de objeto vacío no se ajusta a la forma de esa interfaz.
Jesper
16
Si esto no funciona, ¿por qué hay votos a favor en esta respuesta?
dawez
1
Motivo del
voto negativo
-1

No puedes hacerlo. Las interfaces no existen en tiempo de ejecución (como dijo @basarat ).

Ahora, estoy trabajando con lo siguiente:

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
fuente
Imagínese tener muchos modelos y hacerlo para todos ... es un tiempo muy caro.
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
fuente
1
Este código no funcionará ya que la sintaxis mecanografiada no está disponible en tiempo de ejecución. Si verifica este código en el patio de juegos mecanografiado, notará que lo único que se compila en JavaScript es, console.log(Tes.id)por supuesto, el error 'Error de referencia no detectado: Tes no está definido'
Tushar Shukla