javascript - Hanki Typescript-rajapinnan avaimet merkkijonotaulukkoon


Minulla on paljon tauluja Lovefieldissä ja niiden vastaavat rajapinnat, jotka määrittävät, mitä sarakkeita niissä on.

Esimerkki :

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

Haluaisin saada tämän rajapinnan ominaisuuksien nimet taulukkoon näin:

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

En voi suoraan luoda olioa/taulukkoa rajapinnan IMyTable perusteella, mikä pitäisi ratkaista ongelman, koska saisin taulukoiden rajapinnan nimet dynaamisesti. Siksi minun täytyy käydä läpi nämä ominaisuudet rajapinnassa ja saada taulukko niistä.

Kuinka saavutan tämän tuloksen?

Vastaus #1

Versiosta TypeScript 2.3 lähtien (tai pitäisikö sanoa 2.4, sillä 2.3-versiossa tässä ominaisuudessa on bugi, joka on korjattu [email ), voit luoda mukautetun muuntimen saavuttaaksesi haluamasi asian.

Itse asiassa olen jo luonut sellaisen mukautetun muuntimen, joka mahdollistaa seuraavan.

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

Valitettavasti mukautettuja muuntajia ei tällä hetkellä ole niin helppo käyttää. Niitä täytyy käyttää TypeScript-muunnosrajapinnan kautta sen sijaan, että suorittaisit tsc-komennon. On olemassa ongelma, jossa pyydetään laajennustukea mukautetuille muuntajille.

Vastaus #2

Kohdatessani saman ongelman: minulla oli jättimäinen lista ominaisuuksia, joita halusin käyttää sekä rajapintana (käännösaika) että objektina (ajonaika) siitä.

HUOM: En halunnut kirjoittaa (näppäimistöllä) ominaisuuksia kahteen kertaan! DRY.


Yksi asia, joka kannattaa huomioida tässä, on se, että rajapinnat ovat käänneaikaisia tyyppejä, kun taas oliot ovat pääosin suoritusaikaisia. (Lähde)

Kuten @derek mainitsi toisessa vastauksessa, interfacein ja objectin yhteinen nimittäjä voi olla luokka, joka toimii sekä typenä että arvona.

Joten, TL;DR, seuraavan koodinpätkän pitäisi täyttää tarpeet:

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

(Tässä on yllä oleva koodi Typescript Playgroundissa, jotta voi leikkiä enemmän)

PS. Jos et halua antaa alkuarvoja luokan ominaisuuksille ja pysyä tyypin kanssa, voit käyttää konstruktoria kikkaa:

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

Konstruktorin temppu TypeScript-leikkikentässä.

Vastaus #3

Seuraava edellyttää, että luette avaimet itse, mutta ainakin TypeScript varmistaa, että IUserProfile ja IUserProfileKeys sisältävät täsmälleen samat avaimet (Required<T> lisättiin TypeScript 2.8:ssa):

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

Vastaus #4

Ehkä on jo liian myöhäistä, mutta TypeScriptin versiossa 2.1 voit käyttää keyof saadaksesi tyypin näin:

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

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

Vastaus #5

Turvalliset versiot

Rajapinnan avainten taulukon tai tuplen luominen turvallisten käännösaikaisia tarkistuksia hyödyntäen vaatii hieman luovuutta. Tyypit poistetaan ajonaikana, eikä olioiden tyyppejä (järjestämättömät, nimetty) voida muuntaa tuplen tyypeiksi (järjestetyt, nimeämättömät) ilman ei-tuettuja tekniikoita.

Vertailu muihin vastauksiin

Tässä ehdotetut variantit kaikki ottavat huomioon tai laukaisevat käännösvirheen, jos toistuvia tai puuttuvia tuplaelementtejä annetaan viiteobjektille, kuten IMyTable. Esimerkiksi taulukotyypin (keyof IMyTable)[] ilmoittaminen ei voi havaita näitä virheitä.

Lisäksi ne eivät vaadi tiettyä kirjastoa (viimeinen variantti käyttää ts-morph, jota pitäisin yleisenä kääntäjän wrapperina), tuottavat tuplet-tyypin objektin sijaan (vain ensimmäinen ratkaisu luo taulukon) tai laajan taulukotyypin (vertaa näihin vastauksiin) ja lopuksi eivät tarvitse luokkia.

Variantti 1: Yksinkertainen tyypitetty taulukko

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

+ helpoin +- käsikirja automaattisella täydennyksellä - taulukko, ei tuple

Leikkikenttä

Jos et pidä tietueen luomisesta, tutustu tähän vaihtoehtoon, jossa on Set ja väitetyt tyypit.


Vaihtoehto 2: Tuppi apufunktion kanssa

function createKeys<T extends readonly (keyof IMyTable)[] | [keyof IMyTable]>(
    t: T & CheckMissing<T, IMyTable> & CheckDuplicate<T>): T {
    return t
}

+ tuple +- käsikirja automaattisella täydennyksellä +- kehittyneemmät, monimutkaiset tyypit

Leikkikenttä

Selitys

createKeys suorittaa käännösaikaiset tarkistukset yhdistämällä funktioparametrin tyypin lisättyjen varmennustyyppien kanssa, jotka antavat virheen sopimattomasta syötteestä. (keyof IMyTable)[] | [keyof IMyTable] on "musta taikuus", jolla pakotetaan kääntäjä päättelemään tuplen sijaan taulukko kutsujan puolelta. Vaihtoehtoisesti voit käyttää const-vahvistuksia/as const kutsujan puolelta.

CheckMissing tarkistaa, puuttuuko T:stä avaimia U:sta:

type CheckMissing<T extends readonly any[], U extends Record<string, any>> = {
    [K in keyof U]: K extends T[number] ? never : K
}[keyof U] extends never ? T : T & "Error: missing keys"

type T1 = CheckMissing<["p1"], {p1:any, p2:any}> //["p1"] & "Error: missing keys"
type T2 = CheckMissing<["p1", "p2"], { p1: any, p2: any }> // ["p1", "p2"]

Huomautus: T & "Error: missing keys" on vain mukavia IDE-virheitä varten. Voit myös kirjoittaa never. CheckDuplicates tarkistaa kaksoistuplet-elementit:

type CheckDuplicate<T extends readonly any[]> = {
    [P1 in keyof T]: "_flag_" extends
    { [P2 in keyof T]: P2 extends P1 ? never :
        T[P2] extends T[P1] ? "_flag_" : never }[keyof T] ?
    [T[P1], "Error: duplicate"] : T[P1]
}

type T3 = CheckDuplicate<[1, 2, 3]> // [1, 2, 3]
type T4 = CheckDuplicate<[1, 2, 1]> 
// [[1, "Error: duplicate"], 2, [1, "Error: duplicate"]]

Huomaa: Lisätietoja yksilöllisten kohteiden tarkistuksista tupleissa löytyy tästä viestistä. Version TS 4.1 avulla voimme myös nimetä puuttuvat avaimet virhesanassa - katso tätä Playgroundia.


Vaihtoehto 3: Rekursiivinen tyyppi

Versiolla 4.1 TypeScript tukee virallisesti ehdollisia rekursiivisia tyyppejä, joita voitaisiin mahdollisesti käyttää myös tässä. Tyypin laskenta on kuitenkin kallista yhdistelmällisen monimutkaisuuden vuoksi - suorituskyky heikkenee huomattavasti yli 5-6 kohteen kohdalla. Listaan tämän vaihtoehdon täydellisyyden vuoksi (Playground):

type Prepend<T, U extends any[]> = [T, ...U] // TS 4.0 variadic tuples

type Keys<T extends Record<string, any>> = Keys_<T, []>
type Keys_<T extends Record<string, any>, U extends PropertyKey[]> =
  {
    [P in keyof T]: {} extends Omit<T, P> ? [P] : Prepend<P, Keys_<Omit<T, P>, U>>
  }[keyof T]

const t1: Keys<IMyTable> = ["createdAt", "isDeleted", "id", "title"] // ✔

+ tuple +- manuaali automaattisella täydennyksellä + ei apufunktiota -- suorituskyky


Vaihtoehto 4: Koodigeneraattori/TS-kääntäjä API

Ts-morph on valittu tähän, koska se on hieman yksinkertaisempi käärevaihtoehto alkuperäiselle TS-kääntäjän API:lle. Tietenkin voit myös käyttää kääntäjän API:ta suoraan. Katsotaanpa generaattorin koodi:

// ./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

Kun käännämme ja suoritamme tämän tiedoston komennolla tsc && node dist/mybuildstep.js, tiedosto ./src/generated/IMyTable-keys.ts, jonka sisältö on seuraava, luodaan:

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

+ automaattisesti generoituva ratkaisu + skaalautuva useille ominaisuuksille + ei apufunktiota + tupla - ylimääräinen rakennusvaihe - vaatii tuttavuutta kääntäjän API:n kanssa

Vastaus #6

Tämän pitäisi toimia

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

tai

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

Vastaus #7

Sen sijaan, että määrittelet IMyTable rajapintana, yritä määritellä se luokkana. Typescriptissä voit käyttää luokkaa kuten rajapintaa.

Joten esimerkkiäsi varten määrittele/generoi luokkasi näin:

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

Käytä sitä käyttöliittymänä:

export class SomeTable implements IMyTable {
    ...
}

Hanki avaimet:

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

Aiheeseen liittyviä artikkeleita