8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[TypeScript] `any non-nullish value`としての`{}`型

Last updated at Posted at 2021-12-19

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>unknownRecord<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>unknownRecord<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、つまりundefinednullだけを除外する制約になっています。

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

ちゃんとundefinednullだけ除外できました。any non-nullish valueとしてはこうなってほしいですね。

みんな大好き(?)type-challengesで使われているEqualで比較しても一致します。

type TEST = Equal<{}, Record<never, never>>;
// -> type TEST = true;

やはり、{}をそのままの意味、any non-nullish valueとして置き換えるならRecord<never, never>を使うのが良さそうです。

8
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?