カテゴリー
個人的Typescriptの型入門① 〜 型の基本
※ 当ページには【広告/PR】を含む場合があります。
2024/09/08
2025/07/14

目次
- 1. 型の学習には「Typescript Playground」を使おう
- 2. プリミティブ型とリテラル型
- 3. オブジェクト型と配列型
- 4. 関数型
- 5. void型・any型
- 6. ジェネリック型を理解する
- 7. タプル型
- 8. Union型とIntersection型
- 8-1. Union型
- 8-2. オブジェクト型のUnion型
- 8-3. in演算子を使ったUnion型の絞り込み
- 8-4. in演算子を用いた型の絞り込みの注意点
- 8-5. typeof演算子を用いた型の絞り込み
- 8-6. nullチェック
- 8-7. &&や||でnullチェック
- 8-8. タグ付きUnion型でオブジェクト型の絞り込み
- 8-9. Union型のプロパティ
- 8-10. Intersection型
- 8-11. Union型とIntersection型を組みあわせ
- 8-12. Union型を持つ関数型
- 8-13. 構成が全部関数型のUnion型
- 8-14. 引数がIntersection型で表現される関数型のUnion型
- 8-15. Union型にかかる機能制限
- 9. never型
- 10. Objectクラス型
- 11. unknown型
- 12. まとめ
個人的にはほどほどの付き合いをしていると思って使っているTypescriptの型ですが、場合によっては利用するライブラリがバリバリの型実装で書かれているものを読み解く必要があります。
自分ではそんなマニアックかつ高度な型実装を使うこともない場合でも、他人のコードを見たときにある程度分かるようになっておきたいところです。
そこで今回はTypescriptの型の初歩を抑えつつ、例を交えながら段階的にTypescriptの型にまつわる利用法を列挙していきます。
なお、巷ではとても参考になるTypescriptの型に関する記事が多く存在しています。
とりわけこの記事では以下の詳しい解説されているサイトをベースに、著者が防備録として抑えておきたい利用法をピックアップしています。
詳しい説明が必要の際には、ご本家のほうでご参照ください。
型の学習には「Typescript Playground」を使おう
復習も兼ねて、型の基礎のほうから徐々にTypescriptの型の使い方を慣らして行きましょう。
なおTypescriptの型チェックは、VSCode等のエディタで用意されているものを扱うことを想定していますが、サッとTypescriptを試したい際には定番の
プリミティブ型とリテラル型
プリミティブ型
TypescriptでもJavaScriptのプリミティブ型に対応した
string, number, boolean, symbol, bigint, null, undefined
const a: number = 3;
//✘ Type 'number' is not assignable to type 'string'.
const b: string = a;
strictNullChecksオプション
Typescriptのコンパイルオプション・
--strictNullChecks
strictNullChecks
const a: null = null;
//(strictNullChecksがオンの場合)
//✘ Type 'null' is not assignable to type 'string'.
const b: string = a;
リテラル型
リテラル型はプリミティブ型からさらに細分化した型です。
文字列リテラル型/数値リテラル型/ブーリアンリテラル型があり、それぞれ'foo'や3やtrueというような名前の型で使えます。
const a: 'foo' = 'foo';
//✘ Type '"foo"' is not assignable to type '"bar"'.
const b: 'bar' = 'foo';
リテラル型 --> 上位の型
文字列リテラル型はstring型の部分型であるため、文字列リテラル型をstring型として扱うことができます。
const a: 'foo' = 'foo';
const b: string = a;
他のリテラル型も同様です。
const a: 3 = 3;
const b: number = a;
const a: true = true;
const b: boolean = a;
リテラル型の型推論
const変数に型注釈を省略してもちゃんとリテラル型として推論されます。
//aは'foo'型(文字列リテラル型)と推論
const a = 'foo';
//✘ Type '"foo"' is not assignable to type '"bar"'.
const b: 'bar' = a;
let変数の型推論
リテラル型の型推論は
const
let/var
//aはstring型と推論
let a = 'foo';
const b: string = a;
//✘ Type 'string' is not assignable to type '"foo"'.
const c: 'foo' = a;
letで宣言した変数の型をリテラル型にしたい場合には、型注釈が必須です。
//aは'foo'型
let a: 'foo' = 'foo';
//✘Type '"bar"' is not assignable to type '"foo"'.
a = 'bar';
オブジェクト型と配列型
オブジェクト型
TypeScriptで、Javascriptオブジェクトを表現するための型がオブジェクト型であり、
{ }
例えば、
{foo: string; bar: number}
Typescriptではオブジェクト型の宣言は、
interface
type(型エイリアス)
interface MyObj {
foo: string;
bar: number;
}
type MyObj = {
foo: string;
bar: number;
}
この例では、
{foo: string; bar: number}
オブジェクト型にすることで変数の型注釈として付けることができます。
interface MyObj {
foo: string;
bar: number;
}
const a: MyObj = {
foo: 'foo',
bar: 3,
};
なお、この例で型注釈を付けない場合、オブジェクト型(
{foo: string; bar: number}
オブジェクト型の型エラー
型が合わないオブジェクトを変数に代入したりしようとすると型エラーとなります。
interface MyObj {
foo: string;
bar: number;
}
const a: MyObj = {
foo: 'foo',
//✘Type 'string' is not assignable to type 'number'.
bar: 'BARBARBAR',
};
//✘Property 'bar' is missing in type '{ foo: string; }' but required in type 'MyObj'.
const b: MyObj = {
foo: 'foo',
};
オブジェクト型の部分型
TypeScriptの型システムにおいて、構造的な「部分型」を設計することができます。
以下の例で、MyObj型はMyObj2の部分型(派生型)になっています。
interface MyObj {
foo: string;
bar: number;
}
interface MyObj2 {
foo: string;
}
const a: MyObj = {foo: 'foo', bar: 3};
const b: MyObj2 = a;
一般に部分型のオブジェクトはその上位の型として扱っても矛盾はありません。
この例では、部分型であるMyObj型の値aを、上位の型であるMyObj2型の変数bに代入することができます。
オブジェクトリテラル
interface
type
{...}
{foo: string, bar: numbder}
「オブジェクトリテラル」
let obj1: {id: number, name: string};
const obj2 = {id: 1, name: "hoge"};
上の例では、オブジェクトリテラルを使って、obj1は
{id: number, name: string}
obj2も
{id: number, name: string}
{id: 1, name: "hoge"}
このようにオブジェクトリテラルは、即席の型定義を記述する場合に便利な表記法です。
オブジェクトリテラルで部分型を使う注意点
オブジェクトリテラルに対しては適用外ルールがあり、次のような場合には注意が必要です。
interface MyObj {
foo: string;
bar: number;
}
interface MyObj2 {
foo: string;
}
//✗Object literal may only specify known properties, and 'bar' does not exist in type 'MyObj2'.
const b: MyObj2 = {foo: 'foo', bar: 3};
//あまり意味は無いが以下ならエラーなし
const b: MyObj2 = {foo: 'foo', bar: 3} as MyObj;
一見、
{foo: 'foo', bar: 3}
{foo: string, bar: number}
実際は、このオブジェクトリテラルはMyObj2型に代入できずに、エラーになっています。
これは上位型-部分型の関係性を吟味する以前の問題で、「単なる書き間違いじゃない?」とTypeScriptで判断されるからのようです。
同様に、このオブジェクトリテラルの適用外ルールは、関数の引数の場合にも起こります。
interface MyObj2 {
foo: string;
}
function func(obj: MyObj2): void { }
//✗Object literal may only specify known properties, and 'bar' does not exist in type 'MyObj2'.
func({foo: 'foo', bar: 3});
//あまり意味は無いが以下ならエラーなし
func({foo: 'foo', bar: 3} as MyObj2);
配列型
配列型を表すためには
[]
Array<>
const foo: number[] = [0, 1, 2, 3];
//もしくは
//const foo: Array<number> = [0, 1, 2, 3];
foo.push(4);
関数型
関数型
関数型は例えば
(x: string, y: number)=>boolean
これは、第1引数としてstring型の、第2引数としてnumber型の引数をとり、返り値としてboolean型の値を返す関数型、を意味しています。
function func(arg: string): number {
return Number(arg);
}
const f: (foo: string)=>number = func;
関数型の引数と部分型
関数型の引数にも、上位型-部分型の関係が考慮される必要があります。
以下の例は、関数型の引数に用いられるオブジェクト型で、MyObjがMyObj2の部分型である場合、その関数型である
(obj: MyObj2)=>void
(obj: MyObj)=>void
interface MyObj {
foo: string;
bar: number;
}
interface MyObj2 {
foo: string;
}
const a: (obj: MyObj2)=>void = () => {};
const b: (obj: MyObj)=>void = a;
これは、「上位型(MyObj2)を入力して処理できる関数は、部分型(MyObj)を入力しても当然処理できるはず」、という意味のようです。
逆にすると、「部分型を入力して処理できる関数が、上位型を入力しても正しく処理できる保証はない」ので、以下のようにエラーとなります。
interface MyObj {
foo: string;
bar: number;
}
interface MyObj2 {
foo: string;
}
const b: (obj: MyObj)=>void = () => {};
//✗ Type '(obj: MyObj) => void' is not assignable to type '(obj: MyObj2) => void'.
// Types of parameters 'obj' and 'obj' are incompatible.
// Property 'bar' is missing in type 'MyObj2' but required in type 'MyObj'.
const a: (obj: MyObj2)=>void = b;
関数型の引数の数と部分型
関数型の引数の数に着目すると、先程と同じ理屈で、
(foo: string)=>void
(foo: string, bar: number)=>void
const f1: (foo: string)=>void = () => {};
const f2: (foo: string, bar: number)=>void = f1;
ここでのf2からすると、2番目の引数
bar: number
これを逆にしてしまうとエラーが出ます。
const f1: (foo: string, bar: number)=>void = ()=>{};
//✗ Type '(foo: string, bar: number) => void' is not assignable to type '(foo: string) => void'.
// Target signature provides too few arguments. Expected 2 or more, but got 1.
const f2: (foo: string)=>void = f1;
引数の数の多い関数型を、それより引数の少ない関数型としては扱えないためです。
ただし、特殊な書き間違い防止ルールで、関数を呼び出す側の余計な引数は無視してもらえないので注意しましょう。
const f1: (foo: string)=>void = () => {};
//✗ Expected 1 arguments, but got 2.
f1('foo', 3);
可変長引数
TypeScriptで可変長引数の関数を宣言には配列に対応します。
次の例では、...barに
number[]
const func = (foo: string, ...bar: number[]) => bar;
func('foo');
func('bar', 1, 2, 3);
//✗ Argument of type '"hey"' is not assignable to parameter of type 'number'.
func('baz', 'hey', 2, 3);
void型・any型
void型
void型は「何も返さない」ことを表す関数の返り値の型です。
JavaScriptでの何も返さない関数はundefinedを返すので、void型というのはundefinedのみを値として取る型となります。
実際、void型の変数にundefinedを入れても問題ありません。
const a: void = undefined;
ただし、その逆のvoid型をundefined型の変数に代入することはできません。
const a: void = undefined;
//✗ Type 'void' is not assignable to type 'undefined'.
const b: undefined = a;
通常、void型を変数として利用するケースはほぼ皆無で、「何も返さない関数の返り値の型」として使います。
function foo(): void {
console.log('hello');
}
any型
any型はどんな型とも相互変換可能であり、実質TypeScriptの型システムを無視することに相当します。
const a: any = 3;
const b: string = a;
any型を使うともはや「何でもあり」となり、TypeScriptの型システムを使う意味がなくなってしまいます。
ジェネリック型を理解する
クラス宣言とオブジェクト型
JavaScriptにもクラスを定義する
class
class
class Foo {
method(): void {
console.log('Hello, world!');
}
}
const obj: Foo = new Foo();
const obj2: Foo = {
method: () => {}
};
この例でいうと、Fooはクラス定義であり、同時にオブジェクト型でもあります。
よって、Typescriptにおける
class
Typescriptで純粋にオブジェクト型定義を行いたい場合、
class
interface
type
interface MyFoo {
method: () => void;
}
class Foo {
method(): void {
console.log('Hello, world!');
}
}
const obj: MyFoo = new Foo();
const obj2: Foo = obj;
extends修飾子
Typescript独自の構文に
「extends」
主にextendsの役割は、「interfaceを用いた型の継承(拡張)」です。
interface User {
name: string;
}
interface Root extends User {
isMaster: boolean;
}
//👇Rootは以下と同じ定義
// interface Root {
// name: string;
// isMaster: boolean;
// }
ジェネリクス
ジェネリクスとは、「型引数」とも言われ、受け取った型を遅延で使用する仕組みを作ります。
type A<T> = T;
const str: A<string> = 'moji';
const num: A<number> = 123;
オブジェクト型のジェネリクス
オブジェクト型の名前のあとに
< >
以下の例では、2つの型変数
S
T
Foo
Foo<S,T>
interface Foo<S, T> {
foo: S;
bar: T;
}
const obj: Foo<number, string> = {
foo: 3,
bar: 'hi',
};
例のように、
Foo<number, string>
number
string
クラス定義や関数定義でもジェネリクス
ジェネリクスの構文は、クラス名・関数名のあとに
< >
class Foo<T> {
constructor(obj: T) {}
}
function func<T>(obj: T): void { }
const obj = new Foo<string>('foo');
func<number>(3);
ジェネリクス付きの関数型
先程の例で、ジェネリクスを使ったfuncの関数型は
<T>(obj: T)=>void
関数型の場合、関数が呼び出される際に引数の型が決まるので、型変数もそのまま残しておく必要があるからです。
function func<T>(obj: T): void {}
const f: <T>(obj: T)=>void = func;
型引数の推論
ジェネリクス付き関数を呼び出す際の型引数は省略することができます。
この場合、上の例でいうと
func<number>(3)
<number>
func(3)
引数を省略した場合、関数に与えた引数の情報から型引数の型が推論されます。
次の例では
getId(3)
function getId<T>(value: T): T {
return value;
}
const value = getId(3);
const num: number = value;
const three: 3 = value;
//✘Type '3' is not assignable to type 'string'.
const str: string = value;
getIdの返り値の型は
3
3
3
型引数の推論は便利な一方、複雑なケースでは型変数が推論できないこともあるようで、多用はなるべく控えたいところです。
ジェネリック付きの関数型のプロパティ限定
以下のようなジェネリクスを使った関数型では、型変数・Tにnameプロパティが存在するかわからないためエラーになります。
function f<T>(arg: T): string {
//✗ Property 'name' does not exist on type 'T'.
return arg.name;
}
そこで、型変数Tはnameプロパティを持っているように制限をかけたいときに
extends
interface User {
name: string;
age: number;
};
function f<T extends User>(arg: T): string {
return arg.name;
}
f({ name: 'aaa', age: 123 });
//✗ Argument of type '{ age: number; }' is not assignable to parameter of type 'User'.
// Property 'name' is missing in type '{ age: number; }' but required in type 'User'.
f({ age: 123 });
型変数TはUserの部分型であることが保証され、nameプロパティを持つことになります。
タプル型
タプル型
JavaScriptには「タプル」はありませんが、TypeScriptでは「タプル型」が使えます。
タプル型は多様な型の値を要素を持つ固定長の配列で、関数から複数の値を配列に入れてまとめて返すようなユースケースに利用できます。
const foo: [string, number] = ['foo', 5];
const str: string = foo[0];
function makePair(x: string, y: number): [string, number] {
return [x, y];
}
上の例では、タプル型・
[string, number]
これは長さが2の配列で、1つ目が文字列型、2つ目が数値型が入ったような型を表しています。
タプル型の注意点
TypeScriptがタプルと呼んでいるものは、トランスパイル後に使えば結局はJavascriptの配列です。
次の例は、タプルの2番目の要素を数値型の値から文字列型に値に置き換えるものです。
const tuple: [string, number] = ['foo', 3];
tuple.pop();
tuple.push('Hey!');
const num: number = tuple[1];
//"Hey!"
console.log(num);
配列の組込みメソッドでタプル型を破壊する際にエラーでないため、タプル型の2つ目の要素は数値型であるはずなのに、実行すると文字列型が出力されます。
このように現状、TypeScriptのタプル型は、予期しない挙動が起こる可能性があります。
タプル型の利用には危険が潜んでいることを覚えておくと良いでしょう。
要素数ゼロのタプル型
空の配列型を宣言しようとした場合、
any[]
unknown[]
T[]
タプルを使えば、配列の要素の型を考えることなく「要素数ゼロのタプル型」を作ることができます。
const unit: [] = [];
使いどころはあまりないように思います。
可変長タプル型・その1
もはやタプル型とはなんなのかと言いたくなりますが、固定長の配列として表現されるタプル型を可変長にするテクニックも存在します。
type NumAndStrings = [number, ...string[]];
const a1: NumAndStrings = [3, 'foo', 'bar'];
const a2: NumAndStrings = [5];
//✘ Type 'string' is not assignable to type 'number'.
const a3: NumAndStrings = ['foo', 'bar'];
最初のいくつかの要素の型が特別扱いして、残りを同種の型で有限数並べたような配列の型となります。
上の例で、変数a3は、最初の要素が数値でないのでエラーとなります。
可変長タプル型・その2
タプル型の内部の配列部分は、柔軟な位置に書くことができます。
例えば、最後の要素だけnumber型で他はstring型の配列は次のように書けます。
type StrsAndNumber = [...string[], number];
const b1: StrsAndNumber = ['foo', 'bar', 'baz', 0];
const b2: StrsAndNumber = [123];
// ✘ Type '[string, string]' is not assignable to type 'StrsAndNumber'.
// Type at position 1 in source is not compatible with type at position 1 in target.
// Type 'string' is not assignable to type 'number'.
const b3: StrsAndNumber = ['foo', 'bar'];
ただし、スプレッド(...)を使えるのはタプル型のどこかに1回だけです。
以下のような可変長タプル型は許されていません。
//✗ A rest element cannot follow another rest element.
type StrsAndNumber = [...string[], ...number[]];
オプショナルな要素を持つタプル型
[string, number?]
この場合、2番目の要素はあっても無くてもいい、という意味になります。
type T = [string, number?];
const t1: T = ['foo'];
const t2: T = ['foo', 3];
オプショナルな要素は複数あっても構いませんが、そうでない要素より後に来なければいけません。
例えば以下のタプル型は不可です。
//✗ A required element cannot follow an optional element.
type T = [string?, number];
タプル型と関数の可変長引数
タプル型は関数の可変長引数の型を表すのにも使えます。
下の例では、関数の可変長引数argsの型はタプル型
Args(=[string, number, boolean])
type Args = [string, number, boolean];
const func = (...args: Args) => args[1];
//vはnumber型
const v = func('foo', 3, true);
引数の配列argsがタプル型Argsを持つようにするためには、関数funcの引数の型は、string、number、boolean、の順で与えなければいけません。
このテクニックを使えば、タプル型で、複数の関数の引数の型をまとめて指定することができます。
オプショナルな要素を持つタプル型を用いたオプショナルな引数を持つ関数型
同様に、オプショナルな要素を持つタプルの型を用いた場合はオプショナルな引数を持つ関数の型ができることになります。
type Args = [string, ...number[]];
const func = (f: string, ...args: Args) => args[0];
const v1 = func('foo', 'bar');
const v2 = func('foo', 'bar', 1, 2, 3);
関数呼び出しのスプレッド構文とタプル型
JavaScriptでもスプレッド構文「...」という記法は関数呼び出しのときにも使うことができます。
以下の例では、func(...strings)の意味は、配列stringsの中身をfuncの引数に展開して呼び出すということです。
つまり、funcの最初の引数はstringsの最初の要素になり、2番目の引数は2番目の要素に……となります。
const func = (...args: string[]) => args[0];
const strings: string[] = ['foo', 'bar', 'baz'];
func(...strings);
タプル型はここでも使うことができます。
適切なタプル型の配列を...で展開することで、型の合った関数を呼び出すことができるのです。
const func = (str: string, num: number, b: boolean) => args[0] + args[1];
const args: [string, number, boolean] = ['foo', 3, false];
console.log(func(...args));
タプル型と可変長引数とジェネリクス
タプル型と可変長引数とジェネリクスを組み合わせることによって、さらに複雑な型表現が可能になります。
タプル型をとるような型変数を用いることで、関数の引数列をジェネリクスで扱うことができるのです。
例として、関数の最初の引数があらかじめ決まっているような新しい関数を作る関数bindを書いてみます。
function bind<T, U extends any[], R>(
func: (arg1: T, ...rest: U)=>R,
value: T,
): ((...args: U) => R) {
return (...args: U) => func(value, ...args);
}
const add = (x: number, y: number) => x + y;
const add1 = bind(add, 1);
console.log(add1(5)); // 6
//✗ Argument of type '"foo"' is not assignable to parameter of type 'number'.
add1('foo');
関数bindは2つの引数funcとvalueを取り、新しい関数(...args: U) => func(value, ...args)を返します。
この関数は、受け取った引数列argsに加えて最初の引数としてvalueをfuncに渡して呼び出した返り値をそのまま返す関数です。
ポイントは、まず
U extends any[]
これは新しい記法ですが、「型引数Uはany[]の部分型でなければならない」、という意味です。
string[]などの配列型に加えてタプル型も全部any[]の部分型です。
この制限を加えることにより、
...rest: U
加えて、
bind(add, 1)
T = number, U = [number], R = number
返り値の型は
(...args: U)=>R
(arg: number)=>number
Uがタプル型に推論され、addの引数の情報が失われずにadd1に引き継がれています。
Union型とIntersection型
Union型
Union型は別名「合併型」とも呼ばれ、複数の型のどれかに当てはまるような型を表します。
記法としては、複数の型を
「|」
例えば、
string | number
let value: string | number = 'foo';
value = 100;
value = 'bar';
//✘ Type 'true' is not assignable to type 'string | number'.
value = true;
オブジェクト型のUnion型
プリミティブ型に限らずオブジェクト型でもUnion型を作ることができます。
interface Hoge {
foo: string;
bar: number;
}
interface Piyo {
foo: number;
baz: boolean;
}
type HogePiyo = Hoge | Piyo;
const obj: HogePiyo = {
foo: 'hello',
bar: 0,
};
in演算子を使ったUnion型の絞り込み
Union型に代入された値がどの型になるのか、知りたい場合があります。
この例では「in演算子」を使った判定で、型を検出して適切に絞り込んでくれる機能があります。
interface Hoge {
foo: string;
bar: number;
}
interface Piyo {
foo: number;
baz: boolean;
}
function useHogePiyo(obj: Hoge | Piyo): void {
// objはHoge | Piyo型
if ('bar' in obj) {
//barプロパティがあるならHoge型
console.log('Hoge', obj.bar);
} else {
//barプロパティがないならPiyo型
console.log('Piyo', obj.baz);
}
}
「in演算子」を
'bar' in obj
in演算子を用いた型の絞り込みの注意点
in演算子で、次のようなコードでは注意が必要です。
interface Hoge {
foo: string;
bar: number;
}
interface Piyo {
foo: number;
baz: boolean;
}
function useHogePiyo(obj: Hoge | Piyo): void {
//objはHoge | Piyo型
if ('bar' in obj) {
//barプロパティがある --> objはHoge型
console.log('Hoge', obj.bar);
} else {
//barプロパティがない --> objはPiyo型
console.log('Piyo', obj.baz);
}
}
const obj: Hoge | Piyo = {
foo: 123,
bar: 'bar',
baz: true,
};
useHogePiyo(obj);
ここでobjに代入されている値は、Piyo型の部分型でかつ
bar: string
Piyo型の部分型なので、
Hoge | Piyo
ただし、useHogePiyo関数の判定では、
'bar' in obj
typeof演算子を用いた型の絞り込み
typeof演算子は与えられた値の型を文字列で返す演算子です。
typeof演算子によって、
typeof <変数>
let foo = 'str';
//FooTypeはstring
type FooType = typeof foo;
const str: FooType = 'abcdef';
プリミティブ型のUnion型なら、このtypeof演算子でもっと単純に型の絞り込みができます。
function func(value: string | number): number {
if ('string' === typeof value) {
// valueはstring型なのでlengthプロパティを見ることができる
return value.length;
} else {
// valueはnumber型
return value;
}
}
オブジェクトが絡まないこともあり、in演算子より確実です。
nullチェック
Union型がよく目にするのが、Nullableな値を扱いたい場合です。
例えば、
string | null
典型的なやり方で、if文で
value != null
function func(value: string | null): number {
if (value != null) {
//valueはstring型に絞り込まれる
return value.length;
} else {
return 0;
}
}
&&や||でnullチェック
nullチェックと組み合わせて、
&&
||
function func(value: string | null): number {
return value != null && value.length || 0;
}
タグ付きUnion型でオブジェクト型の絞り込み
プリミティブ型ならUnion型の絞り込みは確実性の高い実装ができるものの、オブジェクト型のUnion型は実装に工夫が必要です。
オブジェクト型のUnion型の絞り込みに推奨されているパターンとして、リテラル型とUnion型を組み合わせて、代数的データ型(タグ付きUnion型)を再現する方法があります。
例として、Option型を簡単に実装する例です。
interface Some<T> {
type: 'Some';
value: T;
}
interface None {
type: 'None';
}
type Option<T> = Some<T> | None;
function map<T, U>(obj: Option<T>, f: (obj: T)=> U): Option<U> {
if (obj.type === 'Some') {
//objはSome<T>型
return {
type: 'Some',
value: f(obj.value),
};
} else {
//objはNone型
return {
type: 'None',
};
}
}
ここでの
Option<T>
Some<T>
None
ポイントは、共通プロパティである
typeプロパティ
'Some'/'None'
typeプロパティのリテラル型をタグとして使うことでUnion型の絞り込みを行っています。
次のように
switch
interface Some<T> {
type: 'Some';
value: T;
}
interface None {
type: 'None';
}
type Option<T> = Some<T> | None;
function map<T, U>(obj: Option<T>, f: (obj: T)=> U): Option<U> {
switch (obj.type) {
case 'Some':
return {
type: 'Some',
value: f(obj.value),
};
case 'None':
return {
type: 'None',
};
}
}
map関数の場合はswitch文を使うほうが拡張に対して強くて安全です。
Union型のプロパティ
異なる型をもつ同名プロパティを持ったオブジェクト型でUnion型を作った場合、そのプロパティもUnion型として推論されます。
どういうことかというと、以下の例では
Hoge | Piyo
string | number
interface Hoge {
foo: string;
bar: number;
}
interface Piyo {
foo: number;
baz: boolean;
}
type HogePiyo = Hoge | Piyo;
function getFoo(obj: HogePiyo): string | number {
//obj.fooはstring | number型
return obj.foo;
}
配列の要素もプロパティの一種なので、同じ挙動となります。
const arr: string[] | number[] = [];
//string[] | number[]型の配列の要素はstring | number型
const elm = arr[0];
Intersection型
Intersection型は別名「交差型」と呼ばれ、
「&」
たとえばこの例では、
Hoge & Piyo
interface Hoge {
foo: string;
bar: number;
}
interface Piyo {
foo: string;
baz: boolean;
}
const obj: Hoge & Piyo = {
foo: 'foo',
bar: 3,
baz: true,
};
Union型とIntersection型を組みあわせ
例えば、Union型とIntersection型を組みあわせた、
(Hoge | Piyo) & Fuga
(Hoge & Fuga) | (Piyo & Fuga)
interface Hoge {
type: 'hoge';
foo: string;
}
interface Piyo {
type: 'piyo';
bar: number;
}
interface Fuga {
baz: boolean;
}
type Obj = (Hoge | Piyo) & Fuga;
function func(obj: Obj) {
//objはFuga型でもありbazを参照可
console.log(obj.baz);
if (obj.type === 'hoge') {
//objはHoge & Fuga型
console.log(obj.foo);
} else {
// objはPiyo & Fuga型
console.log(obj.bar);
}
}
Union型を持つ関数型
関数型を含むUnion型というものも考えることができます。
type Func = (arg: number) => number;
interface MyObj {
prop: string;
}
const obj : Func | MyObj = { prop: '' };
//✘ Cannot invoke an expression whose type lacks a call signature.
// Type 'MyObj' has no compatible call signatures.
obj(123);
当然ながら、関数型とそれ以外の型で作ったUnion型は関数として呼ぶことはできません。
構成が全部関数型のUnion型
では、全て関数型のメンバーでUnion型を作ったら、関数として呼び出せるかというと、そうではありません。
type StrFunc = (arg: string) => string;
type NumFunc = (arg: number) => string;
declare const obj : StrFunc | NumFunc;
//✘ Argument of type '123' is not assignable to parameter of type 'string & number'.
// Type '123' is not assignable to type 'string'.
obj(123);
この例では、文字列を受け取って文字列を返す関数型・StrFuncと、数値を受け取って文字列を返す関数型・NumFuncで、
StrFunc | NumFunc
ですが、
StrFunc | NumFunc
この場合、objはStrFunc型かもしれないので引数は文字列でないといけない一方、NumFunc型かもしれないので引数は数値でないといけません。
つまり、関数として呼び出せる引数は「文字列かつ数値(
string & number
string & number
このように、関数型のUnion型を考えるとき、結果の関数の引数はもともとの引数同士のIntersection型を持つ必要があります。
ちなみに、
string & number
引数がIntersection型で表現される関数型のUnion型
引数の型が現実的なIntersection型で例を見てみます。
この例ではfuncは
HogeFunc | PiyoFunc
interface Hoge {
foo: string;
bar: number;
}
interface Piyo {
foo: string;
baz: boolean;
}
type HogeFunc = (arg: Hoge) => number;
type PiyoFunc = (arg: Piyo) => boolean;
declare const func: HogeFunc | PiyoFunc;
//resは number | boolean 型
const res = func({
foo: 'foo',
bar: 123,
baz: false,
});
HogeFunc | PiyoFunc
Hoge & Piyo
なお、resの型はnumber | boolean型になっているのも注目です。
これは、funcの型がHogeFuncの場合は返り値がnumberであり、PiyoFuncの場合は返り値がbooleanであることから説明できます。
Union型にかかる機能制限
関数型のUnion型は処理の扱いが煩雑なためか、現在のところ制限があります。
具体的には関数のオーバーロードがある場合やジェネリクスが関わる場合に関数が呼べなかったり、引数の型が推論できなかったりする場合があります。
const arr: string[] | number[] = [];
//✘ Parameter 'x' implicitly has an 'any' type.
arr.forEach(x => console.log(x));
//✘ Cannot invoke an expression whose type lacks a call signature.
const arr2 = arr.map(x => x);
never型
never型
never型は「属する値が存在しない型」であり、型システムのもっとも下位にある部分型(任意の型の部分型)です。
よって、どんな値もnever型の変数に入れることはできません。
//✘ Type '0' is not assignable to type 'never'.
const n: never = 0;
一方、never型の値はあらゆる型にも入ります。
//never型の値を作る方法が無いのでdeclareで無理やり宣言
declare const n: never;
const foo: string = n;
never型のユースケース・その1
never型は「絶対に到達できない値」の概念を表現したもので、(TypeScriptの型システムを欺かない限り)never型の値を作ることは不可能です。
never型は、型システムを考える上で、ひっそりと推論される形で出現します。
interface Some<T> {
type: 'Some';
value: T;
}
interface None {
type: 'None';
}
type Option<T> = Some<T> | None;
function map<T, U>(obj: Option<T>, f: (obj: T)=> U): Option<U> {
switch (obj.type) {
case 'Some':
return {
type: 'Some',
value: f(obj.value),
};
case 'None':
return {
type: 'None',
};
default:
//objはnever型
return obj;
}
}
上の例では、処理がdefaultに到達することはありえないので、このobjはnever型になります。
never型のユースケース・その2
never型は値を返す可能性が無い関数の返り値にも現れます。
そのそも返り値が無いことを表すのはvoid型ですが、それは関数が正常終了する場合の話です。
関数がなんらかの異常で中断し、値を返すことなく関数を脱出した場合、これはnever型(到達不可能な値)です。
この挙動は、例えば以下のようなコードで確認できます。
function func(): never {
throw new Error('Hi');
}
const result: never = func();
Objectクラス型
Object型
存在感の薄い準プリミティブ型に
object
JavaScriptでのオブジェクトのみを引数に受け取る関数などを表現するための互換性を保つための型です。
例えば、組み込みのObject.createメソッドは、その引数としてオブジェクトまたはnullのみを受け取る関数です。
//✘ Argument of type '3' is not assignable to parameter of type 'object | null'.
Object.create(3);
{}型
たまに目にする
{}
プロパティが無いといっても、
{foo: string}
{}
const obj = { foo: 'foo' };
const obj2: {} = obj;
ということは、任意のオブジェクト型を、
{}
でも実際には、{}型は、オブジェクト型以外(undefinedとnullは除外)も受け付けてしまうので、「すべてのオブジェクト型を部分型に持つ型」としては使えません。
例えば、以下のように、プリミティブ型に対してもプロパティアクセスができてしまいます。
const o: {} = 3;
プリミティブ型を除外できないのは、型システムとしては欠陥となります。
例えば、プリミティブ型のひとつであるstring型に対して、
length: number;
interface Length {
length: number;
}
const o: Length = 'foobar';
このことから、{}というのはundefinedとnull以外は何でも受け入れてしまうようなとても「弱い型」であると解釈できます。
weak typeルール
「弱い型」は、
{}
{}
困ったことに、そのような型は関数の引数で、オプションを保持したオブジェクト型として良く利用されるため、「weak type」という位置づけで例外的な処理をとります。
interface Options {
foo?: string;
bar?: number;
}
const obj1 = { hoge: 3 };
//✘ Type '{ hoge: number; }' has no properties in common with type 'Options'
const obj2: Options = obj1;
//✘ Type '5' has no properties in common with type 'Options'.
const obj3: Options = 5;
Options型のプロパティはすべてオプショナルなので
{}
{ hoge: number; }
obj3も
{}
「weak typeのルール」は、weak typeが持つプロパティを1つ以上持った型である必要があります。
実際、弱い型はこういったルールで制限しなければ、
{}
ただし
{}
また、「weak typeのルール」はオブジェクト型ではない型も弾いてくれます。
unknown型
unknown型
先程までの話から、
{}
unknown型は、どのような型としても扱うことができる、部分型関係の最上位にある型になります。
この意味ではちょうどnever型の真逆に位置した型です。
const u1: unknown = 3;
const u2: unknown = null;
const u3: unknown = (foo: string)=> true;
unknown型とany型の違い
「どんな型でも入る」と聞くと、unknown型はany型と同じように思えます。
実際には、unknown型はany型とは異なり、型安全に扱うことができます。
というのは、unknown型の値はどんな値なのか分からないため、できることが制限されます。
const u: unknown = 3;
//✘ Object is of type 'unknown'.
const sum = u + 5;
//✘ Object is of type 'unknown'.
const p = u.prop;
上の例では、unknown型では、数値の足し算もプロパティアクセスも不可です。
any型の場合には、エラーが出ようがお構いなしの操作だったものが、unknown型では出来なくなっていることが大きく異なります。
unknown型の絞り込み
unknown型の型絞り込みは、前述したUnion型と同様の絞り込み方法で可能です。
const u: unknown = 3;
if (typeof u === 'number') {
// この中ではuはnumber型
const foo = u + 5;
}
また、クラスのオブジェクト型と、
instanceof
const u: unknown = 3;
class MyClass {
public prop: number = 10;
}
if (u instanceof MyClass) {
//uはMyClass型
u.prop;
}
unknown型とvoid型の意外な関係
unknown型とvoid型は共通する部分が多くあります。
次の例は、一見するとエラーが起こりそうですが、実際は正常に処理されます。
const func: ()=>number = () => 123;
const f: ()=>void = func;
ここでのfuncは
()=>number
その型を
()=>void
前述のように、void型は
const v: void = 1;
void型の関数の返り値は、返り値に興味がないし、何が入っていても構わない、という意味でunknown型相当の返り値になります。
例えば、以下を実行すると、void型の変数valueの中身は123になります。
const func: ()=>number = () => 123;
const f: ()=>void = func;
const value: void = f();
//123
console.log(value);
このことで、void型が必ずしもundefined型である必要すらなく、むしろなんでもありのunknown型に親しい型だと分かります。
まとめ
当初簡単にまとめるつもりが、思い出したいテクニックも多くあり、基礎的な内容を拾っていっても、結構な長丁場になってしましました。
今回は「型の基本」だけをまとめた話でしたが、次回以降の記事Typescriptの実用的なテクニックにもう少し内容を広げていく予定です。
記事を書いた人
ナンデモ系エンジニア
主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。
カテゴリー
- 1. 型の学習には「Typescript Playground」を使おう
- 2. プリミティブ型とリテラル型
- 3. オブジェクト型と配列型
- 4. 関数型
- 5. void型・any型
- 6. ジェネリック型を理解する
- 7. タプル型
- 8. Union型とIntersection型
- 8-1. Union型
- 8-2. オブジェクト型のUnion型
- 8-3. in演算子を使ったUnion型の絞り込み
- 8-4. in演算子を用いた型の絞り込みの注意点
- 8-5. typeof演算子を用いた型の絞り込み
- 8-6. nullチェック
- 8-7. &&や||でnullチェック
- 8-8. タグ付きUnion型でオブジェクト型の絞り込み
- 8-9. Union型のプロパティ
- 8-10. Intersection型
- 8-11. Union型とIntersection型を組みあわせ
- 8-12. Union型を持つ関数型
- 8-13. 構成が全部関数型のUnion型
- 8-14. 引数がIntersection型で表現される関数型のUnion型
- 8-15. Union型にかかる機能制限
- 9. never型
- 10. Objectクラス型
- 11. unknown型
- 12. まとめ