Obtenha as chaves de uma interface Typescript como um conjunto de strings

102

Eu tenho muitas tabelas no Lovefield e suas respectivas interfaces para as colunas que eles têm.
Exemplo:

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

Eu gostaria de ter os nomes das propriedades desta interface em uma matriz como esta:

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

Não posso fazer um objeto / matriz com base na interface IMyTablediretamente, o que deve funcionar porque estaria obtendo os nomes das interfaces das tabelas dinamicamente. Portanto, preciso iterar sobre essas propriedades na interface e obter uma matriz dela.

Como faço para alcançar esse resultado?

Tushar Shukla
fonte

Respostas:

52

A partir do TypeScript 2.3 (ou devo dizer 2.4 , como no 2.3 esse recurso contém um bug que foi corrigido em [email protected] ), você pode criar um transformador personalizado para conseguir o que deseja fazer.

Na verdade, já criei esse transformador personalizado, que permite o seguinte.

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

Infelizmente, os transformadores personalizados atualmente não são tão fáceis de usar. Você deve usá-los com a API de transformação TypeScript em vez de executar o comando tsc. Há um problema ao solicitar um suporte de plug-in para transformadores personalizados.

kimamula
fonte
Obrigado pela sua resposta, eu já vi e instalei este transformador personalizado ontem, mas como ele usa o texto datilografado 2.4, não tem uso para mim no momento.
Tushar Shukla
16
Olá, esta biblioteca atende exatamente aos meus requisitos também, no entanto, estou entendendo ts_transformer_keys_1.keys is not a functionquando sigo as etapas exatas na documentação. existe uma solução alternativa para isso?
Hasitha Shan
Arrumado! Você acha que pode ser estendido para receber um parâmetro de tipo dinâmico (nota 2 no leia-me)?
kenshin
@HasithaShan analise cuidadosamente os documentos - você deve usar a API do compilador TypeScript para que o pacote funcione
Yaroslav Bai
2
Infelizmente, o pacote está quebrado, o que quer que eu faça, sempre recebots_transformer_keys_1.keys is not a function
fr1sk
17

O seguinte requer que você liste as chaves por conta própria, mas pelo menos o TypeScript aplicará IUserProfilee IUserProfileKeysterá exatamente as mesmas chaves ( Required<T>foi adicionado no 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
fonte
Um truque muito legal. Agora é fácil forçar a implementação de todas as chaves de IUserProfilee seria fácil extraí-las de const IUserProfileKeys. Isso é exatamente o que estou procurando. Não há necessidade de converter todas as minhas interfaces em classes agora.
Anddo
13

Tive um problema semelhante, pois tinha uma lista gigante de propriedades das quais queria ter uma interface e um objeto fora dela.

NOTA: Eu não queria escrever (digitar com o teclado) as propriedades duas vezes! Apenas SECO.


Uma coisa a ser observada aqui é que as interfaces são tipos impostos em tempo de compilação, enquanto os objetos são em sua maioria em tempo de execução. ( Fonte )

Como @derek mencionou em outra resposta , o denominador comum de interface e objeto pode ser uma classe que serve tanto um tipo quanto um valor .

Portanto, TL; DR, o seguinte trecho de código deve satisfazer as necessidades:

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

( Aqui está o código acima no Playground Typescript para jogar mais)

PS. Se você não quiser atribuir valores iniciais às propriedades da classe e ficar com o tipo, pode fazer o truque do construtor:

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

Truque do construtor no Playground TypeScript .

Auxílio em
fonte
O propsArraysó está acessível quando você inicializou as chaves.
denkquer,
Não entendi o que você quis dizer com "inicializado" @denkquer. No primeiro exemplo, propsArrayestá disponível antes de tableInstancese é isso que você quer dizer, então claramente antes da inicialização da instância. No entanto, se você está se referindo aos valores falsos atribuídos em MyTableClass, eles estão ali apenas para sugerir o "tipo" das propriedades. Se não os quiser, pode usar o truque do construtor no exemplo PS.
Aidin
1
No meu entendimento, um valor é inicializado quando tem qualquer valor. Seu "truque do construtor" é enganoso porque você não pode simplesmente substituir o MyTableClasscom o último e esperar receber as chaves na propsArraymedida que vars e tipos não inicializados são removidos no tempo de execução. Você sempre deve fornecer a eles algum tipo de valor padrão. Descobri que inicializá-los com undefinedé a melhor abordagem.
denkquer,
@denkquer, desculpe, meu truque do Construtor estava faltando readonlye ?nos parâmetros. Acabei de atualizar isso. Obrigado por apontar. Agora acho que funciona da maneira que você disse, é enganoso e não pode funcionar. :)
Aidin
@Aidin, obrigado por sua solução. Também estou me perguntando se posso evitar a inicialização dos parâmetros. Se eu usar o truque do construtor, não posso mais criar uma interface que estenda MyTableClass .. seu truque do construtor no link do playground datilografado está vazio
Flion
10

Isso deve funcionar

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

ou

var IMyTable: (keyof IMyTable)[] = ["id", "title", "createdAt", "isDeleted"];
Damathryx
fonte
10
Não que esteja errado, mas para ser claro aqui, você está apenas "impondo os valores da matriz" para que sejam corretos. O desenvolvedor ainda precisa escrevê-los duas vezes, manualmente.
Aidin
Embora o que Aidin disse possa ser verdade, para alguns casos, isso era exatamente o que eu procurava, para o meu caso. Obrigado.
Daniel
4
Isso não evitará duplicações de chaves ou chaves ausentes. Likevar IMyTable: Array<keyof IMyTable> = ["id", "createdAt", "id"];
ford04
Para mim, era também o que eu procurava porque desejo aceitar as chaves opcionalmente, mas nada mais do que as chaves definidas na interface. Não esperava que isso fosse o padrão com o código acima. Acho que ainda precisaríamos de uma forma comum de TS para isso. Obrigado em qualquer caso pelo código acima!
nicas de
8

