TypeScriptの型の代入性についてわかりやすくまとめてみた
はじめに
はじめまして!都内でエンジニアをやっているケンタッキーです。
1年ほど前にプログラミングTypeScriptを読んだのですが、内容的に難しい部分が多々あり、自分の整理のためにもTypeScriptの型の代入性についてわかりやすくまとめようと思いこの記事を書きました。
概要
- この記事では、TypeScriptの型の代入性についてTypeScriptの初学者でも理解しやすいように具体例を踏まえて解説しています。
- この記事を読むことで、「
型Aを型Bに割り当てることはできません。」というTypeScript頻出のエラーを理解し、自分で解決できるようになると思います。
この記事で伝えたいこと
- TypeScriptの型の代入性はサブタイプかどうかによって決まる。
- 余剰プロパティチェックはオブジェクトリテラルに対して実行される。
TypeScriptの型の扱い
TypeScriptの型の代入性について説明にする前に、JavaとTypeScriptの型の扱いの違いを比較しながら「TypeScriptの型の扱い」について見ていきます。
先に結論から言うと、
- Javaは「名前的型付け(明示的に名前を指定する必要がある)」で型を扱い、
- TypeScriptは「構造的型付け(構造的に同じ場合、同じ型とみなす)」で型を扱います。
具体例を用いて説明します。
TypeScriptの場合は、構造が同じかどうかで型を判断するので、以下のコードはエラーになりません。
type Person = {
name: string;
age: number;
}
type Employee = {
name: string;
age: number
salary: number
}
function greet(person: Person) {
console.log(`Hello, ${person.name}`)
}
const employee: Employee = {
name: "Alice",
age: 30,
salary: 50000
}
greet(employee) // ✅ EmployeeはPersonの構造を満たすため、代入できる
それに対して、Javaは名前によって型を判断するため以下のコードはエラーになります。
public class Person {
String name;
int age;
}
public class Employee {
String name;
int age;
int salary;
}
void greet(Person person) {
System.out.println("Hello, " + person.name);
}
Employee employee = new Employee();
// ❌ 「Employee cannot be converted to Person」といったエラーが発生
greet(employee);
このようにTypeScriptはJavaと違って型を内部の構造を見て判断しています。
そのため、ある型Aに構造が同じだが別の型Bを代入したりすることができます(型の代入性)
それでは、これからTypeScriptの非常に強力な型システムを支える「型の代入性」について解説していきます。
TypeScriptが構造的型付けを採用している理由
TypeScriptはJavaScriptのスーパーセットとなるプログラミング言語であるため、構造的型付けを採用している理由には、JavaScriptのダックタイピングとオブジェクトリテラルが深く関係しています。
-
ダックタイピング
- インタフェースを
implementsキーワードを使わず、メソッドの構造で判断すること - 「もし鳥がアヒルのように歩き、アヒルのように泣くなら、それはアヒルだ」という言葉が由来
- インタフェースを
-
オブジェクトリテラル
- クラスやインタフェースの型を定義することなくその場でオブジェクトを生成する機能オブジェクトリテラルの例
const circle = { radius: 10, area() { return Math.PI * this.radius ** 2; } }
- クラスやインタフェースの型を定義することなくその場でオブジェクトを生成する機能
JavaScriptは動的型付け言語であり、ダックタイピングを採用していた歴史があります。
また、オブジェクトリテラルによりcircleオブジェクトには型の名前がなく、型名から型を判断することができません。
-> 以上の理由により、TypeScriptは構造的型付けを採用したと考えられます
TypeScriptの型の代入性
TypeScriptの型の代入性とは、簡単に一言でまとめると「型Bは型Aのサブタイプ」ならば、「型Aを期待する箇所に型Bを割り当て可能」といえます。
サブタイプ = 以下の画像のように型Aに内包される型BというイメージでOKです。

TypeScriptの型の関係図を以下に示します。
上に位置するほど親のタイプ(スーパータイプ)となり、下に位置するほど、サブタイプとなります。

