TypeScriptに取り組む
この書籍を応援してください:購入または寄付
(広告です、ブロックしないでください。)

15 オブジェクトの型付け



この章では、TypeScriptでオブジェクトとプロパティがどのように静的に型付けされるかを探ります。

15.1 オブジェクトが果たす役割

JavaScriptでは、オブジェクトは2つの役割を果たすことができます(常に少なくとも1つ、時には混在)。

まず、オブジェクトをレコードとして探ります。辞書としてのオブジェクトには、この章の後半で簡単に触れます。

15.2 オブジェクトの型

オブジェクトには2つの異なる一般的な型があります。

オブジェクトは、そのプロパティを通じて型付けすることもできます。

// Object type literal
let obj3: {prop: boolean};

// Interface
interface ObjectType {
  prop: boolean;
}
let obj4: ObjectType;

次のセクションでは、これらのオブジェクトの型付け方法を詳しく見ていきます。

15.3 TypeScriptにおけるObjectobject

15.3.1 プレーンなJavaScript:オブジェクトとObjectのインスタンス

プレーンなJavaScriptでは、重要な区別があります。

一方、ほとんどのオブジェクトはObjectのインスタンスです。

> const obj1 = {};
> obj1 instanceof Object
true

これはつまり

他方、プロトタイプチェーンにObject.prototypeを持たないオブジェクトを作成することもできます。たとえば、次のオブジェクトにはプロトタイプがまったくありません。

> const obj2 = Object.create(null);
> Object.getPrototypeOf(obj2)
null

obj2は、クラスObjectのインスタンスではないオブジェクトです。

> typeof obj2
'object'
> obj2 instanceof Object
false

15.3.2 TypeScriptにおけるObject(大文字の「O」):クラスObjectのインスタンス

各クラスCは2つのエンティティを作成することを思い出してください。

同様に、TypeScriptには2つの組み込みインターフェースがあります。

これらがインターフェースです。

interface Object { // (A)
  constructor: Function;
  toString(): string;
  toLocaleString(): string;
  valueOf(): Object;
  hasOwnProperty(v: PropertyKey): boolean;
  isPrototypeOf(v: Object): boolean;
  propertyIsEnumerable(v: PropertyKey): boolean;
}

interface ObjectConstructor {
  /** Invocation via `new` */
  new(value?: any): Object;
  /** Invocation via function calls */
  (value?: any): any;

  readonly prototype: Object; // (B)

  getPrototypeOf(o: any): any;

  // ···
}
declare var Object: ObjectConstructor; // (C)

観察

15.3.3 TypeScriptにおけるobject(小文字の「o」):非プリミティブ値

TypeScriptでは、objectはすべての非プリミティブ値(プリミティブ値はundefinednull、ブール値、数値、bigint、文字列)の型です。この型では、値のプロパティにアクセスすることはできません。

15.3.4 Objectobject:プリミティブ値

興味深いことに、型Objectはプリミティブ値とも一致します。

function func1(x: Object) { }
func1('abc'); // OK

なぜでしょうか?プリミティブ値はObject.prototypeを継承するため、Objectに必要なすべてのプロパティを持っています。

> 'abc'.hasOwnProperty === Object.prototype.hasOwnProperty
true

逆に、objectはプリミティブ値と一致しません。

function func2(x: object) { }
// @ts-expect-error: Argument of type '"abc"' is not assignable to
// parameter of type 'object'. (2345)
func2('abc');

15.3.5 Objectobject:互換性のないプロパティ型

Objectを使用すると、オブジェクトにインターフェースObjectの対応するプロパティと競合する型のプロパティがある場合、TypeScriptは警告を発します。

// @ts-expect-error: Type '() => number' is not assignable to
// type '() => string'.
//   Type 'number' is not assignable to type 'string'. (2322)
const obj1: Object = { toString() { return 123 } };

objectでは、TypeScriptは警告を発しません(objectはプロパティを指定せず、競合が発生する可能性がないため)。

const obj2: object = { toString() { return 123 } };

15.4 オブジェクト型リテラルとインターフェース

TypeScriptには、非常に似たオブジェクト型を定義する2つの方法があります。

// Object type literal
type ObjType1 = {
  a: boolean,
  b: number;
  c: string,
};

// Interface
interface ObjType2 {
  a: boolean,
  b: number;
  c: string,
}