Talvez seja tarde demais, mas na versão 2.1 do texto datilografado você pode usar key ofassim:

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
fonte
Obrigado pela resposta, mas não tenho certeza se isso ajuda alguém a usar Tipos criados estaticamente na interface. IMHO, podemos usar interfaces / tipos de forma intercambiável na maioria dos casos. Além disso, isso exigiria a criação manual de tipos para várias interfaces. No entanto, a solução parece boa se alguém precisar apenas extrair tipos de uma interface.
Tushar Shukla
6

Em vez de definir IMyTablecomo uma interface, tente defini-la como uma classe. No texto datilografado, você pode usar uma classe como uma interface.

Então, para seu exemplo, defina / gere sua classe assim:

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

Use-o como uma interface:

export class SomeTable implements IMyTable {
    ...
}

Obtenha as chaves:

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

Você precisará fazer uma classe que implemente sua interface, instanciá-la e usar Object.keys(yourObject)para obter as propriedades.

export class YourClass implements IMyTable {
    ...
}

então

let yourObject:YourClass = new YourClass();
Object.keys(yourObject).forEach((...) => { ... });
Dan Def
fonte
Não funciona no meu caso, eu teria que listar essas propriedades da interface, mas não é isso que eu quero? O nome da interface vem dinamicamente e então eu tenho que determinar suas propriedades
Tushar Shukla
Isso produz um erro (v2.8.3): Cannot extend an interface […]. Did you mean 'implements'?No entanto, usar em implementsvez disso requer a cópia manual da interface, que é exatamente o que eu não quero.
jacob
@jacob desculpe, deveria ter sido implementse eu atualizei minha resposta. Como @basarat afirmou, as interfaces não existem em tempo de execução, então a única maneira é implementá-las como uma classe.
Dan Def
Quer dizer, em vez de uma interface, use uma classe? Infelizmente, não posso, pois a interface vem de um terceiro ( @types/react). Eu os copiei manualmente, mas isso dificilmente é à prova de futuro 😪 Estou tentando vincular dinamicamente métodos que não são do ciclo de vida (que já estão vinculados), mas eles não são declarados em React.Component (a classe).
jacob
Não, quero dizer criar uma classe que implemente sua interface de terceiros e obter as propriedades dessa classe em tempo de execução.
Dan Def
4

Não posso. As interfaces não existem em tempo de execução.

Gambiarra

Crie uma variável do tipo e use Object.keysnela 🌹

Basarat
fonte
1
Você quer dizer assim:var abc: IMyTable = {}; Object.keys(abc).forEach((key) => {console.log(key)});
Tushar Shukla
4
Não, porque esse objeto não tem chaves. Uma interface é algo que o TypeScript usa, mas evapora no JavaScript, portanto, não há informações restantes para informar qualquer "reflexão" ou "interspecção". Tudo o que o JavaScript sabe é que existe um literal de objeto vazio. Sua única esperança é esperar (ou solicitar que ) o TypeScript inclua uma maneira de gerar um array ou objeto com todas as chaves da interface no código-fonte. Ou, como Dan Def diz, se você pode usar uma classe, você terá as chaves definidas na forma de propriedades em cada instância.
Jesper
1
Você também obteria um erro do TypeScript nesta linha: var abc: IMyTable = {}porque o literal do objeto vazio não está de acordo com a forma dessa interface.
Jesper
16
Se isso não funcionar, por que há votos positivos nesta resposta?
dawez
1
motivo de
voto negativo
4

Não existe uma maneira fácil de criar um array de chaves a partir de uma interface. Os tipos são apagados em tempo de execução e os tipos de objetos (não ordenados, nomeados) não podem ser convertidos em tipos de tupla (ordenados, não nomeados) sem algum tipo de hack.


Opção 1: abordagem 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 array simples (-), sem tupla (+ -) escrita manual com preenchimento automático

Extensão: podemos tentar ser extravagantes e usar tipos recursivos para gerar uma tupla. Isso só funcionou para mim por alguns adereços (~ 5,6) até o desempenho degradar enormemente. Tipos recursivos profundamente aninhados também não são oficialmente suportados pelo TS - listo este exemplo aqui para fins de completude.


Opção 2: gerador de código baseado na API do compilador 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

Depois de compilar e executar esse arquivo com tsc && node dist/mybuildstep.js, um arquivo ./src/generated/IMyTable-keys.tscom o seguinte conteúdo é gerado:

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

(+) solução automática (+) tipo de tupla exata (-) requer etapa de construção


PS: Eu escolhi ts-morph, pois é uma alternativa simples para a API do compilador TS original.

ford04
fonte
-1

Você não pode fazer isso. As interfaces não existem em tempo de execução (como @basarat disse).

Agora, estou trabalhando com o seguinte:

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
fonte
Imagine ter vários modelos e fazer isso para todos ... é uma hora tão cara.
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
fonte
1
Este código não funcionará, pois a sintaxe de texto digitado não está disponível em tempo de execução. Se você verificar este código no playground de texto digitado, notará que a única coisa que compila em JavaScript é o console.log(Tes.id)que, claro, seria o erro 'Erro de referência não capturado: Tes não está definido'
Tushar Shukla