typescript - Δημιουργία τύπου Record με κλειδιά σταθερού μήκους από πίνακα συμβολοσειρών κλειδιών σε άλλο interface
Αυτό είναι το ελάχιστο παράδειγμα κώδικα. Έχω μια διεπαφή με όνομα Foo η οποία έχει ιδιότητα one η οποία δέχεται έναν πίνακα του τύπου αντικειμένου.
interface Foo {
one: {
key: string;
// more properties
}[]
}
Εδώ ο τύπος του key είναι μια συμβολοσειρά.
Θέλω να δημιουργήσω έναν τύπο Record όπου τα κλειδιά θα είναι τύπου ένωσης string literal που θα παίρνουμε από τον πίνακα των key μιας ιδιότητας στον τύπο Foo και οι τιμές μπορούν να είναι οποιουδήποτε τύπου string.
Έχω προβλήματα στη δημιουργία αυτού του τύπου.
Σκηνοθετημένο Παράδειγμα:
const example: Foo = {
one: [{ key: "mykey1" }, {key: "mykey2"}, {key: "mykey3"}]
}
const A = {} as Record<string, string>; // as of now I did it like this
// I need to strongly type this constant.
example.one.map(el => {
A[el.key] = "anything" // contrived example
})
Απάντηση #1
Η επισήμανση μιας μεταβλητής ως Foo απορρίπτει τις βασικές πληροφορίες που χρειάζεστε
Το κύριο εμπόδιο εδώ είναι ότι όταν σχολιάζετε μια μεταβλητή με έναν (μη-ένωση) τύπο, όπως const example: Foo=..., ο μεταγλωττιστής θα θεωρήσει αυτή τη μεταβλητή ότι είναι αυτού του τύπου ανεξαρτήτως από την τιμή με την οποία την αρχικοποιείτε. Και έτσι οποιαδήποτε πιο συγκεκριμένη πληροφορία σχετικά με την αρχικοποιημένη τιμή, όπως ο τύπος συμβολοσειράς literal "mykey1", θα χαθεί. Ο ορισμός του Foo έχει απλά string ως την ιδιότητα key των στοιχείων του πίνακα one, και έτσι string είναι αυτό που θα πάρετε αν προσπαθήσετε να ελέγξετε τον τύπο της μεταβλητής:
const example: Foo = {
one: [{ key: "mykey1" }, { key: "mykey2" }, { key: "mykey3" }]
};
// const example: Foo
type KeyType = typeof example["one"][number]["key"]
// type KeyType = string, oops
Έτσι λοιπόν, πραγματικά δεν θέλεις να σχολιάσεις το example (εκτός αν θέλεις να γράψεις χειροκίνητα τον πλήρη τύπο με όλες τις απαιτούμενες πληροφορίες). Αντίθετα, θα πρέπει να προσπαθήσεις να κάνεις τον μεταγλωττιστή να συμπεράνει τον τύπο του example με τέτοιο τρόπο ώστε να περιέχει τις πληροφορίες που σε ενδιαφέρουν, ενώ ταυτόχρονα να θεωρείται αναθέσιμος στον Foo.
Η απλή παράλειψη της σημείωσης επίσης πετάει αυτές τις βασικές πληροφορίες
Αν απλώς παραλείψουμε τη σημείωση, αυτό επίσης δεν είναι επαρκές:
const example = {
one: [{ key: "mykey1" }, { key: "mykey2" }, { key: "mykey3" }]
};
// const example: { one: { key: string; }[]; }
type KeyType = typeof example["one"][number]["key"]
// type KeyType = string, oops
Ξανά, είναι απλώς string. Αυτό συμβαίνει επειδή οι προεπιλεγμένοι εικαστικοί κανόνες για την εξαγωγή τύπων μεταβλητών τείνουν να επεκτείνουν οποιαδήποτε ιδιοκτησία string literal σε απλό string. Οι ιδιότητες μερικές φορές επαναπροσδιορίζονται και ο μεταγλωττιστής δεν έχει ιδέα ότι δεν προτίθεστε να γράψετε example.one[0].key="somethingElse". Και έτσι, ο string είναι ο τύπος που εξάγει αντί για "mykey1". Χρειάζεται να κάνουμε κάτι άλλο.
Μια const δήλωση θα διατηρήσει την βασική πληροφορία, αλλά δεν είναι πια συμβατή με το Foo
Αν θέλετε να πείτε στον μεταγλωττιστή ότι δεν πρόκειται να τροποποιήσετε τα περιεχόμενα του example και ότι θα πρέπει να προσπαθήσει να συναγάγει τον πιο συγκεκριμένο τύπο που είναι δυνατόν, μπορείτε να χρησιμοποιήσετε μια δήλωση const στον αρχικοποιητή:
const example = {
one: [{ key: "mykey1" }, { key: "mykey2" }, { key: "mykey3" }]
} as const;
/* const example: { readonly one: readonly [
{ readonly key: "mykey1"; },
{ readonly key: "mykey2"; },
{ readonly key: "mykey3"; }
];
*/
type KeyType = typeof example["one"][number]["key"]
// type KeyType = "mykey1" | "mykey2" | "mykey3", hooray!
Τώρα το example θεωρείται ότι είναι ένα αντικείμενο με μια readonly ιδιότητα που περιέχει έναν readonly πίνακα από αντικείμενα με readonly ιδιότητες των οποίων οι τιμές είναι τα string literals που μας ενδιαφέρουν. Και έτσι το KeyType είναι ακριβώς η ένωση των string literals που θέλουμε.
Αυτό είναι υπέροχο, και θα είχαμε τελειώσει εδώ, εκτός από μια λεπτομέρεια. Ο συναγόμενος τύπος του example δεν μπορεί να ανατεθεί στον Foo σας όπως έχει οριστεί. Αποδεικνύεται ότι οι readonly πίνακες δεν μπορούν να ανατεθούν σε μεταβλητούς πίνακες (γίνεται το αντίστροφο), και έτσι συμβαίνει το εξής:
function acceptFoo(foo: Foo) { }
acceptFoo(example); // error! one is readonly
// -----> ~~~~~~~
Ίσως θα πρέπει να επανακαθορίσετε το Foo ώστε να είναι συμβατό με μια δήλωση const
Προτίθεσαι ποτέ να προσθέσεις ή να αφαιρέσεις στοιχεία από την ιδιότητα one ενός Foo; Αν όχι, τότε η πιο εύκολη λύση εδώ είναι απλώς να επαναπροσδιορίσεις το Foo:
interface Foo {
one: readonly { // <-- change to readonly array type
key: string;
// more properties
}[]
}
acceptFoo(example); // okay now
Αν δεν θέλεις να κάνεις αυτή την αλλαγή, τότε χρειάζεσαι κάποια άλλη λύση. Και ακόμα κι αν θέλεις, η παράλειψη της σημείωσης και η χρήση του as const έχει σαν παρενέργεια το γεγονός ότι το example ίσως να μην είναι πραγματικά έγκυρο Foo, και δεν θα το αντιληφθείς μέχρι να προσπαθήσεις να το χρησιμοποιήσεις ως Foo αργότερα:
const example = {
one: [{ key: "mykey1" }, { key: "mykey2" }, { kee: "mykey3" }]
} as const; // no error here
/* const example: { readonly one: readonly [
{ readonly key: "mykey1"; },
{ readonly key: "mykey2"; },
{ readonly kee: "mykey3"; }
];
*/
acceptFoo(example); // error here
Το τρίτο στοιχείο του one έχει μια ιδιότητα kee αντί για ιδιότητα key. Αυτό είναι λάθος, αλλά δεν υπάρχει σφάλμα στη γραμμή const example=... επειδή τίποτα δεν λέει ότι πρέπει να είναι ένα Foo. Λαμβάνετε σφάλμα αργότερα όταν το χειρίζεστε σαν ένα Foo.
Αυτό μπορεί να είναι αποδεκτό για σένα, ή ίσως όχι. Αν είναι, τότε μπορούμε να σταματήσουμε εδώ. Αν όχι, συνέχισε να διαβάζεις:
Ή μπορείτε να δημιουργήσετε μια γενική βοηθητική συνάρτηση για να συμπεράνει έναν τύπο συμβατό με Foo που επίσης διατηρεί τις πληροφορίες κλειδιών
Μια άλλη ιδέα αντί να χρησιμοποιήσετε as const είναι να δημιουργήσετε μια γενική βοηθητική συνάρτηση που καθοδηγεί τη διαδικασία συμπερασμού. Κατά την εκτέλεση, θα επιστρέφει απλώς την είσοδό της, οπότε φαίνεται σαν να μην κάνει τίποτα. Ακολουθεί ένας τρόπος για να το κάνετε:
interface FooWithKeys<K extends string> extends Foo {
one: {
key: K;
// more properties
}[]
}
const asFoo = <K extends string>(fwk: FooWithKeys<K>) => fwk;
Ο τύπος FooWithKeys<K> είναι μια επέκταση του Foo όπου τα κλειδιά στο one είναι γνωστό ότι είναι K, το οποίο περιορίζεται να είναι υποτύπος του string. Ένας τύπος FooWithKeys<"a" | "b"> μπορεί να ανατεθεί σε Foo, αλλά ο μεταγλωττιστής γνωρίζει ότι τα κλειδιά είναι a ή b και όχι απλώς string.
Η βοηθητική συνάρτηση asFoo() θα εξετάσει την είσοδό της και θα συμπεράνει έναν τύπο για το K που να είναι συμβατός με αυτήν. Δεδομένου ότι η παράμετρος τύπου K περιορίζεται σε string, ο μεταγλωττιστής θα προσπαθήσει να συμπεράνει τύπους string literals για αυτήν αν είναι δυνατό.
Ας το δούμε σε δράση:
const example = asFoo({
one: [{ key: "mykey1" }, { key: "mykey2" }, { key: "mykey3" }]
})
// const example: FooWithKeys<"mykey1" | "mykey2" | "mykey3">
Φαίνεται καλό. Τώρα μπορούμε να πάρουμε τον τύπο του κλειδιού όπως προηγουμένως:
type KeyType = typeof example["one"][number]["key"]
// type KeyType = "mykey1" | "mykey2" | "mykey3", hooray!
και φτιάξτε το λεξικό σας A:
const A = {} as Record<KeyType, string>;
example.one.map(el => {
A[el.key] = "anything"
})
Και το example εξακολουθεί να θεωρείται Foo:
acceptFoo(example); // okay
Και αν κάναμε κάποιο λάθος με το example, θα παίρναμε ένα σφάλμα αμέσως εκεί:
const example = asFoo({
one: [{ key: "mykey1" }, { key: "mykey2" }, { kee: "mykey3" }] // error!
// -----------------------------------------> ~~~~~~~~~~~~~
// Object literal may only specify known properties, and 'kee' does not exist in type
})
Σύνδεσμος playground για κώδικα
