typescript - Inferă/restrânge argumentul funcției din proprietatea fratără


Vreau să restrâng tipul unui argument de funcție pe baza unei proprietăți înrudite. Știu că tipurile existențiale ar fi de ajutor, dar nu sunt disponibile, așa că folosesc o funcție ajutătoare. În prezent am asta datorită lui jcalz în comentarii

// helper function
function helper<T extends readonly unknown[]>(o: {
  choices: T;
  func: (opts: T[number]) => T[number];
}) {
  return o;
}

helper({
  choices: [
    "one",
    "two",
    "three",
  ], // as const
  func(o) { // should be "one" | "two" | "three", but is string
    return o;
  }
})

Vreau ca o din func să fie de tip "one" | "two" | "three", așa că problema mea este că TypeScript nu inferă opțiunile ca un tuple, ci ca string[]. Nu pot să adaug as const la choices pentru că funcția va fi apelată în JavaScript.

Știu că aș putea să împart opțiunile și funcția în argumente separate ale funcției ajutătoare, dar acest lucru nu este posibil deoarece vor exista mai multe proprietăți în obiect. Scopul proprietăților este de a descrie func, ce are nevoie ca intrare și ce returnează.

Scopul final este să am funcția ajutătoare scrisă în TypeScript, dar folosită ca o adnotare de tip în JavaScript. Prin urmare, nu pot furniza niciun tip atunci când apelez funcția sau să folosesc as const acolo unde este marcat cu un comentariu.

Răspuns #1

Ai dori ca helper() să trateze literalul de array value ["one", "two", "three"] ca având tipul ["one", "two", "three"]; adică, ca un tuplu de tipuri literale string în loc de doar string[]. Acest lucru este destul de ușor de făcut din partea apelantului funcției; apelanții pot folosi o afirmație const ca ["one", "two", "three"] as const. Dar în cazurile în care apelanții nu doresc (sau nu pot) să folosească o afirmație const, ar fi frumos ca funcția însăși să se ocupe de acest lucru.

Am deschis microsoft/TypeScript#30680 acum ceva timp, întrebând despre o modalitate ușoară de a face asta. Poate ar arăta așa:

// INVALID TYPESCRIPT SYNTAX, DO NOT TRY THIS
//                        vvvvv
function helper<T extends const readonly unknown[]>(o: {
    choices: T;
    func: (opts: T[number]) => T[number];
}) {
    return o;
}

unde const ar putea fi aplicat cumva parametrului de tip generic. Din păcate, acest lucru nu face parte în prezent din limbaj.

Din fericire, există trucuri pentru a determina compilatorul să se comporte într-un fel oarecum similar.


Dacă vrei să dai compilatorului un indiciu că ar trebui să deducă tipuri literale, poți consulta microsoft/TypeScript#10676. Dacă un parametru de tip este restricționat la un tip care include string, atunci compilatorul va tinde să deducă tipuri literale de string. Același lucru este valabil și pentru number. Deci, în timp ce un T nerestricționat (cum ar fi T extends unknown) va deduce string atunci când vede "one", un T extends string restricționat va deduce "one". Dacă nu vrei cu adevărat să restricționezi parametrul de tip, atunci trebuie să găsești cea mai largă constrângere posibilă care să conțină explicit string și/sau number. Nu poți folosi tipul unknown deoarece T extends string | unknown se prăbușește imediat la T extends unknown. Totuși, poți face ceva de genul

type Narrowable = string | number | bigint | boolean | 
  symbol | object | undefined | void | null | {};

Și apoi scrie T extends Narrowable în loc de T extends unknown. Sau, în cazul tău, T extends readonly Narrowable[] în loc de T extends readonly unknown[].


Dacă vrei ca compilatorul să deducă tupluri în loc de tipuri de array, există și alte trucuri. TypeScript 4.0 a introdus tipurile de tupluri variadice. Dacă U este un parametru de tip asemănător unui array, atunci compilatorul va tinde să vadă (arr: U) și să deducă un tip de array neordonat pentru U. Dar dacă scrii (arr: [...U]) sau (arr: readonly [...U]), va tinde să deducă un tip de tuplu.


Combinând acestea obținem:

function helper<T extends readonly Narrowable[]>(o: {
    choices: readonly [...T];
    func: (opts: T[number]) => T[number];
}) {
    return o;
}

Și poți verifica că funcționează:

helper({
    choices: [
        "one",
        "two",
        "three",
    ], // as const
    func(o) { // "one" | "two" | "three"
        return o;
    }
})

Arată bine.

Link de teren de joacă către cod