区切り文字としてセミコロンまたはカンマを使用できます。末尾の区切り文字は許可されており、オプションです。

15.4.1 オブジェクト型リテラルとインターフェースの違い

このセクションでは、オブジェクト型リテラルとインターフェースの最も重要な違いを見ていきます。

15.4.1.1 インライン化

オブジェクト型リテラルはインライン化できますが、インターフェースはできません。

// Inlined object type literal:
function f1(x: {prop: number}) {}

// Referenced interface:
function f2(x: ObjectInterface) {} 
interface ObjectInterface {
  prop: number;
}
15.4.1.2 重複する名前

重複する名前を持つ型エイリアスは違法です。

// @ts-expect-error: Duplicate identifier 'PersonAlias'. (2300)
type PersonAlias = {first: string};
// @ts-expect-error: Duplicate identifier 'PersonAlias'. (2300)
type PersonAlias = {last: string};

逆に、重複する名前を持つインターフェースはマージされます。

interface PersonInterface {
  first: string;
}
interface PersonInterface {
  last: string;
}
const jane: PersonInterface = {
  first: 'Jane',
  last: 'Doe',
};
15.4.1.3 マップ型

マップ型(A行)の場合、オブジェクト型リテラルを使用する必要があります。

interface Point {
  x: number;
  y: number;
}

type PointCopy1 = {
  [Key in keyof Point]: Point[Key]; // (A)
};

// Syntax error:
// interface PointCopy2 {
//   [Key in keyof Point]: Point[Key];
// };

  マップ型に関する詳細情報

マップ型は、本書の現在の範囲を超えています。詳細については、TypeScript Handbookを参照してください。

15.4.1.4 多態性this

多態性this型は、インターフェースでのみ使用できます。

interface AddsStrings {
  add(str: string): this;
};

class StringBuilder implements AddsStrings {
  result = '';
  add(str: string) {
    this.result += str;
    return this;
  }
}

  このセクションの出典

  これからは、「インターフェース」は「インターフェースまたはオブジェクト型リテラル」を意味します(特に明記されていない限り)。

15.4.2 TypeScriptではインターフェースは構造的に機能する

インターフェースは構造的に機能します。一致するために実装する必要はありません。

interface Point {
  x: number;
  y: number;
}
const point: Point = {x: 1, y: 2}; // OK

このトピックの詳細については、[コンテンツは含まれていません]を参照してください。

15.4.3 インターフェースとオブジェクト型リテラルのメンバー

インターフェースとオブジェクト型リテラルの本体内の構造は、そのメンバーと呼ばれます。これらが最も一般的なメンバーです。

interface ExampleInterface {
  // Property signature
  myProperty: boolean;

  // Method signature
  myMethod(str: string): number;

  // Index signature
  [key: string]: any;

  // Call signature
  (num: number): string;

  // Construct signature
  new(str: string): ExampleInstance; 
}
interface ExampleInstance {}

これらのメンバーを詳しく見ていきましょう。

プロパティシグネチャは自明である必要があります。呼び出しシグネチャコンストラクトシグネチャについては、本書の後半で説明します。次に、メソッドシグネチャとインデックスシグネチャを詳しく見ていきます。

15.4.4 メソッドシグネチャ

TypeScriptの型システムに関する限り、メソッド定義と値が関数であるプロパティは同等です。

interface HasMethodDef {
  simpleMethod(flag: boolean): void;
}
interface HasFuncProp {
  simpleMethod: (flag: boolean) => void;
}

const objWithMethod: HasMethodDef = {
  simpleMethod(flag: boolean): void {},
};
const objWithMethod2: HasFuncProp = objWithMethod;

const objWithOrdinaryFunction: HasMethodDef = {
  simpleMethod: function (flag: boolean): void {},
};
const objWithOrdinaryFunction2: HasFuncProp = objWithOrdinaryFunction;

const objWithArrowFunction: HasMethodDef = {
  simpleMethod: (flag: boolean): void => {},
};
const objWithArrowFunction2: HasFuncProp = objWithArrowFunction;

私の推奨事項は、プロパティをどのように設定する必要があるかを最もよく表現する構文を使用することです。

15.4.5 インデックスシグネチャ:辞書としてのオブジェクト

これまで、固定キーを持つレコードとしてのオブジェクトにのみインターフェースを使用してきました。オブジェクトを辞書として使用することを示すにはどうすればよいでしょうか?たとえば、次のコードフラグメントでTranslationDictは何であるべきでしょうか?