つまり、Unknown型やAny型はほぼ全ての型のスーパータイプに当たり、Never型は全ての型のサブタイプに該当します。
Never型の例を見てみましょう。
「型Bは型Aのサブタイプ」ならば、「型Aを期待する箇所に型Bを割り当て可能」といえます。
Never型は全ての型のサブタイプなので、どの型にも割り当てることができます。
逆にNever型のサブタイプは存在しないのでどの型も代入することができません。
// ❌ エラー: 型 '1' を型 'never' に割り当てることはできません。ts(2322)
const neverValue: never = 1
// ✅ エラーにならない
const numberValue: number = neverValue
Object型とArray型の関係にも注目してみましょう。
Array型はObject型のサブタイプなので、Object型を期待する箇所にArray型を代入可能です。
const arrayValue: Array<number> = [1, 2, 3]
// ✅ エラーにならない
const objectValue: Object = arrayValue
// ❌ エラー:逆はエラーになる
const arrayValue2: Array<number> = objectValue
図に記載はありませんが、プリミティブ型とリテラル型についても考えてみましょう。
サブタイプの内包イメージがあれば、どちらがどちらに代入可能か想像つきそうですよね?
リテラル型はプリミティブ型のサブタイプなので、プリミティブ型を期待する箇所には、リテラル型が代入可能となります。
(もちろんリテラル型を期待する箇所にプリミティブ型は代入不可能です)
// ✅ エラーにならない
const primitiveValue: number = 1
// ❌ エラー: 型 'number' を型 '1' に割り当てることはできません。ts(2322)
const literalValue: 1 = primitiveValue
ユニオン型についても触れておきます。
ユニオン型は、代入しようとしている型が期待するいずれかの型のサブタイプであれば、代入可能となります。
type Union = number | Array<number>
// ✅ 1(リテラル型)はnumber型のサブタイプなので、エラーにならない
const unionValue: Union = 1
Any型とUnknown型とNever型
TypeScriptの型の中でも少し特殊なAny型とUnknown型とNever型について解説します。
Any型
any型は、すべての型に代入可能な型であり、すべての型を代入可能な型という特徴を持つ、メタモンみたいな型です。any型を使ってしまうと、型安全性が一瞬で失われてしまうのでできるだけ使用は避けたいですね。
以下に例を示します。
// ✅ エラーにならない
const anyValue: any = 1
// ✅ エラーにならない
const notAnyValue: number = anyValue
Unknown型
unknown型は、すべての型のスーパータイプに位置する王様のような(アルセウスみたいな)型ですが、any型より安全な型として利用できます。
なぜかというと、すべての型のスーパータイプなので、どんな値でも代入可能ですが、unknown型を他の型に代入しようとすると(TypeScriptの仕様的に)エラーになり、型ガードが必要になるからです。
// ✅ エラーにならない
const unknownValue: unknown = 1
// ❌ エラー:型 'unknown' を型 'number' に割り当てることはできません。ts(2322)
const notUnknownValue: number = unknownValue
// ✅ 型ガードでエラーが解消される
if (typeof unknownValue === "number") {
const numberValue: number = unknownValue
}
この型安全性がany型よりunknown型が好まれる理由です。
Never型
再掲になりますが、never型はすべての型のサブタイプです。そのため、どんな型にも代入することができますが、決してnever型に他の型を代入することができません。
// ❌ 型 '1' を型 'never' に割り当てることはできません。
const neverValue: never = 1
// ✅ エラーにならない
const stringValue: string = neverValue
以上で特殊な型についての説明を終わります。
次は、鬼門であるオブジェクトのサブタイプについて解説していきます。
オブジェクトのサブタイプ
オブジェクトのサブタイプの定義は「オブジェクトAが、オブジェクトBの全ての必須プロパティのサブタイプを満たすプロパティを有している」ならば、「オブジェクトAはオブジェクトBのサブタイプとみなせるので、オブジェクトBを期待する箇所にオブジェクトAを代入可能」となります。
簡単な例で確認しましょう。
以下のコードはObj2がObjの全ての必須プロパティにおいて、サブタイプを満たすプロパティを保持しているので、Objを期待する箇所にObj2を代入することが可能です。
type Obj = {
name: string
age: number
}
type Obj2 = {
name: string
age: number
salary: number
}
const obj: Obj = { name: "John", age: 30}
const obj2: Obj2 = { name: "Ann", age: 20, salary: 4000}
const log = (o: Obj) => {
console.log(o)
}
// ✅ エラーにならない
log(obj)
// ✅ エラーにならない
log(obj2)
では、逆の場合はどうでしょうか?
ObjはObj2の全ての必須プロパティのサブタイプを満たすプロパティ保持していません。(salaryがない)
そのため、Obj2を期待する箇所にObjを代入するとエラーになります。
const log2 = (o: Obj2) => {
console.log(o)
}
// ❌ エラーになる:型 'Obj' の引数を型 'Obj2' のパラメーターに割り当てることはできません。
// プロパティ 'salary' は型 'Obj' にありませんが、型 'Obj2' では必須です。ts(234
log2(obj)
// ✅ エラーにならない
log2(obj2)
サブタイプでないとエラーになるパターンも確認しましょう。
以下のコードでは、Obj.ageが20 | 30とリテラル型のユニオン型に変更しています。Obj2はObjのすべてのプロパティのサブタイプを持つプロパティを有する必要がありますが、ageはnumber型であり、プリミティブ型のサブタイプではない(スーパータイプ)ので、今度はエラーになります。
type Obj = {
name: string
age: 20 | 30 // リテラル型
}
type Obj2 = {
name: string
age: number
salary: number
}
const obj: Obj = { name: "John", age: 30}
const obj2: Obj2 = { name: "Ann", age: 20, salary: 4000}
const log = (o: Obj) => {
console.log(o)
}
// ✅ エラーにならない
log(obj)
// ❌ エラーになる:型 'Obj2' の引数を型 'Obj' のパラメーターに割り当てることはできません。
// プロパティ 'age' の型に互換性がありません。
// 型 'number' を型 '20 | 30' に割り当てることはできません。
log(obj2)
最後にオプショナルなプロパティのケースについても確認しておきましょう
例えば、Obj2のsalaryがオプショナルになった場合objは代入可能になるでしょうか?
答えは代入可能になります。
salaryはnumber | undefinedの型になり、
undefinedは定義がないという意味なので、Objになくてもサブタイプを満たすからです
type Obj = {
name: string
age: number
}
type Obj2 = {
name: string
age: number
salary?: number
}
const obj: Obj = { name: "John", age: 30}
const obj2: Obj2 = { name: "Ann", age: 20, salary: 4000}
const log2 = (o: Obj2) => {
console.log(o)
}
// ✅ エラーにならない
log2(obj)
// ✅ エラーにならない
log2(obj2)
これでオブジェクトのサブタイプは「オブジェクトAが、オブジェクトBの全ての必須プロパティのサブタイプを満たすプロパティを有している」というのがイメージを掴むことができたのではないでしょうか?
次にオブジェクトの鬼門でもある、余剰プロパティチェックについて解説します。
余剰プロパティチェック
先ほどの説明では、オブジェクトAが、オブジェクトBの全プロパティのサブタイプを有しているならば、その時点でサブタイプとみなすことができ、代入できると説明しました。
しかし、実はオブジェクトAがオブジェクトBにはないプロパティ(余剰なプロパティ)を持っていたらエラーになるケースがあります。
それは、オブジェクトリテラルを代入しようとした時です。
✍️ オブジェクトリテラルとは{}で囲まれた直接なオブジェクトです。
// これはオブジェクトリテラル
const person = {
name: "Alice",
age: 30
}
// これはオブジェクトリテラルでない
const person2 = person
オブジェクトリテラルを代入するときに、余剰プロパティチェックというものが走り、不要なプロパティを持っているとエラーになるのです。
実際にコードで確認してみましょう。
先ほどの例でobjにはオブジェクトリテラルを代入していましたが、新しくsalaryというプロパティを追加すると、エラーが発生します。このエラーが余剰プロパティチェックです。これは厳密にいうとサブタイプとはまた別の概念ですが、TypeScriptを扱う上でとても重要な概念です。
type Obj = {
name: string
age: 20 | 30
}
type Obj2 = {
name: string
age: number
salary: number
}
// ❌ エラーになる:オブジェクト リテラルは既知のプロパティのみ指定できます。'salary' は型 'Obj' に存在しません。ts(2353)
const obj: Obj = { name: "John", age: 30, salary: 1000}
const obj2: Obj2 = { name: "Ann", age: 20, salary: 4000}
余剰プロパティチェックの具体例をもう少し見ていきましょう。
以下の例はオブジェクトリテラルを直接代入しているので余剰プロパティチェックが走ります。
type Option = {
baseUrl: string
cacheSize: number
}
class API {
constructor(option: Option){}
}
// ❌ エラー: オブジェクト リテラルは既知のプロパティのみ指定できます。'timeout' は型 'Option' に存在しません。ts(2353)
new API({
baseUrl: "https://api.example.com",
cacheSize: 1000,
timeout: 1000,
})
次はasで型アサーションをした例です。
型アサーションを利用すると、オブジェクトリテラルを代入しても余剰プロパティチェックは実行されません。
// ✅ エラーにならない
new API({
baseUrl: "https://api.example.com",
cacheSize: 1000,
timeout: 1000,
} as Option)
次は変数に代入した例です。変数に代入することでオブジェクトリテラルでなくなる(フレッシュなオブジェクトになる)ので、余剰プロパティチェックは実行されません。
const option = {
baseUrl: "https://api.example.com",
cacheSize: 1000,
timeout: 1000,
}
// ✅ エラーにならない
new API(option)
最後に、変数に型を定義した場合です。これはOption型にオブジェクトリテラルを代入する時点で余剰プロパティチェックが走るのでエラーになります。
// ❌ エラー:オブジェクト リテラルは既知のプロパティのみ指定できます。'timeout' は型 'Option' に存在しません。ts(2353)
const option2: Option = {
baseUrl: "https://api.example.com",
cacheSize: 1000,
timeout: 1000,
}
以上で余剰プロパティチェックの説明を終了します。
最後に、関数型の型の代入性について説明します。
関数型
関数型は以下の条件を満たした場合、サブタイプと見なすことができます。
- 引数の数:代入先が代入元より多い引数
- 引数の型:代入先の引数型が代入元の引数型のスーパータイプ
- 戻り値の型:代入先の戻り値型が代入元の戻り値型のサブタイプ
つまり、上記の3つの条件を満たしていないとサブタイプと判断されないので型の代入が行えません。
言葉より具体例を見た方が理解しやすいと思います。
まずは引数の数が異なる関数の例から示します。
- 引数の数:代入先が代入元より多い引数
以下のコードでは、Func1の型にFunc2の型を代入しようとする際に、引数の数が、Func2の方がFunc1より多いためエラーになります。
逆に、Func2の型にFunc1の型を代入する場合は、Func1がFunc2より少ない引数を持つため、余分な引数は無視されエラーになりません。
type Func1 = (x: number) => number;
type Func2 = (x: number, y: number) => number;
const func1: Func1 = (x) => x * 2;
const func2: Func2 = (x, y) => x + y;
// ❌ エラー:型 'Func2' を型 'Func1' に割り当てることはできません。ターゲット署名の引数が少なすぎます。2 以上が必要ですが、1 でした。
const func3: Func1 = func2;
// ✅ エラーにならない
const func4: Func2 = func1;
次に引数の型がスーパータイプとなる例を示します。
- 引数の型:代入先の引数型が代入元の引数型のスーパータイプ
以下のコードでは、Func1とFunc2の引数の数は同じですが、Func1のxの型(1 | 10 | 100)がFunc2のxの型(number)のサブタイプとなっています。
つまり、Func1を期待する箇所にFunc2を代入可能ですが、その逆は代入不可能です。
type Func1 = (x: 1 | 10| 100, y: number) => number;
type Func2 = (x: number, y: number) => number;
const func1: Func1 = (x, y) => x * y;
const func2: Func2 = (x, y) => x * y;
// ✅ エラーにならない
const func3: Func1 = func2
// ❌ エラー:型 'Func1' を型 'Func2' に割り当てることはできません。パラメーター 'x' および 'x' は型に互換性がありません。型 'number' を型 '1 | 10 | 100' に割り当てることはできません。ts(2322)
const func4: Func2 = func1
最後に戻り値がサブタイプとなる例を示します。
- 戻り値の型:代入先の戻り値型が代入元の戻り値型のサブタイプ
Func1の戻り値型(number)はFunc2の戻り値型(number | string)のサブタイプです。
そのため、Func2を期待する箇所にFunc1は代入可能ですが、その逆はエラーになります。
type Func1 = (x: number, y: number) => number;
type Func2 = (x: number, y: number) => number | string;
const func1: Func1 = (x, y) => x * y;
const func2: Func2 = (x, y) => x * y;
// ❌ エラー:型 'Func2' を型 'Func1' に割り当てることはできません。
// 型 'string | number' を型 'number' に割り当てることはできません。
// 型 'string' を型 'number' に割り当てることはできません。ts(2322)
const func3: Func1 = func2
// ✅ エラーにならない
const func4: Func2 = func1
おわりに
近頃は生成AIが発達して簡単なエラーであればすぐ解決してくれますが、だからこそ、エンジニアの理解力や認知力で差が出ると個人的に考えているため、このタイミングで、TypeScriptの基本について自分の整理のためにもまとめてみました。
あまり記事を書くのは得意ではないですが、次は「TypeScriptの型推論」や「現場でよく使うTips」、「型の構築」なんかについて記事を書こうかなと思っているのでお楽しみに✨
Discussion