web-dev-qa-db-ja.com

TypeScriptジェネリックの型をいくつかの型のいずれかに制限する

ジェネリックの入力をいくつかのタイプの1つに制限しようとしています。私が見つけた最も近い表記は、共用体タイプを使用しています。ここに簡単な例があります:

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>;

この例で私が探しているのは、TKeystringまたはnumberのいずれかでなければならないが、それらの和集合ではないという方法です。

考え?

注:これは、幅広い質問の特定のケースです。たとえば、textまたはstring(解析済みMarkdown)のいずれかであるStructuredTextを受け入れ、それを変換して、正確に返す関数がある別のケースがあります。対応するタイプ(サブタイプではない)。

function formatText<T extends string | StructuredText>(text: T): T {/*...*/}

技術的にはオーバーロードとしてそれを書くことができましたが、それは正しい方法のようには思えません。

function formatText(text: string): string;
function formatText(text: StructuredText): StructuredText;
function formatText(text) {/*...*/}

オーバーロードは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
16
bjnsn

2019-06-20のTS3.5 +用に更新

問題#1:インデックス署名パラメーターのK extends string | number

ええ、これは非常に満足できる方法で行うことはできません。いくつかの問題があります。 1つ目は、TypeScriptが認識するのは、2つの直接インデックスシグネチャタイプである[k: string][k: number]だけです。それでおしまい。これらの和集合([k: string | number]なし)、またはそれらのサブタイプ([k: 'a'|'b']なし)、またはそれらのエイリアスでさえできません:(no [k: s] where type s = string)。

2番目の問題は、インデックスタイプとしてのnumberが奇妙な特殊なケースであり、残りのTypeScriptに一般化できないことです。 JavaScriptでは、すべてのオブジェクトインデックスは、使用される前に文字列値に変換されます。つまり、a['1']a[1]は同じ要素です。したがって、ある意味では、インデックスとしてのnumberタイプは、stringのサブタイプに似ています。 numberリテラルをあきらめて、代わりにstringリテラルに変換することをいとわない場合は、より簡単な方法があります。

その場合は、 マップされたタイプ を使用して、必要な動作を取得できます。実際、Record<>と呼ばれるタイプがあります 標準ライブラリに含まれています これは、私が使用することをお勧めします:

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

仕事にかなり近い。だが:

declare const dictNumber: IDict<number, Foo>; // nope, sorry

numberを機能させるための欠落している部分は、numericStringのような型で、

type numericString = '0'|'1'|'2'|'3'|'4'|'5'|'6'|'7' // ... etc etc

次に、IDict<numericString, Foo>を使用して、IDict<number, Foo>のように動作させることができます。そのような型がないと、TypeScriptに強制的にこれを実行させようとしても意味がありません。非常に説得力のあるユースケースがない限り、あきらめることをお勧めします。

問題#2:リストから型に拡張できるジェネリック:

ここで何が欲しいのか理解できたと思います。アイデアは、string | numberのような共用体を拡張する型の引数を取る関数が必要であるということですが、その共用体の1つ以上の要素に拡張された型を返す必要があります。サブタイプの問題を回避しようとしています。したがって、引数が1の場合、1の出力をコミットせず、numberのみを使用します。

これまでは、オーバーロードを使用するだけでした。

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);
}

これはあなたが望むように動作します:

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

そしてそれはあなたがあなたの組合に2つのタイプしかない場合に私が与えるだろう提案です。ただし、ユニオンの要素の空でないサブセットごとに1つのオーバーロードシグネチャを含める必要があるため、大きなユニオン(たとえば、string | number | boolean | StructuredText | RegExp)の場合、扱いにくくなる可能性があります。

オーバーロードの代わりに conditional types を使用できます:

// 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];

以下にその仕組みを示します。

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

そして、これがあなたの関数を宣言する方法です:

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);
}

そしてそれは以前と同じように動作します:

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

ふew。お役に立てば幸いです。幸運を!

コードへのリンク

12
jcalz