TypeScriptでプログラミングしていると{}
型をGenericsの制約とかで使おうとして、よくeslintに怒られます。
type Foo<T extends {}> = { [KEY in keyof T]: Bar<T[KEY]> };
// Don't use `{}` as a type. `{}` actually means "any non-nullish value".
// - If you want a type meaning "any object", you probably want `Record<string, unknown>` instead.
// - If you want a type meaning "any value", you probably want `unknown` instead.
// - If you want a type meaning "empty object", you probably want `Record<string, never>` instead.eslint@typescript-eslint/ban-types
曰く
型として
{}
を使用しないでください。実際には{}
は「任意の非ヌル値」を意味します。
- もし「任意のオブジェクト」を意味する型なら、多分
Record<string, unknown>
が代わりになるでしょう。- もし「任意の値」を意味する型なら、多分
unknown
が代わりになるでしょう。- もし「空のオブジェクト」を意味する型なら、多分
Record<string, never>
が代わりになるでしょう。 eslint@typescript-eslint/ban-types
しかし、警告に書いてある、any non-nullish value
としての{}
型を使おうとしたとき、ここに上げられているRecord<string, unknown>
やunknown
、Record<string, never>
では代わりになりませんでした。
any non-nullish value
としての{}
型の代わりには何を使えば良いのでしょうか?
TL; DR
Record<never, never>
が使えました。
type D<T extends Record<never, never>> = T;
type Dundefined = D<undefined>;
// -> 型 'undefined' は制約 'Record<never, never>' を満たしていません。
type Dnull = D<null>;
// -> 型 'null' は制約 'Record<never, never>' を満たしていません。
type Dtrue = D<true>;
type Dfalse = D<false>;
type D1 = D<1>;
type Dabc = D<'abc'>;
type Darray = D<[]>;
type Dempty = D<{}>;
type Dobject = D<{ abc: true }>;
type Diterator = D<{ [Symbol.iterator]: true }>;
Recordの第2引数はnever
でなくとも、string
でもunknown
でもOKなので、要はkeyがない(never
)ってことのようです。
{}
の代わりに使ってみる
eslintの警告にあるようにRecord<string, unknown>
やunknown
、Record<string, never>
を{}
の代わりに使ってみます。
まずは元の{}
を使ったもの。
type O<T extends {}> = T;
type Oundefined = O<undefined>;
// -> 型 'undefined' は制約 '{}' を満たしていません。
type Onull = O<null>;
// -> 型 'null' は制約 '{}' を満たしていません。
type Otrue = O<true>;
type Ofalse = O<false>;
type O1 = O<1>;
type Oabc = O<'abc'>;
type Oarray = O<[]>;
type Oempty = O<{}>;
type Oobject = O<{ abc: true }>;
type Oiterator = O<{ [Symbol.iterator]: true }>;
any non-nullish value
、つまりundefined
とnull
だけを除外する制約になっています。
Record<string, unknown>
type A<T extends Record<string, unknown>> = T;
type Aundefined = A<undefined>;
// -> 型 'undefined' は制約 'Record<string, unknown>' を満たしていません。
type Anull = A<null>;
// -> 型 'null' は制約 'Record<string, unknown>' を満たしていません。
type Atrue = A<true>;
// -> 型 'boolean' は制約 'Record<string, unknown>' を満たしていません。
type Afalse = A<false>;
// -> 型 'boolean' は制約 'Record<string, unknown>' を満たしていません。
type A1 = A<1>;
// -> 型 'number' は制約 'Record<string, unknown>' を満たしていません。
type Aabc = A<'abc'>;
// -> 型 'string' は制約 'Record<string, unknown>' を満たしていません。
type Aarray = A<[]>;
// -> 型 '[]' は制約 'Record<string, unknown>' を満たしていません。
// 型 'string' is missing in type '[]' のインデックス シグネチャがありません。
type Aempty = A<{}>;
type Aobject = A<{abc: true}>;
type Aiterator = A<{[Symbol.iterator]: true}>;
オブジェクト型以外の型を除外(配列も除外)するには役に立ちそうですが、any non-nullish value
ではないですね。
unknown
type B<T extends unknown> = T;
type Bundefined = B<undefined>;
type Bnull = B<null>;
type Btrue = B<true>;
type Bfalse = B<false>;
type B1 = B<1>;
type Babc = B<'abc'>;
type Barray = B<[]>;
type Bempty = B<{}>;
type Bobject = B<{ abc: true }>;
type Biterator = B<{ [Symbol.iterator]: true }>;
extends unknown
では何の制約にもなりませんが、分かりやすいように明示的に書いておきます。
まあ書いたように何の制約にもならないのですべてエラー無しです。{}
の代わりには使えません。
eslintの警告にもあるように任意の型として使うなら、ということなのでしょう。
Record<string, never>
type C<T extends Record<string, never>> = T;
type Cundefined = C<undefined>;
// -> 型 'undefined' は制約 'Record<string, never>' を満たしていません。
type Cnull = C<null>;
// -> 型 'null' は制約 'Record<string, never>' を満たしていません。
type Ctrue = C<true>;
// -> 型 'boolean' は制約 'Record<string, never>' を満たしていません。
type Cfalse = C<false>;
// -> 型 'boolean' は制約 'Record<string, never>' を満たしていません。
type C1 = C<1>;
// -> 型 'number' は制約 'Record<string, never>' を満たしていません。
type Cabc = C<'abc'>;
// -> 型 'string' は制約 'Record<string, never>' を満たしていません。
type Carray = C<[]>;
// -> 型 '[]' は制約 'Record<string, never>' を満たしていません。
// 型 'string' is missing in type '[]' のインデックス シグネチャがありません。
type Cempty = C<{}>;
type Cobject = C<{ abc: true }>;
// -> 型 '{ abc: true; }' は制約 'Record<string, never>' を満たしていません。
// プロパティ 'abc' はインデックス シグネチャと互換性がありません。
// 型 'boolean' を型 'never' に割り当てることはできません。
type Citerator = C<{ [Symbol.iterator]: true }>;
空のオブジェクト{}
だけが通るのかと思いきや、シンボルを指定した{ [Symbol.iterator]: true }
も通ってしまいました。
シンボルで指定されたプロパティも除外するならRecord<string|symbol, never>
とするべきでしょうか?
ついでなのでkeyとして指定できるnumber
も付け加えておきましょう。
type C2<T extends Record<string | symbol | number, never>> = T;
type C2undefined = C2<undefined>;
// -> 型 'undefined' は制約 'Record<string | symbol | number, never>' を満たしていません。
type C2null = C2<null>;
// -> 型 'null' は制約 'Record<string | symbol | number, never>' を満たしていません。
type C2true = C2<true>;
// -> 型 'boolean' は制約 'Record<string | symbol | number, never>' を満たしていません。
type C2false = C2<false>;
// -> 型 'boolean' は制約 'Record<string | symbol | number, never>' を満たしていません。
type C21 = C2<1>;
// -> 型 'number' は制約 'Record<string | symbol | number, never>' を満たしていません。
type C2abc = C2<'abc'>;
// -> 型 'string' は制約 'Record<string | symbol | number, never>' を満たしていません。
type C2array = C2<[]>;
// -> 型 '[]' は制約 'Record<string | symbol | number, never>' を満たしていません。
// 型 'string' is missing in type '[]' のインデックス シグネチャがありません。
type C2empty = C2<{}>;
type C2object = C2<{ abc: true }>;
// -> 型 '{ abc: true; }' は制約 'Record<string | symbol | number, never>' を満たしていません。
// プロパティ 'abc' はインデックス シグネチャと互換性がありません。
// 型 'boolean' を型 'never' に割り当てることはできません。
type C2iterator = C2<{ [Symbol.iterator]: true }>;
// -> 型 '{ [Symbol.iterator]: true; }' は制約 'Record<string | number | symbol, never>' を満たしていません。
// プロパティ '[Symbol.iterator]' はインデックス シグネチャと互換性がありません。
// 型 'boolean' を型 'never' に割り当てることはできません。
{}
以外は除外できるようになりました。それに何か意味があるのかどうかは分かりませんが。
any non-nullish value
としての{}
型の代わりにはなりませんでした。
空のオブジェクトとは?
ここで初心に戻って{}
、つまり空のオブジェクトとは何なのか考えてみます。
空のオブジェクト、{}
とは、プロパティを1つも持たないもの、です。
通常のオブジェクトは、{ [KEY in keyof T]: T[KEY] }
で表せるようにkeyof T
が存在していますが、空のオブジェクトにはそれが存在していないので、keyof T
が何もない、つまりnever
となります。
type KEYS<T> = keyof T;
type KEYSEMPTY = KEYS<{}>;
// -> type KEYSEMPTY = never;
ここから逆算して型を作ってみると
type EMPTYOBJECT = { [KEY in never]: unknown };
となります。
Recordを使って書きなおせば
type EMPTYOBJECT = Record<never, unknown>;
となります。
プロパティの型は存在していないので何でも構いません。どうせならneverを強調して
type EMPTYOBJECT = Record<never, never>;
としておきましょう。
使ってみる
type D<T extends Record<never, never>> = T;
type Dundefined = D<undefined>;
// -> 型 'undefined' は制約 'Record<never, never>' を満たしていません。
type Dnull = D<null>;
// -> 型 'null' は制約 'Record<never, never>' を満たしていません。
type Dtrue = D<true>;
type Dfalse = D<false>;
type D1 = D<1>;
type Dabc = D<'abc'>;
type Darray = D<[]>;
type Dempty = D<{}>;
type Dobject = D<{ abc: true }>;
type Diterator = D<{ [Symbol.iterator]: true }>;
ちゃんとundefined
とnull
だけ除外できました。any non-nullish value
としてはこうなってほしいですね。
みんな大好き(?)type-challengesで使われているEqual
で比較しても一致します。
type TEST = Equal<{}, Record<never, never>>;
// -> type TEST = true;
やはり、{}
をそのままの意味、any non-nullish value
として置き換えるならRecord<never, never>
を使うのが良さそうです。