function translate(dict: TranslationDict, english: string): string {
  return dict[english];
}

TranslationDictが文字列キーを文字列値にマッピングするオブジェクト用であることを示すために、インデックスシグネチャ(A行)を使用します。

interface TranslationDict {
  [key:string]: string; // (A)
}
const dict = {
  'yes': 'sí',
  'no': 'no',
  'maybe': 'tal vez',
};
assert.equal(
  translate(dict, 'maybe'),
  'tal vez');
15.4.5.1 インデックスシグネチャキーの型付け

インデックスシグネチャキーは、stringまたはnumberのいずれかである必要があります。

15.4.5.2 文字列キーと数値キー

プレーンなJavaScriptと同様に、TypeScriptの数値プロパティキーは文字列プロパティキーのサブセットです(「JavaScript for impatient programmers」を参照)。したがって、文字列インデックスシグネチャと数値インデックスシグネチャの両方がある場合、前者のプロパティ型は後者のスーパータイプである必要があります。次の例は、ObjectRegExpのスーパータイプであるため機能します。

interface StringAndNumberKeys {
  [key: string]: Object;
  [key: number]: RegExp;
}

// %inferred-type: (x: StringAndNumberKeys) =>
// { str: Object; num: RegExp; }
function f(x: StringAndNumberKeys) {
  return { str: x['abc'], num: x[123] };
}
15.4.5.3 インデックスシグネチャとプロパティシグネチャおよびメソッドシグネチャ

インターフェースにインデックスシグネチャとプロパティまたはメソッドシグネチャの両方がある場合、インデックスプロパティ値の型も、プロパティ値またはメソッドの型のスーパータイプである必要があります。

interface I1 {
  [key: string]: boolean;

  // @ts-expect-error: Property 'myProp' of type 'number' is not assignable
  // to string index type 'boolean'. (2411)
  myProp: number;
  
  // @ts-expect-error: Property 'myMethod' of type '() => string' is not
  // assignable to string index type 'boolean'. (2411)
  myMethod(): string;
}

対照的に、次の2つのインターフェースではエラーは発生しません。

interface I2 {
  [key: string]: number;
  myProp: number;
}

interface I3 {
  [key: string]: () => string;
  myMethod(): string;
}

15.4.6 インターフェースはObjectのインスタンスを記述する

すべてのインターフェースは、Objectのインスタンスであり、Object.prototypeのプロパティを継承するオブジェクトを記述します。

次の例では、型{}のパラメーターxは、戻り値の型Objectと互換性があります。

function f1(x: {}): Object {
  return x;
}

同様に、{}にはメソッド.toString()があります。

function f2(x: {}): { toString(): string } {
  return x;
}

15.4.7 過剰プロパティチェック:余分なプロパティが許可されるのはいつか?

例として、次のインターフェースを検討してください。

interface Point {
  x: number;
  y: number;
}

このインターフェースを解釈できる方法は(特に)2つあります。

TypeScriptは両方の解釈を使用します。それがどのように機能するかを探るために、次の関数を使用します。

function computeDistance(point: Point) { /*...*/ }

デフォルトでは、過剰なプロパティ.zは許可されています。

const obj = { x: 1, y: 2, z: 3 };
computeDistance(obj); // OK

ただし、オブジェクトリテラルを直接使用すると、過剰なプロパティは禁止されます。

// @ts-expect-error: Argument of type '{ x: number; y: number; z: number; }'
// is not assignable to parameter of type 'Point'.
//   Object literal may only specify known properties, and 'z' does not
//   exist in type 'Point'. (2345)
computeDistance({ x: 1, y: 2, z: 3 }); // error

computeDistance({x: 1, y: 2}); // OK
15.4.7.1 オブジェクトリテラルで過剰なプロパティが禁止されるのはなぜですか?

オブジェクトリテラルに対するより厳格な規則はなぜでしょうか?それらはプロパティキーのスペルミスに対する保護を提供します。次のインターフェースを使用して、それが何を意味するかを示します。

interface Person {
  first: string;
  middle?: string;
  last: string;
}
function computeFullName(person: Person) { /*...*/ }

プロパティ.middleはオプションであり、省略できます(オプションのプロパティについては、この章の後半で説明します)。TypeScriptにとって、その名前のタイプミスは、それを省略し、過剰なプロパティを提供しているように見えます。ただし、この場合は過剰なプロパティが許可されないため、タイプミスを検出できます。

