ObjectとobjectObjectのインスタンスを記述するこの章では、TypeScriptでオブジェクトとプロパティがどのように静的に型付けされるかを探ります。
JavaScriptでは、オブジェクトは2つの役割を果たすことができます(常に少なくとも1つ、時には混在)。
レコードは、開発時に既知の固定量のプロパティを持ちます。各プロパティは異なる型を持つことができます。
辞書は、開発時に名前が不明な任意の数のプロパティを持ちます。すべてのプロパティキー(文字列やシンボル)は同じ型を持ち、プロパティ値も同様です。
まず、オブジェクトをレコードとして探ります。辞書としてのオブジェクトには、この章の後半で簡単に触れます。
オブジェクトには2つの異なる一般的な型があります。
大文字の「O」を持つObjectは、クラスObjectのすべてのインスタンスの型です。
let obj1: Object;小文字の「o」を持つobjectは、すべての非プリミティブ値の型です。
let obj2: object;オブジェクトは、そのプロパティを通じて型付けすることもできます。
// Object type literal
let obj3: {prop: boolean};
// Interface
interface ObjectType {
prop: boolean;
}
let obj4: ObjectType;次のセクションでは、これらのオブジェクトの型付け方法を詳しく見ていきます。
ObjectとobjectObjectのインスタンスプレーンなJavaScriptでは、重要な区別があります。
一方、ほとんどのオブジェクトはObjectのインスタンスです。
> const obj1 = {};
> obj1 instanceof Object
trueこれはつまり
Object.prototypeがそれらのプロトタイプチェーンにある
> Object.prototype.isPrototypeOf(obj1)
trueそれらはそのプロパティを継承します。
> obj1.toString === Object.prototype.toString
true他方、プロトタイプチェーンにObject.prototypeを持たないオブジェクトを作成することもできます。たとえば、次のオブジェクトにはプロトタイプがまったくありません。
> const obj2 = Object.create(null);
> Object.getPrototypeOf(obj2)
nullobj2は、クラスObjectのインスタンスではないオブジェクトです。
> typeof obj2
'object'
> obj2 instanceof Object
falseObject(大文字の「O」):クラスObjectのインスタンス各クラスCは2つのエンティティを作成することを思い出してください。
C。C。同様に、TypeScriptには2つの組み込みインターフェースがあります。
インターフェースObjectは、Object.prototypeから継承されたプロパティを含む、Objectのインスタンスのプロパティを指定します。
インターフェースObjectConstructorは、クラスObjectのプロパティを指定します。
これらがインターフェースです。
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)観察
Objectという名前の変数(C行)と、Objectという名前の型(A行)の両方があります。Objectの直接のインスタンスは独自のプロパティを持たないため、Object.prototypeもObjectと一致します(B行)。object(小文字の「o」):非プリミティブ値TypeScriptでは、objectはすべての非プリミティブ値(プリミティブ値はundefined、null、ブール値、数値、bigint、文字列)の型です。この型では、値のプロパティにアクセスすることはできません。
Objectとobject:プリミティブ値興味深いことに、型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');Objectとobject:互換性のないプロパティ型型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 } };TypeScriptには、非常に似たオブジェクト型を定義する2つの方法があります。
// Object type literal
type ObjType1 = {
a: boolean,
b: number;
c: string,
};
// Interface
interface ObjType2 {
a: boolean,
b: number;
c: string,
}区切り文字としてセミコロンまたはカンマを使用できます。末尾の区切り文字は許可されており、オプションです。
このセクションでは、オブジェクト型リテラルとインターフェースの最も重要な違いを見ていきます。
オブジェクト型リテラルはインライン化できますが、インターフェースはできません。
// Inlined object type literal:
function f1(x: {prop: number}) {}
// Referenced interface:
function f2(x: ObjectInterface) {}
interface ObjectInterface {
prop: number;
}重複する名前を持つ型エイリアスは違法です。
// @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',
};マップ型(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を参照してください。
this型多態性this型は、インターフェースでのみ使用できます。
interface AddsStrings {
add(str: string): this;
};
class StringBuilder implements AddsStrings {
result = '';
add(str: string) {
this.result += str;
return this;
}
} これからは、「インターフェース」は「インターフェースまたはオブジェクト型リテラル」を意味します(特に明記されていない限り)。
インターフェースは構造的に機能します。一致するために実装する必要はありません。
interface Point {
x: number;
y: number;
}
const point: Point = {x: 1, y: 2}; // OKこのトピックの詳細については、[コンテンツは含まれていません]を参照してください。
インターフェースとオブジェクト型リテラルの本体内の構造は、そのメンバーと呼ばれます。これらが最も一般的なメンバーです。
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 {}これらのメンバーを詳しく見ていきましょう。
プロパティシグネチャはプロパティを定義します。
myProperty: boolean;メソッドシグネチャはメソッドを定義します。
myMethod(str: string): number;注:パラメーターの名前(この場合はstr)は、動作の仕組みを文書化するのに役立ちますが、他の目的はありません。
インデックスシグネチャは、辞書として使用される配列またはオブジェクトを記述するために必要です。
[key: string]: any;注:名前keyはドキュメントの目的でのみ存在します。
呼び出しシグネチャにより、インターフェースで関数を記述できます。
(num: number): string;コンストラクトシグネチャにより、インターフェースでクラスとコンストラクタ関数を記述できます。
new(str: string): ExampleInstance; プロパティシグネチャは自明である必要があります。呼び出しシグネチャとコンストラクトシグネチャについては、本書の後半で説明します。次に、メソッドシグネチャとインデックスシグネチャを詳しく見ていきます。
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;私の推奨事項は、プロパティをどのように設定する必要があるかを最もよく表現する構文を使用することです。
これまで、固定キーを持つレコードとしてのオブジェクトにのみインターフェースを使用してきました。オブジェクトを辞書として使用することを示すにはどうすればよいでしょうか?たとえば、次のコードフラグメントで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');インデックスシグネチャキーは、stringまたはnumberのいずれかである必要があります。
anyは許可されていません。string|number)は許可されていません。ただし、インターフェースごとに複数のインデックスシグネチャを使用できます。プレーンなJavaScriptと同様に、TypeScriptの数値プロパティキーは文字列プロパティキーのサブセットです(「JavaScript for impatient programmers」を参照)。したがって、文字列インデックスシグネチャと数値インデックスシグネチャの両方がある場合、前者のプロパティ型は後者のスーパータイプである必要があります。次の例は、ObjectがRegExpのスーパータイプであるため機能します。
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] };
}インターフェースにインデックスシグネチャとプロパティまたはメソッドシグネチャの両方がある場合、インデックスプロパティ値の型も、プロパティ値またはメソッドの型のスーパータイプである必要があります。
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;
}Objectのインスタンスを記述するすべてのインターフェースは、Objectのインスタンスであり、Object.prototypeのプロパティを継承するオブジェクトを記述します。
次の例では、型{}のパラメーターxは、戻り値の型Objectと互換性があります。
function f1(x: {}): Object {
return x;
}同様に、{}にはメソッド.toString()があります。
function f2(x: {}): { toString(): string } {
return x;
}例として、次のインターフェースを検討してください。
interface Point {
x: number;
y: number;
}このインターフェースを解釈できる方法は(特に)2つあります。
.xと.yを正確に持つすべてのオブジェクトを記述できます。言い換えれば、これらのオブジェクトは過剰なプロパティ(必要なプロパティよりも多いプロパティ)を持ってはなりません。.xと.yを少なくとも持つすべてのオブジェクトを記述できます。言い換えれば、過剰なプロパティは許可されています。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オブジェクトリテラルに対するより厳格な規則はなぜでしょうか?それらはプロパティキーのスペルミスに対する保護を提供します。次のインターフェースを使用して、それが何を意味するかを示します。
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'});考え方としては、オブジェクトがどこか別の場所から来たものであれば、それはすでに精査済みで、タイプミスはないと想定できるということです。そうなれば、それほど注意深くなくても大丈夫です。
もしタイプミスが問題にならないのであれば、私たちの目標は柔軟性を最大化することであるべきです。次の関数を考えてみましょう。
interface HasYear {
year: number;
}
function getAge(obj: HasYear) {
const yearNow = new Date().getFullYear();
return yearNow - obj.year;
}getAge()に渡されるほとんどの値に対して余分なプロパティを許可しないと、この関数の有用性は非常に限られたものになるでしょう。
インターフェースが空の場合(またはオブジェクト型リテラル{}が使用されている場合)、余分なプロパティは常に許可されます。
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オブジェクトにプロパティがないことを強制したい場合は、次のトリックを使用できます(クレジット: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オブジェクトリテラルで余分なプロパティを許可したい場合はどうすればよいでしょうか?例として、インターフェース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); // OK3つ目のオプションは、型パラメータを使用するようにcomputeDistance1()を書き直すことです。
function computeDistance2<P extends Point>(point: P) { /*...*/ }
computeDistance2({ x: 1, y: 2, z: 3 }); // OK4つ目のオプションは、余分なプロパティを許可するようにインターフェースPointを拡張することです。
interface PointEtc extends Point {
[key: string]: any;
}
function computeDistance3(point: PointEtc) { /*...*/ }
computeDistance3({ x: 1, y: 2, z: 3 }); // OKTypeScriptが余分なプロパティを許可しないことが問題になる2つの例を続けます。
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;
}.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はこれを許可せず、いずれかの回避策を使用する必要があります。
これらは、さまざまな手段で作成されたオブジェクトに対して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を使用すると、結果のプロパティを追加および変更できます。
プロパティ名の後に疑問符(?)を付けると、そのプロパティはオプションになります。同じ構文を使用して、関数、メソッド、コンストラクターのパラメータをオプションとしてマークします。次の例では、プロパティ.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'};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|stringやnull|stringなどの型は、省略を明示的にしたい場合に役立ちます。このような明示的に省略されたプロパティを見ると、それらは存在しているがオフにされていることを知ることができます。
次の例では、プロパティ.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;TypeScriptは、自身のプロパティと継承されたプロパティを区別しません。それらはすべて単にプロパティであると見なされます。
interface MyInterface {
toString(): string; // inherited property
prop: number; // own property
}
const obj: MyInterface = { // OK
prop: 123,
};objはObject.prototypeから.toString()を継承します。
このアプローチの欠点は、JavaScriptの一部の現象をTypeScriptの型システムで記述できないことです。良い点は、型システムがよりシンプルになることです。