J'essaie de contraindre l'entrée d'un générique à l'un des nombreux types. La notation la plus proche que j'ai trouvée utilise des types d'union. Voici un exemple trivial:
interface IDict<TKey extends string | number, TVal> {
// Error! An index signature parameter type must be
// a 'string' or a 'number'
[key: TKey]: TVal;
}
declare const dictA: IDict<string, Foo>;
declare const dictB: IDict<number, Foo>;
Ce que je recherche, dans cet exemple, est une façon de dire que TKey
doit être soit string
ou number
, mais pas leur union.
Pensées?
Remarque: Il s'agit d'un cas spécifique d'une question plus large. Par exemple, j'ai un autre cas où j'ai une fonction qui accepte text
qui peut être soit un string
ou StructuredText
(analyse Markdown), la transforme et renvoie exactement le type correspondant (pas un sous-type).
function formatText<T extends string | StructuredText>(text: T): T {/*...*/}
Techniquement, je pourrais écrire cela comme une surcharge, mais cela ne semble pas être la bonne façon.
function formatText(text: string): string;
function formatText(text: StructuredText): StructuredText;
function formatText(text) {/*...*/}
Une surcharge s'avère également problématique, car elle n'accepte pas un type d'union:
interface StructuredText { tokens: string[] }
function formatText(txt: string): string;
function formatText(txt: StructuredText): StructuredText;
function formatText(text){return text;}
let s: string | StructuredText;
let x = formatText(s); // error
Mis à jour pour TS3.5 + le 2019-06-20
K extends string | number
pour le paramètre de signature d'index:Oui, cela ne peut pas être fait de manière très satisfaisante. Il y a quelques problèmes. La première est que TypeScript ne reconnaît que deux types de signature d'index direct: [k: string]
, et [k: number]
. C'est tout. Vous ne pouvez pas faire l'union de ceux-ci (pas de [k: string | number]
), ou un sous-type de ceux-ci (pas de [k: 'a'|'b']
), ou même un alias de ceux-ci: (pas de [k: s]
où type s = string
).
Le deuxième problème est que number
en tant que type d'index est un cas spécial étrange qui ne se généralise pas bien au reste de TypeScript. En JavaScript, tous les index d'objets sont convertis en leur valeur de chaîne avant d'être utilisés. Cela veut dire que a['1']
et a[1]
sont le même élément. Donc, dans un certain sens, le type number
en tant qu'index ressemble plus à un sous-type de string
. Si vous êtes prêt à abandonner les littéraux number
et à les convertir à la place en littéraux string
, vous aurez plus de facilité.
Si c'est le cas, vous pouvez utiliser types mappés pour obtenir le comportement souhaité. En fait, il existe un type appelé Record<>
c'est inclus dans la bibliothèque standard c'est exactement ce que je suggère d'utiliser:
type Record<K extends string, T> = {
[P in K]: T;
};
type IDict<TKey extends string, TVal> = Record<TKey, TVal>
declare const dictString: IDict<string, Foo>; // works
declare const dictFooBar: IDict<'foo' | 'bar', Foo>; // works
declare const dict012: IDict<'0' | '1' | '2', Foo>; // works
dict012[0]; // okay, number literals work
dict012[3]; // error
declare const dict0Foo: IDict<'0' | 'foo',Foo>; // works
Assez proche du travail. Mais:
declare const dictNumber: IDict<number, Foo>; // nope, sorry
La pièce manquante permettant à number
de fonctionner serait un type comme numericString
défini comme
type numericString = '0'|'1'|'2'|'3'|'4'|'5'|'6'|'7' // ... etc etc
puis vous pouvez utiliser IDict<numericString, Foo>
qui se comporterait comme vous le souhaitez IDict<number, Foo>
à. Sans un type comme celui-là, il ne sert à rien d'essayer de forcer TypeScript à le faire. Je recommanderais d'abandonner, sauf si vous avez un cas d'utilisation très convaincant.
Je pense que je comprends ce que vous voulez ici. L'idée est que vous souhaitez une fonction qui prend un argument d'un type qui étend une union comme string | number
, mais il doit renvoyer un type qui est élargi à un ou plusieurs des éléments de cette union. Vous essayez d'éviter un problème avec les sous-types. Donc, si l'argument est 1
, vous ne voulez pas vous engager à sortir un 1
, juste un number
.
Avant maintenant, je dirais simplement utiliser des surcharges:
function zop(t: string): string; // string case
function zop(t: number): number; // number case
function zop(t: string | number): string | number; // union case
function zop(t: string | number): string | number { // impl
return (typeof t === 'string') ? (t + "!") : (t - 2);
}
Cela se comporte comme vous le souhaitez:
const zopNumber = zop(1); // return type is number
const zopString = zop('a'); // return type is string
const zopNumberOrString = zop(
Math.random()<0.5 ? 1 : 'a'); // return type is string | number
Et c'est la suggestion que je ferais si vous n'avez que deux types dans votre syndicat. Mais cela pourrait devenir compliqué pour les grands syndicats (par exemple, string | number | boolean | StructuredText | RegExp
), car vous devez inclure une signature de surcharge pour chaque sous-ensemble d'éléments non vide de l'union.
Au lieu de surcharges, nous pouvons utiliser types conditionnels :
// OneOf<T, V> is the main event:
// take a type T and a Tuple type V, and return the type of
// T widened to relevant element(s) of V:
type OneOf<
T,
V extends any[],
NK extends keyof V = Exclude<keyof V, keyof any[]>
> = { [K in NK]: T extends V[K] ? V[K] : never }[NK];
Voici comment cela fonctionne:
declare const str: OneOf<"hey", [string, number, boolean]>; // string
declare const boo: OneOf<false, [string, number, boolean]>; // boolean
declare const two: OneOf<1 | true, [string, number, boolean]>; // number | boolean
Et voici comment déclarer votre fonction:
function zop<T extends string | number>(t: T): OneOf<T, [string, number]>;
function zop(t: string | number): string | number { // impl
return (typeof t === 'string') ? (t + "!") : (t - 2);
}
Et il se comporte comme avant:
const zopNumber = zop(1); // 1 -> number
const zopString = zop('a'); // 'a' -> string
const zopNumberOrString = zop(
Math.random()<0.5 ? 1 : 'a'); // 1 | 'a' -> string | number
Ouf. J'espère que cela pourra aider; bonne chance!