// @ts-expect-error: Argument of type '{ first: string; mdidle: string;
// last: string; }' is not assignable to parameter of type 'Person'.
//   Object literal may only specify known properties, but 'mdidle'
//   does not exist in type 'Person'. Did you mean to write 'middle'?
computeFullName({first: 'Jane', mdidle: 'Cecily', last: 'Doe'});
15.4.7.2 オブジェクトがどこかから来た場合に過剰なプロパティが許可されるのはなぜですか?

考え方としては、オブジェクトがどこか別の場所から来たものであれば、それはすでに精査済みで、タイプミスはないと想定できるということです。そうなれば、それほど注意深くなくても大丈夫です。

もしタイプミスが問題にならないのであれば、私たちの目標は柔軟性を最大化することであるべきです。次の関数を考えてみましょう。

interface HasYear {
  year: number;
}

function getAge(obj: HasYear) {
  const yearNow = new Date().getFullYear();
  return yearNow - obj.year;
}

getAge()に渡されるほとんどの値に対して余分なプロパティを許可しないと、この関数の有用性は非常に限られたものになるでしょう。

15.4.7.3 空のインターフェースは余分なプロパティを許可する

インターフェースが空の場合(またはオブジェクト型リテラル{}が使用されている場合)、余分なプロパティは常に許可されます。

interface Empty { }
interface OneProp {
  myProp: number;
}

// @ts-expect-error: Type '{ myProp: number; anotherProp: number; }' is not
// assignable to type 'OneProp'.
//   Object literal may only specify known properties, and
//   'anotherProp' does not exist in type 'OneProp'. (2322)
const a: OneProp = { myProp: 1, anotherProp: 2 };
const b: Empty = {myProp: 1, anotherProp: 2}; // OK
15.4.7.4 プロパティのないオブジェクトのみを一致させる

