TypeScriptと構造的型付け
プログラミング言語にとって、型システムは大事なトピックです。型システムとは、プログラム内のさまざまな値や変数に「型」を割り当てる決まりを指します。この決まりによってデータの性質や扱い方が決まります。特に、どのように型と型を区別するのか、逆に、どのように型同士が互換性ありと判断するかは、言語の使いやすさや安全性に直結するテーマです。
考えてみましょう。string型とboolean型は同じものと見なせるでしょうか?これらは明らかに異なるデータ型であり、たとえばboolean型の変数に文字列を代入することは、型の安全性を守る上で望ましくありません。このような型の区別は、プログラムを正しく動作させるために欠かせません。
さらに、型の「互換性」も重要な概念です。たとえば、次のふたつのクラスを考えます。
tsclassPerson {walk () {}}classDog {walk () {}}
tsclassPerson {walk () {}}classDog {walk () {}}
これらのクラスは、walkメソッドを持つ点で似ています。このようなとき、Person型とDog型は「互換性がある」とみなすことができるでしょうか。それとも、まったく異なる型として扱うべきでしょうか。
こうした問題を扱うために、プログラミング言語はさまざまな型システムを採用しています。どのように型を区別すべきか、また、どのように型同士の互換性を判断するべきか、このような観点から型システムの仕様を考える必要があります。TypeScriptでは、「構造的型付け」という型システムが採用されています。構造的型付けがどのように型を区別し、逆にどのように型同士に互換性があると判断するのか、こうした言語仕様を知ることは、よいコードを書くために役立ちます。
型の区別に関する2つのアプローチ
プログラミング言語における型の区別や互換性の判定には、主に次の2つのアプローチが存在します。
- 名前的型付け
- 構造的型付け
ここからは、TypeScriptだけでなく他の言語も含めて、それぞれのアプローチについて見ていきましょう。
名前的型付け
名前的型付け(nominal typing)は、型の名前に基づいて型の区別を行う方法です。このアプローチでは、型同士が同一かどうかを判断する際に、その型の名前が重要な役割を果たします。たとえば、string型とnumber型は名前が異なるため、異なる型として扱います。同様に、型が同じ名前を持つ場合(例:stringとstring)は、同じ型と判断します。このアプローチでは、Person型とDog型は名前が異なるため、異なる型として扱い、互換性もなしと判断します。
名前的型付けを採用している言語の例としては、Java、PHP、C#、Swiftなどが挙げられます。これらの言語では、型の互換性は型の名前によって制御されます。たとえばJavaでは、次のようにPersonインスタンスをDog型の変数に代入しようとすると、型の不一致がコンパイルエラーとして報告されます。
javaclass Person {}class Dog {}class Main {public static void main(String[] args) {Person person = new Person();Dog dog = person; // コンパイルエラー: 不適合な型}}
javaclass Person {}class Dog {}class Main {public static void main(String[] args) {Person person = new Person();Dog dog = person; // コンパイルエラー: 不適合な型}}
この例では、Person型とDog型は名前が異なるため、Javaの型システムはこれらを異なる型として扱い、互換性がないと判断します。このように、名前的型付けでは型の名前が型の同一性および互換性を判断するための基準となります。
構造的型付け
構造的型付け(structural typing)は、型の名前ではなく、その「構造」に着目して型の区別や互換性を判定するアプローチです。この方法では、型が持つプロパティやメソッドの構造が同一であれば、異なる名前を持つ型同士でも互換性があると見なします。TypeScriptはこの構造的型付けを型システムとして採用しています。
構造的型付けの考え方を、PersonクラスとDogクラスの例で具体的に見てみましょう。
tsclassPerson {walk () {}}classDog {walk () {}}
tsclassPerson {walk () {}}classDog {walk () {}}
これらのクラスは、名前は異なりますが、構造が同じです。両クラスともwalkメソッドをひとつ持っています。このメソッドは引数を取らず、戻り値も持ちません。構造的型付けの観点からは、この共通の構造によりPersonとDogは互換性があると判断されます。
TypeScriptのコード例を見てみましょう。
tsconstperson = newPerson ();constdog :Dog =person ; // コンパイルエラーにならない
tsconstperson = newPerson ();constdog :Dog =person ; // コンパイルエラーにならない
このコードでは、PersonインスタンスをDog型の変数に代入していますが、コンパイルエラーになりません。これは、PersonとDogが構造的に互換性があるためです。
一方で、構造が異なる場合は互換性が認められません。
tsclassPerson {speak () {}}classDog {bark () {}}constperson = newPerson ();constProperty 'bark' is missing in type 'Person' but required in type 'Dog'.2741Property 'bark' is missing in type 'Person' but required in type 'Dog'.: dog Dog =person ; // コンパイルエラーになる
tsclassPerson {speak () {}}classDog {bark () {}}constperson = newPerson ();constProperty 'bark' is missing in type 'Person' but required in type 'Dog'.2741Property 'bark' is missing in type 'Person' but required in type 'Dog'.: dog Dog =person ; // コンパイルエラーになる
この場合、PersonとDogは異なるメソッドを持っているため、構造的に互換性がないと見なされ、代入しようとするとコンパイルエラーが発生します。
構造的型付けを採用している他の言語には、Go言語があります。このように構造的型付けは、型の名前よりもその「構造」に重点を置いた型システムを提供し、柔軟かつ直感的なプログラミングを可能にします。
次の表は、名前的型付けと構造的型付けの特徴をまとめたものです。
| 名前的型付け | 構造的型付け | |
|---|---|---|
| 型の区別基準 | 型の名前 | 型の構造 |
| 互換性の判断 | 名前が同じであれば互換性あり | 構造が同じであれば互換性あり |
| 主な採用言語 | Java, C#, Swift, PHPなど | TypeScript, Goなど |
部分型
多くのプログラミング言語では、型と型の関係性を階層関係で捉えることができます。階層構造において、頂点に位置するのはもっとも抽象的な型です。階層を下に進むほど具体的な型に分化していきます。階層構造の上位に位置する型を基本型(supertype)と言います。下層の型と比べると、基本型は抽象的な型です。階層構造の下位に位置する型を部分型(subtype)と呼びます。部分型は、基本型が持つすべての性質や振る舞い(メソッドやプロパティ)を持ちつつ、加えて新たな性質や振る舞いも持つ型です。
たとえば、図形と面積に関する型を考えたとき、図形(Shape)という基本型の下に、円(Circle)や長方形(Rectangle)という部分型が定義できます。Shapeは下位の型に比べて抽象的な型で、面積を求められる能力(areaメソッド)を持っています。一方で、Circleはより具体的な型で、Shapeの能力を引き継ぎつつ、半径(radius)という新たな属性を持っています。同様に、RectangleもShapeの能力を引き継ぎつつ、幅(width)と高さ(height)という新たな属性を持っています。
部分型は基本型と互換性があります。基本型の変数に部分型の値を代入することが可能です。たとえば、CircleとRectangleは異なる型ですが、同じShapeとして扱うことができます。より抽象的な階層レベルで扱えると利便性が高まります。たとえば、異なる図形同士の面積を合計するケースです。Shape型の変数にCircleやRectangleの値を代入して、それらの合計面積を求めることができます。
tsfunctiontotalArea (shape1 :Shape ,shape2 :Shape ): number {returnshape1 .area () +shape2 .area ();}constcircle = newCircle ({radius : 10 });constrectangle = newRectangle ({width : 10,height : 20 });totalArea (circle ,rectangle ); // CircleとRectangleをShapeとして扱える
tsfunctiontotalArea (shape1 :Shape ,shape2 :Shape ): number {returnshape1 .area () +shape2 .area ();}constcircle = newCircle ({radius : 10 });constrectangle = newRectangle ({width : 10,height : 20 });totalArea (circle ,rectangle ); // CircleとRectangleをShapeとして扱える
ある型とある型が、基本型と部分型の関係になるかどうかを判断する基準は、名前的型付けと構造的型付けでも異なります。たとえば、CircleがShapeの部分型かどうかは、名前的型付けと構造的型付けで判断基準が異なるということです。それぞれどのような判断基準があるのか、次の節で見ていきましょう。
名前的部分型
名前的型付けを採用しているプログラミング言語では、型の階層関係を定義する際に、型の名前とその関係性に重点を置きます。このアプローチでは、クラスやインターフェースの継承を通じて、型間の親子関係(基本型と部分型の関係)が形成されます。名前的型付けのアプローチで扱われる部分型のことを名前的部分型(nominal subtype)と呼びます。
たとえば、Javaではextendsキーワードを使用して、基本型と部分型の関係性を宣言します。この宣言により、特定のクラスが別のクラスの部分型であることをJavaコンパイラに知らせます。
javaclass Shape {}class Circle extends Shape {}
javaclass Shape {}class Circle extends Shape {}
このコード例では、CircleクラスがShapeクラスを継承しています。この継承により、CircleはShapeの部分型となります。この階層関係により、Shape型の変数にCircle型のインスタンスを代入することが可能になります。この代入は、CircleがShapeの部分型であるために、型の互換性が保証されているからです。
javaShape shape = new Circle();
javaShape shape = new Circle();
一方で、CircleとShape間にextendsキーワードによる継承関係が宣言されていない場合、両者の間に階層関係は存在しません。
javaclass Shape {}class Circle {}
javaclass Shape {}class Circle {}
この状況では、Shape型の変数にCircle型のインスタンスを代入しようとすると、型不一致のエラーが発生します。このエラーは、CircleとShapeが互換性のない独立した型であるとJavaコンパイラに判断されたために起きます。
javaShape shape = new Circle();// エラー: 不適合な型: CircleをShapeに変換できません
javaShape shape = new Circle();// エラー: 不適合な型: CircleをShapeに変換できません
構造的部分型
構造的型付けを採用しているTypeScriptでは、型間の階層関係もその構造に基づいて判断されます。このアプローチでは、型の名前ではなく、型が持つプロパティやメソッドの構造に着目して、基本型と部分型の関係性を判断します。このような部分型のことを構造的部分型(structural subtype)と呼びます。
次のTypeScriptのコード例を考えてみましょう。
tsclassShape {area (): number {return 0;}}classCircle {radius : number;constructor(radius : number) {this.radius =radius ;}area (): number {returnMath .PI * this.radius ** 2;}}
tsclassShape {area (): number {return 0;}}classCircle {radius : number;constructor(radius : number) {this.radius =radius ;}area (): number {returnMath .PI * this.radius ** 2;}}
この例では、CircleクラスはShapeクラスのareaメソッドを持っており、追加でradiusプロパティを定義しています。extendsキーワードを使用していないにもかかわらず、CircleはShapeの部分型として扱われます。これは、CircleがShapeの持つ構造(ここではareaメソッド)を含んでいるためです。その結果、Shape型の変数にCircle型のインスタンスを代入することが可能になります。
tsconstshape :Shape = newCircle (10);
tsconstshape :Shape = newCircle (10);
TypeScriptでもextendsキーワードを用いてクラス間の継承関係を宣言できます。しかし、これは部分型かどうかを判定するための基準には用いられません。これはJavaのような名前的部分型の言語とは異なる点です。extendsキーワードが持つ効果は、親クラスの機能を継承すること、そして、子クラスが親クラスのインターフェースを守ることです。
tsclassAnimal {walk () {}}classDog extendsAnimal {walk () {}}
tsclassAnimal {walk () {}}classDog extendsAnimal {walk () {}}
このコードでは、DogがAnimalを継承しています。この例では、DogのwalkメソッドがAnimalのwalkメソッドと同じ引数と戻り値を持っているため、DogはAnimalのインターフェースを守っているということになります。DogがAnimalのインターフェースを守っているため、Dogについてコンパイルエラーは発生しません。
一方で、子クラスが親クラスのインターフェースを守らない場合、TypeScriptはエラーを報告します。次のコード例では、DogクラスのwalkメソッドがAnimalクラスのそれと異なる引数を持っています。DogクラスはAnimalクラスのインターフェースを守っていないということです。この例では、walkメソッドに対して、その旨の警告がなされます。これがextendsキーワードの効果です。
tsclassAnimal {walk () {}}classDog extendsAnimal {Property 'walk' in type 'Dog' is not assignable to the same property in base type 'Animal'. Type '(speed: number) => void' is not assignable to type '() => void'. Target signature provides too few arguments. Expected 1 or more, but got 0.2416Property 'walk' in type 'Dog' is not assignable to the same property in base type 'Animal'. Type '(speed: number) => void' is not assignable to type '() => void'. Target signature provides too few arguments. Expected 1 or more, but got 0.( walk speed : number) {} // コンパイルエラーになる}
tsclassAnimal {walk () {}}classDog extendsAnimal {Property 'walk' in type 'Dog' is not assignable to the same property in base type 'Animal'. Type '(speed: number) => void' is not assignable to type '() => void'. Target signature provides too few arguments. Expected 1 or more, but got 0.2416Property 'walk' in type 'Dog' is not assignable to the same property in base type 'Animal'. Type '(speed: number) => void' is not assignable to type '() => void'. Target signature provides too few arguments. Expected 1 or more, but got 0.: number) {} // コンパイルエラーになる walk (speed }
構造的型付けの採用理由
TypeScriptが構造的型付けを採用した背景には、JavaScriptの特性が深く関わっています。ここでは、なぜTypeScriptが構造的型付けを選んだのかについて考えてみましょう。
ダックタイピング
ダックタイピングは、オブジェクトの型よりもオブジェクトの持つメソッドやプロパティが何であるかによってオブジェクトを判断するプログラミングスタイルです。ダックタイピングの世界では、特定のインターフェースをimplementsキーワードを使うなどして明示的に実装する必要はありません。代わりに、オブジェクトが特定の規約にしたがっているか、たとえば、特定のメソッドを持っているかという基準で、そのオブジェクトの型を判断します。ダックタイピングでは、型を判断するために型の名前を使わないのが一般的です。ちなみに、ダックタイピングという用語は、「もし鳥がアヒルのように歩き、アヒルのように鳴くなら、それはアヒルだ」という言葉に由来しています。
ダックタイピングは、動的型付け言語によく見られます。JavaScriptも動的型付け言語であり、ダックタイピングとともに歩んできた歴史があります。TypeScriptはJavaScriptの延長線上にある言語です。そのため、ダックタイピングが行えるような型システムが求められました。構造的型付けは、ダックタイピングに適した型システムです。こうした背景もTypeScriptが構造的型付けを採用した理由のひとつと考えられます。
オブジェクトリテラル
JavaScriptの特徴のひとつにはオブジェクトリテラルがあります。オブジェクトリテラルは、クラスやインターフェースなどの型を定義することなく、その場でオブジェクトを生成する機能です。
tsconstcircle = {radius : 10,area () {returnMath .PI * this.radius ** 2;},};
tsconstcircle = {radius : 10,area () {returnMath .PI * this.radius ** 2;},};
上の例のように、circleオブジェクトには型の名前がありません。型に名前がない以上、名前的型付けのように型名を使って型を判断することができません。こうしたJavaScriptコードを扱えるようにするためにも、TypeScriptは構造的型付けを採用したと考えられます。
構造的型システムの利点
構造的型付けの柔軟性や便利さは注目に値するところがあります。ここでは、具体例を交えて構造的型付けの利点について見ていきましょう。
モックテストの簡略化
構造的型付けは、モックテストや依存性の注入を簡単に行えるようにします。特に、外部のAPIやサービスに依存するコンポーネントをテストする際に、その依存関係を模倣したモックオブジェクトを簡単に作成できます。名前的型付けでは、モック化したいオブジェクトをまずインターフェース化する必要があります。その上で、インターフェースを実装するモッククラスを用意します。構造的型付けでは、必要なメソッドやプロパティを持つオブジェクトリテラルを直接提供するだけで、テスト用のモックを簡単に用意できます。インターフェースの定義が省けるため、構造がシンプルになり、テストの準備も省力化されます。
次の例では、UserServiceクラスがUserApiに依存しています。この依存関係をテストするために、UserApiのメソッドgetUserを模倣したモックを作成し、UserServiceの動作をテストします。
tstypeUser = {id : number;name : string };classUserApi {asyncgetUser (id : number):Promise <User | undefined> {// 実装は割愛しますが、fetchなどを使って実際のAPIを呼び出す実装をイメージしてください。}}classUserService {privateapi :UserApi ;constructor(api :UserApi ) {this.api =api ;}asyncuserExists (id : number):Promise <boolean> {constuser = await this.api .getUser (id );returnuser !==undefined ;}}
tstypeUser = {id : number;name : string };classUserApi {asyncgetUser (id : number):Promise <User | undefined> {// 実装は割愛しますが、fetchなどを使って実際のAPIを呼び出す実装をイメージしてください。}}classUserService {privateapi :UserApi ;constructor(api :UserApi ) {this.api =api ;}asyncuserExists (id : number):Promise <boolean> {constuser = await this.api .getUser (id );returnuser !==undefined ;}}
テストケースでは、UserApiの構造を満たすオブジェクトを直接作成し、UserServiceのインスタンスに渡すだけで単体テストを行えます。
tstest ("ユーザーがいるときはtrueを返す", async () => {// モックオブジェクトを直接作成constapi :UserApi = {asyncgetUser (id ) {return {id ,name : "Alice" };},};// モックオブジェクトをUserServiceに渡してテストconstservice = newUserService (api );constresult = awaitservice .userExists (123);expect (result ).toBe (true);});
tstest ("ユーザーがいるときはtrueを返す", async () => {// モックオブジェクトを直接作成constapi :UserApi = {asyncgetUser (id ) {return {id ,name : "Alice" };},};// モックオブジェクトをUserServiceに渡してテストconstservice = newUserService (api );constresult = awaitservice .userExists (123);expect (result ).toBe (true);});
このように、構造的型付けを利用することで、テスト対象の依存物の注入がより簡単になります。
構造的型付けの注意点
構造的型付けは、その柔軟性により多くの利点を提供しますが、注意が必要な点もあります。特に、意図せず型に互換性が生じる可能性があることがそのひとつです。
構造的型付けシステムでは、型の互換性はその構造に基づいて判断されます。このため、異なる目的や意味合いを持つ型が、偶然同じ構造を持っている場合に、意図せずに互換性があると判断されることがあります。
tsclass UserId {id: string;}class ProductId {id: string;}const userId: UserId = new UserId();const productId: ProductId = userId; // 代入できるが、意図した設計ではない
tsclass UserId {id: string;}class ProductId {id: string;}const userId: UserId = new UserId();const productId: ProductId = userId; // 代入できるが、意図した設計ではない
この例では、UserIdクラスとProductIdクラスがあり、どちらもidプロパティを持つ同じ構造になっています。TypeScriptはこれらの型を互換性があるとみなします。なぜなら構造が同じだからです。しかし、データモデルやドメインモデルの観点からは、ユーザーのIDと商品のIDはまったく異なる概念であり、型システムで区別したい場合がほとんどです。値オブジェクト(value object)のようなデザインパターンをTypeScriptで用いる場合は、このような問題に注意が必要です。型としてどうしても区別したい場合は、後述の「名前的型付けを実現する方法」で紹介するテクニックを検討してみてください。
名前的型付けを実現する方法
TypeScriptは基本的に構造的型付けを採用していますが、名前的型付けになる場合や、名前的型付けを模倣するデザインパターンもあります。これは、TypeScriptの型システムの柔軟性を利用したテクニックであり、プログラムの正当性を強化するために用いられることがあります。
privateメンバーを持つクラス
TypeScriptでは、privateメンバーを持つクラスは、他のクラスと区別されます。これは、privateメンバーがそのクラス固有のものであるため、異なるクラスのインスタンス同士は、構造が同じであっても互換性がないと見なされるからです。
tsclassUserId {privateid : string;constructor(id : string) {this.id =id ;}getId (): string {return this.id ;}}classProductId {privateid : string;constructor(id : string) {this.id =id ;}getId (): string {return this.id ;}}constuserId :UserId = newUserId ("1");constType 'UserId' is not assignable to type 'ProductId'. Types have separate declarations of a private property 'id'.2322Type 'UserId' is not assignable to type 'ProductId'. Types have separate declarations of a private property 'id'.: productId ProductId =userId ; // 代入エラー
tsclassUserId {privateid : string;constructor(id : string) {this.id =id ;}getId (): string {return this.id ;}}classProductId {privateid : string;constructor(id : string) {this.id =id ;}getId (): string {return this.id ;}}constuserId :UserId = newUserId ("1");constType 'UserId' is not assignable to type 'ProductId'. Types have separate declarations of a private property 'id'.2322Type 'UserId' is not assignable to type 'ProductId'. Types have separate declarations of a private property 'id'.: productId ProductId =userId ; // 代入エラー
この例では、UserIdとProductIdは、内部的にprivateメンバーidを持っていますが、互いに別の型として扱われます。つまり、名前的型付けのように、名前によって型が区別されるようになります。
📄️ 公称型クラス
TypeScriptでは、クラスに1つでも非パブリックなプロパティがあると、そのクラスだけ構造的部分型ではなく公称型(nominal typing)になります。
ブランド型
ブランド型(または幽霊型(phantom type)、opaque type)は、型を区別するためのプロパティを型に持たせることで、その型を明確に区別するデザインパターンです。これは、型にメタデータのようなタグをつけることで、構造的には同じであっても型と型を区別できるようにします。
tsinterfaceUserId {__brand : "UserId";id : number;}interfaceProductId {__brand : "ProductId";id : number;}
tsinterfaceUserId {__brand : "UserId";id : number;}interfaceProductId {__brand : "ProductId";id : number;}
この例では、__brandプロパティを使ってUserId型とProductId型を区別しています。これにより、両者が構造的に同じidプロパティを持っていても、型システム上では異なる型として扱われます。これは、構造的型付けの特徴をうまく利用したテクニックです。構造的型付けでは、構造が異なる場合は互換性がないと見なすわけですから、__brandのような構造を意図的に違えるものを使うことで、型を区別することができるのです。
ブランド型で用いられる__brandプロパティは、型を区別するためのものであり、実行時のデータとして持たせる必要はありません。このため、__brandプロパティは、実際のデータには含まれないようにすることが一般的です。これを達成するために、__brandプロパティはasキーワードを使って型アサーションを行う手法がよく使われます。
tsconstuserId = {id : 1 } asUserId ;
tsconstuserId = {id : 1 } asUserId ;
ブランド型を用いて作られた値は、あたかも名前的型付けのように、名前によって型が区別されるようになります。
tsconstuserId = {id : 1 } asUserId ;constType 'UserId' is not assignable to type 'ProductId'. Types of property '__brand' are incompatible. Type '"UserId"' is not assignable to type '"ProductId"'.2322Type 'UserId' is not assignable to type 'ProductId'. Types of property '__brand' are incompatible. Type '"UserId"' is not assignable to type '"ProductId"'.: productId ProductId =userId ; // 代入不可
tsconstuserId = {id : 1 } asUserId ;constType 'UserId' is not assignable to type 'ProductId'. Types of property '__brand' are incompatible. Type '"UserId"' is not assignable to type '"ProductId"'.2322Type 'UserId' is not assignable to type 'ProductId'. Types of property '__brand' are incompatible. Type '"UserId"' is not assignable to type '"ProductId"'.: productId ProductId =userId ; // 代入不可
これらのテクニックを利用することで、構造的型付けのTypeScriptでも、名前に依存した型の区別が行えます。名前による型の区別が必要な場合は、これらのテクニックを検討してみるとよいでしょう。
まとめ
| 名前的型付け | 構造的型付け | |
|---|---|---|
| 型の区別基準 | 型の名前 | 型の構造(プロパティやメソッドなど) |
| 互換性の判断基準 | 名前が同じであれば互換性あり | 構造が同じであれば互換性あり |
| 基本型と部分型の明示性 | 明示的(extendsなどのキーワードによる継承を使用) | 暗黙的(型の構造が一致する場合、自動的に部分型とみなされる) |
| 主な採用言語 | Java, C#, Swift, PHP | TypeScript, Go |
| 利点 | - 型の名前に基づく明確な区別が可能 - 明示的な型の階層関係により、設計の意図を明確にできる | - ダックタイピングにより、アドホックにオブジェクトを作れる |
| 欠点 | - 型間の互換性が名前に依存し、柔軟性に欠ける場合がある | - 意図しない型間の互換性が生じる可能性がある - 型の区別が直感的でない場合がある |
構造的型付けはTypeScriptの型システムの核心を成す概念であり、型の互換性をその構造に基づいて判断します。これは、型の名前ではなく、型が持つプロパティやメソッドの構造を見て型の同一性や互換性を判断するというものです。このアプローチは、JavaScriptの動的で柔軟な特性に対応するために採用されており、ダックタイピングやオブジェクトリテラルといったJavaScriptの特徴と良く合います。
構造的型付けは柔軟性が高く、モックテストなどを容易にしますが、意図せず互換性が生じる可能性もあるという注意点があります。しかし、privateメンバーやブランド型といったテクニックを用いることで、構造的型付けのシステム内で名前的型付けの振る舞いを模倣し、型の明確な区別を実現することも可能です。
構造的型付けを理解し、適切に活用することで、より安全で保守しやすいコードを書くことができるでしょう。
学びをシェアする
・TypeScriptは構造的型付け
・構造的型付けは型名より構造を重視
・構造的型付けは型の構造で互換性判断
・privateやブランド型で名前的型付けを模倣できる
・構造的型付けは意図しない互換性に注意
『サバイバルTypeScript』より