4
votes

Tuple avec un type différent pour le dernier élément (commencez par l'élément rest)

J'ai un type Foo qui est un Array qui peut contenir n'importe quel nombre d'éléments Bar , avec un dernier élément Qux facultatif.

Voici quelques exemples de données valides:

[qux, qux]
[qux, bar]
[bar, bar, qux, bar]
[bar, bar, qux, qux]

Exemples de données invalides:

[]
[bar]
[qux]
[bar, qux]
[bar, bar, bar, bar, bar]
[bar, bar, bar, qux]

Actuellement, je l'ai comme type Foo = Array<Bar | Qux> , mais cela ne capture pas le fait qu'un seul Qux est autorisé, et uniquement en tant que dernier élément.

Je ne sais pas si je devrais même m'attendre à ce que Typescript soit capable d'exprimer cela, ou s'il y a un avantage pratique si cela pourrait être réalisé.


2 commentaires

Puisque les éléments de repos doivent être les derniers, je ne vois pas de moyen simple de l'implémenter. Voir ce ticket et ce ticket pour plus de discussions sur le sujet. Cette réponse pourrait aider avec une solution alambiquée.


@Nit Merci, c'est exactement l'information que je recherchais. Si vous le postez comme réponse, je serais heureux de l'accepter.


3 Réponses :


1
votes

Étant donné qu'en Javascript, les paramètres rest ne sont autorisés que comme dernier paramètre, la même approche a été historiquement adoptée pour les types dans Typescript.
Il existe des tickets de longue date pour supprimer cette limitation pour les types, voir ici et ici , mais pour le moment, ce n'est toujours pas pris en charge.

Étant donné que vous ne pouvez pas définir strictement des tuples spécifiques suivis de repos dans Typescript, il ne semble pas y avoir de moyen simple de l'implémenter tant que la fonctionnalité n'est pas ajoutée à Typescript lui-même.

Alternativement, vous pouvez générer manuellement un nombre confortable de listes de types valides dans une étape de construction distincte, mais à ce stade, je doute que la valeur gagnée en vaille la peine.


0 commentaires

6
votes

Mise à jour: TS 4.0

Dans JS, les paramètres de repos doivent toujours être le dernier paramètre de fonction. TS 4.0 est livré avec une fonctionnalité appelée types de tuple variadiques , que vous pouvez considérer ici comme des "paramètres de repos pour les types".

La chose intéressante est que les types de tuple variadiques peuvent être placés n'importe où dans les paramètres de fonction, pas seulement à la fin. Nous pouvons maintenant définir une quantité arbitraire d'éléments de bar avec un dernier élément optionnel qux .

Exemple: [bar, bar, <and arbitrary more>, qux] peut être interprété comme un tuple variadique [...T, qux] , où T représente tous les éléments de la bar .

Solution 1: Type d' Assert distinct ( Playground )

const arr1 = [{ bar: "bar1" }, { bar: "foo1" }, { qux: 42 }] as const
const arr2 = [{ bar: "bar2" }, { bar: "foo2" }] as const
const arr3 = [{ qux: 42 }] as const

const typedArr1: AssertQuxIsLast<typeof arr1> = arr1
const typedArr2: AssertQuxIsLast<typeof arr2> = arr2 // error (OK)
const typedArr3: AssertQuxIsLast<typeof arr3> = arr3

Vous pouvez remplacer la assert fonction d'aide avec une sorte de type « sentinelle »:

type Bar = { bar: string };
type Qux = { qux: number };

// asserts Qux for the last index of T, other elements have to be Bar
type AssertQuxIsLast<T extends readonly any[]> = {
  [K in keyof T]: StringToNumber[Extract<K, string>] extends LastIndex<T>
    ? T[K] extends Qux
      ? Qux
      : never
    : T[K] extends Bar
    ? Bar
    : never;
};

// = calculates T array length minus 1
type LastIndex<T extends readonly any[]> = ((...t: T) => void) extends ((
  x: any,
  ...u: infer U
) => void)
  ? U["length"]
  : never;

// TS doesn't support string to number type conversions 😥,
// so we support (index) numbers up to 256
export type StringToNumber = {
  [k: string]: number;
  0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, 10: 10, 11: 11, 12: 12, 13: 13, 14: 14, 15: 15, 16: 16, 17: 17, 18: 18, 19: 19, 20: 20, 21: 21, 22: 22, 23: 23, 24: 24, 25: 25, 26: 26, 27: 27, 28: 28, 29: 29, 30: 30, 31: 31, 32: 32, 33: 33, 34: 34, 35: 35, 36: 36, 37: 37, 38: 38, 39: 39, 40: 40, 41: 41, 42: 42, 43: 43, 44: 44, 45: 45, 46: 46, 47: 47, 48: 48, 49: 49, 50: 50, 51: 51, 52: 52, 53: 53, 54: 54, 55: 55, 56: 56, 57: 57, 58: 58, 59: 59, 60: 60, 61: 61, 62: 62, 63: 63, 64: 64, 65: 65, 66: 66, 67: 67, 68: 68, 69: 69, 70: 70, 71: 71, 72: 72, 73: 73, 74: 74, 75: 75, 76: 76, 77: 77, 78: 78, 79: 79, 80: 80, 81: 81, 82: 82, 83: 83, 84: 84, 85: 85, 86: 86, 87: 87, 88: 88, 89: 89, 90: 90, 91: 91, 92: 92, 93: 93, 94: 94, 95: 95, 96: 96, 97: 97, 98: 98, 99: 99, 100: 100, 101: 101, 102: 102, 103: 103, 104: 104, 105: 105, 106: 106, 107: 107, 108: 108, 109: 109, 110: 110, 111: 111, 112: 112, 113: 113, 114: 114, 115: 115, 116: 116, 117: 117, 118: 118, 119: 119, 120: 120, 121: 121, 122: 122, 123: 123, 124: 124, 125: 125, 126: 126, 127: 127, 128: 128, 129: 129, 130: 130, 131: 131, 132: 132, 133: 133, 134: 134, 135: 135, 136: 136, 137: 137, 138: 138, 139: 139, 140: 140, 141: 141, 142: 142, 143: 143, 144: 144, 145: 145, 146: 146, 147: 147, 148: 148, 149: 149, 150: 150, 151: 151, 152: 152, 153: 153, 154: 154, 155: 155, 156: 156, 157: 157, 158: 158, 159: 159, 160: 160, 161: 161, 162: 162, 163: 163, 164: 164, 165: 165, 166: 166, 167: 167, 168: 168, 169: 169, 170: 170, 171: 171, 172: 172, 173: 173, 174: 174, 175: 175, 176: 176, 177: 177, 178: 178, 179: 179, 180: 180, 181: 181, 182: 182, 183: 183, 184: 184, 185: 185, 186: 186, 187: 187, 188: 188, 189: 189, 190: 190, 191: 191, 192: 192, 193: 193, 194: 194, 195: 195, 196: 196, 197: 197, 198: 198, 199: 199, 200: 200, 201: 201, 202: 202, 203: 203, 204: 204, 205: 205, 206: 206, 207: 207, 208: 208, 209: 209, 210: 210, 211: 211, 212: 212, 213: 213, 214: 214, 215: 215, 216: 216, 217: 217, 218: 218, 219: 219, 220: 220, 221: 221, 222: 222, 223: 223, 224: 224, 225: 225, 226: 226, 227: 227, 228: 228, 229: 229, 230: 230, 231: 231, 232: 232, 233: 233, 234: 234, 235: 235, 236: 236, 237: 237, 238: 238, 239: 239, 240: 240, 241: 241, 242: 242, 243: 243, 244: 244, 245: 245, 246: 246, 247: 247, 248: 248, 249: 249, 250: 250, 251: 251, 252: 252, 253: 253, 254: 254, 255: 255
};

Solution 2: surcharge de fonctions ( Playground )

type Bar = "bar"
type Qux = "qux"

function assert<T extends Bar[] = []>(
    arg: (Qux | Bar)[] extends [...T, Qux] ? never : [...T, Qux]): [...T, Qux]
function assert<T extends Bar[] = []>(arg: [...T]): [...T]
function assert<T extends Bar[] = []>(arg: [...T, Qux?]) { return arg }

Ancienne réponse (<= TS 3.9)

Si vous avez vraiment besoin de paramètres de repos suivis d'un dernier élément (la spécification ne les autorise qu'à être les derniers, voir la réponse de @ Nit), voici une alternative:

Hypothèses

  • Une valeur de tableau concrète est présente et typée (par exemple as const )
  • taille du tableau jusqu'à 256 éléments (limite de taille codée en dur StringToNumber )
  • Vous ne voulez pas déclarer le type d'assertion Bar / Qux manière récursive : bien que techniquement possible, les types récursifs ne sont pas officiellement supportés jusqu'à la version 4.1 ( Note: mise à jour ).

Code ( aire de jeux )

type Sentinel<T, U extends T> = U
const arr1 = ["bar", "bar"] as const
const arr2 = ["qux", "bar"] as const
type Arr1Type = Sentinel<Assert<typeof arr1>, typeof arr1> // OK
type Arr2Type = Sentinel<Assert<typeof arr2>, typeof arr2> // error

Des tests

type Assert<T extends readonly (Bar | Qux)[]> =
    T extends readonly [] ? T :
    T extends readonly [...infer I, infer U] ?
    I extends [] ? T :
    I extends Bar[] ? T : readonly [...{ [K in keyof I]: Bar }, U] :
    never

function assert<T extends readonly (Bar | Qux)[] | readonly [Bar | Qux]>(
  a: Assert<T>): T { return a as T }

// OK
const ok1 = assert([])
const ok2 = assert(["bar"])
const ok3 = assert(["qux"])
const ok4 = assert(["bar", "qux"])
const ok5 = assert(["bar", "bar", "bar"])
const ok6 = assert(["bar", "bar", "bar", "qux"])

// errors
const err1 = assert(["qux", "qux"])
const err2 = assert(["qux", "bar"])
const err3 = assert(["bar", "bar", "qux", "bar"])
const err4 = assert(["bar", "bar", "qux", "qux"])

Explications

AssertQuxIsLast est un type de tuple mappé . Chaque clé K dans T est un index de la forme "0" , "1" , "2" , etc. Mais ce type est une string , pas un number !

Afin d'affirmer Qux pour le dernier indice, nous devons convertir K revenir au number afin de le comparer avec T['length'] , qui retourne la longueur du tableau number . Comme le compilateur ne prend pas encore en charge une conversion de chaîne dynamique en nombre , nous avons créé notre propre convertisseur avec StringToNumber .


2 commentaires

C'est une réponse étonnamment élaborée. Merci. Ma conclusion est qu'à toutes fins pratiques, cela n'en vaudrait pas la peine.


@ last-child merci, vous êtes les bienvenus. Mis à jour avec une alternative TS 4.0



1
votes

Cette fonctionnalité devrait être livrée avec TS 4.2 .

Exemple du PR de mise en œuvre :

type Foo = [...Bar[], Qux]

Votre type deviendra

function f1(...args: [...string[], number]) {
    const strs = args.slice(0, -1) as string[];
    const num = args[args.length - 1] as number;
    // ...
}

f1(5);
f1('abc', 5);
f1('abc', 'def', 5);
f1('abc', 'def', 5, 6);  // Error


0 commentaires