オブジェクトにプロパティがないことを強制したい場合は、次のトリックを使用できます(クレジット:Geoff Goodman

interface WithoutProperties {
  [key: string]: never;
}

// @ts-expect-error: Type 'number' is not assignable to type 'never'. (2322)
const a: WithoutProperties = { prop: 1 };
const b: WithoutProperties = {}; // OK
15.4.7.5 オブジェクトリテラルで余分なプロパティを許可する

オブジェクトリテラルで余分なプロパティを許可したい場合はどうすればよいでしょうか?例として、インターフェースPointと関数computeDistance1()を考えてみましょう。

interface Point {
  x: number;
  y: number;
}

function computeDistance1(point: Point) { /*...*/ }

// @ts-expect-error: Argument of type '{ x: number; y: number; z: number; }'
// is not assignable to parameter of type 'Point'.
//   Object literal may only specify known properties, and 'z' does not
//   exist in type 'Point'. (2345)
computeDistance1({ x: 1, y: 2, z: 3 });

1つのオプションは、オブジェクトリテラルを中間変数に割り当てることです。

const obj = { x: 1, y: 2, z: 3 };
computeDistance1(obj);

2つ目のオプションは、型アサーションを使用することです。

computeDistance1({ x: 1, y: 2, z: 3 } as Point); // OK

3つ目のオプションは、型パラメータを使用するようにcomputeDistance1()を書き直すことです。

function computeDistance2<P extends Point>(point: P) { /*...*/ }
computeDistance2({ x: 1, y: 2, z: 3 }); // OK

4つ目のオプションは、余分なプロパティを許可するようにインターフェースPointを拡張することです。

interface PointEtc extends Point {
  [key: string]: any;
}
function computeDistance3(point: PointEtc) { /*...*/ }

computeDistance3({ x: 1, y: 2, z: 3 }); // OK

TypeScriptが余分なプロパティを許可しないことが問題になる2つの例を続けます。

15.4.7.5.1 余分なプロパティを許可する:例Incrementor

この例では、Incrementorを実装したいのですが、TypeScriptは余分なプロパティ.counterを許可しません。

interface Incrementor {
  inc(): void
}
function createIncrementor(start = 0): Incrementor {
  return {
    // @ts-expect-error: Type '{ counter: number; inc(): void; }' is not
    // assignable to type 'Incrementor'.
    //   Object literal may only specify known properties, and
    //   'counter' does not exist in type 'Incrementor'. (2322)
    counter: start,
    inc() {
      // @ts-expect-error: Property 'counter' does not exist on type
      // 'Incrementor'. (2339)
      this.counter++;
    },
  };
}

残念ながら、型アサーションを使用しても、まだ1つの型エラーがあります。

function createIncrementor2(start = 0): Incrementor {
  return {
    counter: start,
    inc() {
      // @ts-expect-error: Property 'counter' does not exist on type
      // 'Incrementor'. (2339)
      this.counter++;
    },
  } as Incrementor;
}

インターフェースIncrementorにインデックスシグネチャを追加することができます。または、特にそれが不可能な場合は、中間変数を導入できます。

function createIncrementor3(start = 0): Incrementor {
  const incrementor = {
    counter: start,
    inc() {
      this.counter++;
    },
  };
  return incrementor;
}
15.4.7.5.2 余分なプロパティを許可する:例.dateStr

次の比較関数は、プロパティ.dateStrを持つオブジェクトをソートするために使用できます。

function compareDateStrings(
  a: {dateStr: string}, b: {dateStr: string}) {
    if (a.dateStr < b.dateStr) {
      return +1;
    } else if (a.dateStr > b.dateStr) {
      return -1;
    } else {
      return 0;
    }
  }

たとえば、単体テストでは、オブジェクトリテラルでこの関数を直接呼び出したい場合があります。TypeScriptはこれを許可せず、いずれかの回避策を使用する必要があります。

15.5 型推論

これらは、さまざまな手段で作成されたオブジェクトに対してTypeScriptが推論する型です。

// %inferred-type: Object
const obj1 = new Object();

// %inferred-type: any
const obj2 = Object.create(null);

// %inferred-type: {}
const obj3 = {};

// %inferred-type: { prop: number; }
const obj4 = {prop: 123};

// %inferred-type: object
const obj5 = Reflect.getPrototypeOf({});

原則として、Object.create()の戻り値の型はobjectになる可能性があります。ただし、anyを使用すると、結果のプロパティを追加および変更できます。

15.6 インターフェースのその他の機能

15.6.1 オプションのプロパティ

プロパティ名の後に疑問符(?)を付けると、そのプロパティはオプションになります。同じ構文を使用して、関数、メソッド、コンストラクターのパラメータをオプションとしてマークします。次の例では、プロパティ.middleはオプションです。

interface Name {
  first: string;
  middle?: string;
  last: string;
}

したがって、そのプロパティを省略しても問題ありません(行A)。

const john: Name = {first: 'Doe', last: 'Doe'}; // (A)
const jane: Name = {first: 'Jane', middle: 'Cecily', last: 'Doe'};
15.6.1.1 オプション vs. undefined|string

.prop1.prop2の違いは何でしょうか?

interface Interf {
  prop1?: string;
  prop2: undefined | string; 
}

オプションのプロパティは、undefined|stringができることすべてを実行できます。前者には値undefinedを使用することもできます。

const obj1: Interf = { prop1: undefined, prop2: undefined };

ただし、省略できるのは.prop1だけです。

const obj2: Interf = { prop2: undefined };

// @ts-expect-error: Property 'prop2' is missing in type '{}' but required
// in type 'Interf'. (2741)
const obj3: Interf = { };

undefined|stringnull|stringなどの型は、省略を明示的にしたい場合に役立ちます。このような明示的に省略されたプロパティを見ると、それらは存在しているがオフにされていることを知ることができます。

15.6.2 読み取り専用プロパティ

次の例では、プロパティ.propは読み取り専用です。

interface MyInterface {
  readonly prop: number;
}

その結果、読み取ることはできますが、変更することはできません。

const obj: MyInterface = {
  prop: 1,
};

console.log(obj.prop); // OK

// @ts-expect-error: Cannot assign to 'prop' because it is a read-only
// property. (2540)
obj.prop = 2;

15.7 JavaScriptのプロトタイプチェーンとTypeScriptの型

TypeScriptは、自身のプロパティと継承されたプロパティを区別しません。それらはすべて単にプロパティであると見なされます。

interface MyInterface {
  toString(): string; // inherited property
  prop: number; // own property
}
const obj: MyInterface = { // OK
  prop: 123,
};

objObject.prototypeから.toString()を継承します。

このアプローチの欠点は、JavaScriptの一部の現象をTypeScriptの型システムで記述できないことです。良い点は、型システムがよりシンプルになることです。

15.8 この章の出典