typescript - Πώς να περάσετε μια περιορισμένη συμβολοσειρά σε έναν τύπο χρησιμοποιώντας ως παραμέτρους μια παράμετρο που χρησιμοποιεί τον ίδιο περιορισμό


Μπορώ να φτιάξω αριθμό σαν αυτό:

type Digits = '0'| '1'| '2'| '3'| '4'| '5'| '6'| '7'| '8'| '9'
type NonZero = Exclude <Digits, '0'>
type Negative = '-'

type PositiveNumbers =
  | NonZero
  | `${NonZero}${Digits}`
  | `${NonZero}${Digits}${Digits}`
  | `${NonZero}${Digits}${Digits}${Digits}`
  | `${NonZero}${Digits}${Digits}${Digits}${Digits}`
  | `${NonZero}${Digits}${Digits}${Digits}${Digits}${Digits}`  // Error: Expression produces a union type that is too complex to represent.(2590)

type Numbers =
  | PositiveNumbers
  | `${Negative}${PositiveNumbers}`

Αλλά μετά περιορίζομαι σε αριθμούς από -99999 έως 99999, και εκτός αυτού η χρήση του τύπου Numbers σε άλλους τύπους κάνει τον μεταγλωττιστή να δυσκολεύεται. Έτσι χρησιμοποιώ έναν τύπο για να αναπαριστώ αριθμούς ως περιορισμό ως εξής:

type ConstrainNumber <N extends string> =
  N extends '0'
  ? unknown
  : N extends `${infer Char}${infer Rest}`
    ? Char extends '0'
      ? never
      : Char extends Negative
        ? ConstrainNumber <Rest>
        : ConstrainNumberRec <N, Exclude <Digits, '0'>>
    : never

type ConstrainNumberRec <S extends string, D = Digits> =
  S extends `${infer Char}${infer Rest}`
  ? Char extends D ? ConstrainNumberRec <Rest> : never
  : unknown

Όταν το χρησιμοποιείτε σε έναν τύπο λειτουργεί όπως αναμένεται:

type UseNumbers <N extends string & _N, _N = ConstrainNumber <N>> = N

type testValid = UseNumbers <'2875467'>
type testInvalid = UseNumbers <'02875467'> // Expected error: Type 'string' does not satisfy the constraint 'never'.(2344)

Αλλά όταν χρησιμοποιώ έναν τύπο που χρησιμοποιεί UseNumbers, σπάει

type UseUseNumbers <N extends string & _N, _N = ConstrainNumber <N>> = 
  UseNumbers <N> // Unwanted Error: type 'N' does not satisfy the constraint 'string & ConstrainNumber<N>'

Πώς μπορώ να περάσω μια συμβολοσειρά περιορισμού σε έναν τύπο χρησιμοποιώντας ως παραμέτρους μια παράμετρο που χρησιμοποιεί τον ίδιο περιορισμό;
Και επιπλέον, δεν είμαι σίγουρος να καταλάβω γιατί σπάει; Στο UseNumbers <N> το N είναι ήδη περιορισμός ή όχι;

παιδική χαρά

Απάντηση #1

Το κύριο ζήτημα είναι ότι οι προεπιλεγμένες τιμές των γενικών παραμέτρων δεν είναι γενικοί περιορισμοί:

type Foo<T extends number> = T;
type BadFoo = Foo<string>; // error, not satisfying constraint

type Bar<T = number> = T;
type DefaultBar = Bar; // number
type StillGoodBar = Bar<string>; // string

Στο παραπάνω, η παραμετροποίηση τύπου T στο Foo περιορίζεται σε number, ενώ στο Bar δεν υπάρχει περιορισμός αλλά απλώς προεπιλέγεται σε number. Έτσι, δεν μπορείτε να γράψετε Foo<string>. Αν απλώς γράψετε Bar, θα είναι Bar<number>, αλλά τίποτα δεν εμποδίζει κάποιον να γράψει Bar<string>. Δεν είναι σφάλμα και ο μεταγλωττιστής δεν μπορεί να υποθέσει μέσα στο Bar ότι το T θα είναι υποτύπος του number.


Στην ορισμό αυτό,

type UseUseNumbers<N extends string & _N, _N = ConstrainNumber<N>> = ...

Η παράμετρος τύπου _N δεν έχει περιορισμό. Μπορεί να είναι οτιδήποτε, παρόλο που προεπιλέγεται σε ConstrainNumber<N>. Μόνο το N περιορίζεται σε string & _N.

Η κύρια χρήση σας εδώ είναι ότι κανείς δεν καθορίζει το _N, και ότι το UseUseNumbers<N> είναι ισοδύναμο με το UseUseNumbers<N, ConstrainNumber<N>>, το οποίο μεταγλωττίζεται αν και μόνο αν N extends string & ConstrainNumber<N>. Αυτός είναι ένας έξυπνος τρόπος για να αποφύγετε το σφάλμα περιορισμού κυκλικότητας που θα συνέβαινε αν γράφατε type Oopsie<N extends string & ConstrainNumber<N>>, αλλά αυτό το κόλπο λειτουργεί μόνο αφήνοντας ανοικτό το ενδεχόμενο το _N να μην εξαρτάται από το N.

Και έτσι, ο μεταγλωττιστής δεν μπορεί να γνωρίζει ότι το _N είναι κάτι συγκεκριμένο. Κάποιος ανεύθυνος προγραμματιστής μπορεί να γράψει UseUseNumbers<string, unknown> ή UseUseNumbers<"abc","abc" | "def"> ή κάποια άλλη τρελή μη τυποποιημένη χρήση του UseUseNumbers η οποία τυχαίνει να μεταγλωττιστεί. Αυτό πιθανώς να είναι αναπόφευκτο (αν και το αν είναι αποτρέψιμο ή όχι είναι εκτός θέματος για την ερώτηση), αλλά επίσης απίθανο να συμβεί πραγματικά, ειδικά αν τεκμηριώσετε ότι το _N είναι για «εσωτερική χρήση μόνο» (αλλά και πάλι, εκτός θέματος). Το πόσο θέλετε να λάβετε υπόψη αυτή την πιθανότητα εξαρτάται από εσάς.


Τέλος πάντων, ο μεταγλωττιστής είναι εκνευρισμένος με

type UseUseNumbers<N extends string & _N, _N = ConstrainNumber<N>> =
  UseNumbers<N> // <-- error

Γιατί; επειδή UseNumbers<N> αξιολογείται ως UseNumbers<N, ConstrainNumber<N>>, το οποίο (παρόμοια με το UseUseNumbers) συντάσσεται μόνο αν N extends string & ConstrainNumber<N>. Αλλά ο μεταγλωττιστής δεν ξέρει τι ισχύει για το τρέχον N, το οποίο είναι γνωστό μόνο ότι επεκτείνεται σε string & _N, και το _N δεν έχει περιορισμούς. Επομένως, δεν συντάσσεται.


Ο τρόπος να διορθωθεί αυτό είναι να πάρουμε απλώς ρητά την υπόθεση ότι κανείς δεν πρόκειται να πειράξει το _N στο UseUseNumbers, και να το περάσουμε κάτω στο UseNumbers:

type UseUseNumbers<N extends string & _N, _N = ConstrainNumber<N>> =
  UseNumbers<N, _N> // <-- this fixes it

Δεδομένου ότι το _N στο UseNumbers δεν έχει περιορισμούς, μπορείτε να γράψετε UseNumbers<N, _N>, το οποίο θα μεταγλωττιστεί μόνο αν και μόνο αν N extends string & _N. Αλλά γνωρίζουμε ότι αυτό είναι αληθές από τον ορισμό του N στο UseUseNumbers. Όσο οι άνθρωποι γράφουν UseUseNumbers<N> και δεν περνούν ένα δεύτερο τύπο παραμέτρου, τότε αυτό θα αξιολογηθεί σε UseNumbers<N> όπως επιθυμείται.

Σύνδεσμος playground για κώδικα