LoginSignup
3
4

More than 1 year has passed since last update.

第 4 章 TypeScript で型をご安全に メモ

Last updated at Posted at 2021-05-09

型アノテーション(Type Annotation)

TypeScript では value: Type というフォーマットで宣言時の変数 に型の注釈がつけられる
アノテー ションによって静的に型付けされた情報はコンパイル時のチェックに用いられ、書かれたコード中 に型の不整合があるとコンパイルエラーになる

$ ts-node
>let n : number = 3;
>n = 'foo';
[eval].ts:2:1 - error TS2322: Type '"foo"' is not assignable to type 'number'.
> if (n) console.log('`n` is truthy');
`n` is truthy
$ node
> const s = '123'; >constn=456; >s*3
369
>246/s
2
>s+n
'123456'
$ ts-node
> const s = '123';
>const n = 456;
>s*3
[eval].ts:4:1 - error TS2362: The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.
>246/s
[eval].ts:4:1 - error TS2363: The right-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.
>s+n
'123456'

JavaScript には『暗黙の型変換』という機能があって、この例のように文字列データを算術演算しようとすると、勝手にその変数の型を数値型に変換してしまう
TypeScript では、数値型と互換性のない型のデータは算術演算が許されないのでコンパイルエラーになる
+ は加算ではなく文字列の連結として評価される

型推論(Type Inference)

コンパイラがその文脈からその型を推測できる場合は、型ア ノテーションを省略しても自動的に補完して解釈してくれる

JavaScript と TypeScript のちがいはほぼ型システムだけ

型の種類

プリミティブ型は JavaScript と共通した次の 7 種類
・Boolean 型 ...... true および false の 2 つの真偽値を扱うデータ型。型名は boolean

・Number 型 ...... 数値を扱うためのデータ型。型名は number

・BigInt 型 ...... number 型では表現できない大きな数値(253 以上)を扱う型。型名は bigint

・String 型 ...... 文字列を扱うためのデータ型。型名は string

・Symbol 型 ......「シンボル値」という固有の識別子を表現する値の型。型名は symbol

・Null 型 ...... 何のデータも含まれない状態を明示的に表す値。型名は null

・Undefined 型 ......「未定義」であることを表す値。型名は undefined

配列の型

//型名の後ろに [] をつけると、その型データの配列になる
const numArr: number[] = [1, 2, 3];

//配列は Array オブジェクトとして定義する別の書き方もできる
//ジェネリクス(Generics)
const strArr: Array<string> = ['one', 'two', 'three'];

オブジェクトの型定義

const red: { rgb: string, opacity: number } = { rgb: 'ff0000', opacity: 1 };

TypeScript ではオブジェクトの型に名前をつけることができるようになってる。それが『インターフェース(Interface)』と呼ばれる

interface Color { readonly rgb: string; opacity: number; name?: string;
}
const turquoise: Color = { rgb: '00afcc', opacity: 1 };
turquoise.name = 'Turquoise Blue';
turquoise.rgb = '03c1ff'; // error TS2540: Cannot assign to 'rgb' because it is a read-only
property.

readonly 修飾子を つけたプロパティは書き換え不可になる
プロパティ名の末尾に ? をつけると、そのプロ パティは省略可能になる

interface Status { level: number;
maxHP: number;
maxMP: number;
[attr: string]: number;
}
const myStatus: Status = { level: 99,
maxHP: 999,
maxMP: 999,
attack: 999,
defense: 999, };

4 つめが任意のキーのプロパティ値を定義してる。これは『インデック スシグネチャ(Index Signature)』というもので、これのおかげで attack や defense というプロパテ ィを追加できてる。
インデックスシグネチャのキーに使える型は、文字列と数値の 2 種類のみ

enum 型

TypeScript の enum はデフォルトでは数値で、かつ型安全が保証されない
バージ ョン 2.4.0 から導入された文字列 enum を使えばその問題は解決する

>enumPet{
> Cat='Cat',
> Dog='Dog',
> Rabbit='Rabbit', >}
> let Tom: Pet = Pet.Cat;
> Tom = 'Hamster';
[eval].ts:7:1 - error TS2322: Type '"Hamster"' is not assignable to type 'Pet'.
> Tom = 'Dog';
[eval].ts:8:1 - error TS2322: Type '"Dog"' is not assignable to type 'Pet'.

リテラル型

リテラル型は単独ではあまり使いみちがないんだけど、演算子 | で並べることによってあ たかも列挙型のように扱える
これは『共用体型』というものの合わせ技

> let Mary: 'Cat' | 'Dog' | 'Rabbit' = 'Cat'; > Mary = 'Rabbit';
> Mary = 'Parrot';
[eval].ts:5:1 - error TS2322: Type '"Parrot"' is not assignable to type '"Cat" | "Dog" | "Rabbit"'.

文字列リテラル型は文字列 enum と比べてもシンプルに記述できて扱いやすく、JavaScript へのコ ンパイル後のコードもより短くなるというメリットがある
リテラル型には文字列だけじゃなく、値が数値に限定された『数値リテラル型』もある。

タプル型

TypeScript には個々の要素の型と、その順番や要素数に制約を設けられる特殊な配列

const charAttrs: [number, string, boolean] = [1, 'patty', true];

タプルの定義にもレストパラメータが使える

const spells: [number, ...string[]] = [7, 'heal', 'sizz', 'snooz'];

2021 年 2 月リリースの TypeScript 4.2 で導入されたばかりのものなので、使うとき はバージョンに気をつけて

タプルの使い方

> const userAttrs: [number, string, boolean] = [1, 'patty', true]; 
> const [id, username, isAdmin] = userAttrs;
>console.log(id,username,isAdmin); //1 patty true

any 型

any で定義された変数は、その名のとおりいかなる型の値でも受けつけるようになる

> let val: any = 100; 100
> val = 'buz';
> val = null;

any で定義するとそこだけ JavaScript に差し戻すようなものといえる

unknown型

any の型 安全版で、任意の型の値を代入できる点は同じ、それ自体は何のプロパティもプロトタイプメソッドも持たない型

never型

何者も代入できない型

関数の型定義

TypeScriptではコンパイラオプションに noImplicitAnyが指定されてないと、引数の型定義がなくても暗黙の内にany型があてがわれてコンパイルが通ってしまうので、まずは設定ファイルの tsconfig.json でそのオプションを有効に

TypeScript では戻り値の型は型推論が有効な場合には省略もできるけど、引数の型は必ず指定する必要がある
何も返さない関数の戻り値型は void になる

// function declaration statement {
function add(n: number, m: number): number { return n + m;
}
console.log(add(2,4)); //6 }
// function keyword expression {
const add = function(n: number, m: number): number { return n + m;
};
console.log(add(5,7)); //12 }
// arrow function expression {
const add = (n: number, m: number): number => n + m; 
const hello = (): void => {
console.log('Hello!'); };
console.log(add(8,1)); //9
hello(); // Hello! }

引数 と戻り値をまとめて定義する方法もある。関数を『呼び出し可能オブジェクト(Callable Object)』 として定義するもの

// callable object type {
interface NumOp {
(n: number, m: number): number;
}
const add: NumOp = function (n, m) { return n + m;
};
const subtract: NumOp = (n, m) => n - m;
console.log(add(1, 2)); // 3
console.log(subtract(7,2)); //5 }
// in-line {
const add: (n: number, m: number) => number = function (n, m) { return n + m;
};
const subtract: (n: number, m: number) => number = (n, m) => n - m;
console.log(add(3, 7)); // 10
console.log(subtract(10, 8)); // 2 }

前者がインターフェースとして呼び出し可能オブジェクトを定義し、それを関数式に適用したも の。後者はそれをアロー型アノテーションによってインラインで行ってる

関数の型宣言にジェネリクスを用いる記法

> const toArray = <T>(arg1: T, arg2: T): T[] => [arg1, arg2]; 
> toArray(8, 3);
[8,3]
> toArray('foo', 'bar'); [ 'foo', 'bar' ]
> toArray(5, 'bar');
[eval].ts:4:12 - error TS2345: Argument of type '"bar"' is not assignable to parameter of type
'number'.

型引数(Type Parameter)
数に渡す引数と同じで、任意の型を <> によって引数と して渡すことで、その関数の引数や戻り値の型に適用できるようになる
最初の例では T は 型推論によって number、次の例では string になってる
最後のは引 数 arg1 と arg2 の型が任意のものに統一されてないのでエラー

データの型に束縛されないよう型を抽象化してコードの再利用性を向上させつつ、 静的型付け言語の持つ型安全性を維持するプログラミング手法を『ジェネリックプログラミング
(Generic Programming)』と呼ぶ。

そして型引数を用いて表現するデータ構造のことを『ジェネリクス(Generics)』っていう

TypeScript でも可変長引数はちゃんと型安全に扱える

> const toArrayVariably = <T>(...args: T[]): T[] => [...args]; > toArrayVariably(1, 2, 3, 4, 5);
[1,2,3,4,5]
> toArrayVariably(6, '7', 8);
[eval].ts:3:20 - error TS2345: Argument of type '"7"' is not assignable to parameter of type
'number'.

TypeScript でのクラスの扱い

TypeScript のクラスと JavaScript のク ラスは、一見似ているようで異なる部分がそこそこある。

class Rectangle {
readonly name = 'rectangle'; 
sideA: number;
sideB: number;
constructor(sideA: number, sideB: number) { 
this.sideA = sideA;
this.sideB = sideB;
}
getArea = (): number => this.sideA * this.sideB; }

TypeScript ではメンバー変数は、クラスの最初で その宣言をしておく必要がある

『プロパティ初期化子(Property Initializer)』という機能
コンストラクタに引 数がないクラスでは、インスタンスの初期化をこれだけで済ませてコンストラクタを省略できる

宣言時に readonly 修飾子を付けることで、そのメンバー変数を変更不可にもできる

『アクセス修飾子(Acdess Modifier)』
宣言時につけることでそのメンバーのアクセシビリティをコントロールできる
アクセス修飾子のバリエーションは次のとおり
・public ...... 自クラス、子クラス、インスタンスすべてからアクセス可能。デフォルトではすべ てのメンバーがこの public になる

・protected ...... 自クラスおよび子クラスからアクセス可能。インスタンスからはアクセス不可

・private...... 自クラスからのみアクセス可能。子クラスおよびインスタンスからはアクセス不可

TypeScript でのクラスの扱い

今日のオブジェクト指向プログラミングでは、『継承よりも合成Compostion over Inheritance)』のスタイルが優勢になってる

class Square extends Rectangle { 
readonly name = 'square'; 
side: number;
constructor(side: number) { 
super(side, side);
} 
}

前者の継承で書いてるほう
Square は暗黙の内に不必要な公開メンバー変数 sideA と sideB まで 継承してしまっていて、それがのちのちバグを生む芽になりかねない。また getArea() メソッドが 完全に共有されているため、親クラスの実装を不用意に変更できず、継続的なコード改善の障害に なる。つまり保守性が悪い

class Square {
readonly name = 'square'; 
side: number;
constructor(side: number) { 
this.side = side;
}
getArea = (): number => new Rectangle(this.side, this.side).getArea(); }

後者の合成の例では、Rectangle クラスを独立したただの部品として扱ってる
ただその API としての入力と出力の仕様を知って さえいればいい。そして依存がないゆえに Rectangle クラスの内部の変更に Square クラスが影響さ
れることはない。個々のモジュールの独立性が高く、より保守性にすぐれたコード

React でもコンポーネントをクラスで作成するときは、継承を避けるよう公式ドキュメントに書 かれてる

クラスの 2 つの顔

クラスの型を抽象化して定義する方法が、TypeScript には 2 つある。

abstract 修飾子 を用いて抽象クラスを定義するもの

継承そのものがダメというより、抽象クラスはその定義に実装を含むことができてしまうからだ よ。避けるべきは実装を伴った継承で、実装を伴わずに型だけを適用したいの。そこで 2 つめの選択肢、インターフェースを使う

interface Shape {
readonly name: string;
getArea: () => number; 
}

interface Quadrangle { 
sideA: number; 
sideB?: number; 
sideC?: number; 
sideD?: number;
}

class Rectangle implements Shape, Quadrangle { 
readonly name = 'rectangle';
sideA: number;
sideB: number;

constructor(sideA: number, sideB: number) { 
this.sideA = sideA;
this.sideB = sideB;
}
getArea = (): number => this.sideA * this.sideB; }

JavaScript では関数は第一級オブジェクトでプロパティの値にできるから、インターフェー スでもそのまま関数の型を定義すれば、それがそのメンバーメソッドの型になる

Quadrangle インターフェースの getArea の定義にアロー構文を使ったけど、getArea(): number という書き方もできる
そして実はこの 2 つの定義のやり方には微妙に差分がある
アロー構文だと『オーバーロード(Overload)』ができない

class Point { 
x: number = 0; 
y: number = 0;
}

const pointA = new Point();
const pointB: Point = { x: 2, y: 4 };

interface Point3d extends Point { 
z: number = 0;
}
const pointC: Point3d = { x: 5, y: 5, z: 10 };

インターフェースはクラスみたいに extendsで拡張できる

TypeScript でクラスを定義すると、実際には 2 つの宣言が同時に実行されてる
ひとつ はそのクラスインスタンスのインターフェース型宣言。もうひとつはコンストラクタ関数 の宣言

クラスは TypeScript にとって、型でもあり関数でもあるという二重の存在

型エイリアス VS インターフェース

インターフェースはオブジェクトとクラスの型が定義できる

TypeScript にはもうひとつ、任意の型に別名を与えて再利用できる型エイリアス(Type Alias)というものがある

type Unit = 'USD' | 'EUR' | 'JPY' | 'GBP';

type TCurrency = { 
unit: Unit; 
amount: number;
};

interface ICurrency { 
unit: Unit; 
amount: number;
}

const priceA: TCurrency = { unit: 'JPY', amount: 1000 };
const priceB: ICurrency = { unit: 'USD', amount: 10 };

新しい型を作成しているのではなく、無名の文字列リテラル型 にそれを参照するための別名 Unit を与えてる
コンパイラはそれを区別してる

TCurrency のほうは構造まで表示されてますけど、ICurrency は名前だけしか表示されませんね。 このちがいって?
これは型そのものに名前がついてるかどうかによるちがい
インターフェース文は型の宣言 なので、その型には本来の名前が与えられる。いっぽう型エイリアスの構文はすでに無名で作られてしまった型に参照のための別名を与えているものなので、その型には本来の名前がないまま
セミコロンの有無も同様で、関数宣言とインタ ーフェース構文はブロック {} で終わる文なのでセミコロンが不要。関数式と型エイリアスの構文 は最終的に代入文になるのでセミコロンが必要になる

インターフェースは、よくいえば拡張に対してオープンな性質がある

interface User { 
name: string;
}

interface User {
age: number; 
}

interface User {
species: 'rabbit' | 'bear' | 'fox' | 'dog';
}

const rolley: User = { name: 'Rolley Cocker', age: 8,
species: 'dog',
};

同じプロパティ値を別の型として上書き定義はできないし、他のプロパティはそのままなので再 宣言ではない
あくまで新しいプロパティの型定義が追加されていくだけ

普通に React でアプリケーショ ンを開発するのなら一貫して型エイリアスだけを使っていればいいと思う。そのほうが interface と type の構文が混在することもなくシンプルに書ける。

共用体型と交差型

TypeScript では既存の型を組み合わせて、より複雑な型を表現できる

そのひとつ、まずは共用体型(Union Types、交差型)

$ ts-node
> let id: number | string = 208239;
>id
208239
> id = 'a6ba7fb9-8435-4226-804e-387f3d2e53a7'; 
>id'
a6ba7fb9-8435-4226-804e-387f3d2e53a7'

演算子 | で型を並べることで、それらの内のいずれかの型が適用される複合的な型になる

オブジェクト型も共用体型に適用できるんだけど、これも基本は文字列リテラル型と同じで、 単にその並べられたオブジェクトの型のいずれかが適用される

typeA={ 
foo: number;
bar?: string; 
};
type B = { foo: string }; 
type C = { bar: string }; 
type D = { baz: boolean };

typeAorB=A|B; //{foo:number|string;bar?:string} 
typeAorC=A|C; //{foo:number;bar?:string}or{bar:string}
typeAorD=A|D; //{foo:number;bar?:string}or{baz:boolean}

交差型(Intersection Types、インターセクション型)
演算子 & で並べていく
共用体型が『A または B』と適用範囲を増やしていくのに対して、 交差型は『A かつ B』と複数の型をひとつに結合させるもの
用途としては、もっぱらオブ ジェクト型の合成に使われる

type A = { foo: number }; 
type B = { bar: string }; 
typeC={
foo?: number;
baz: boolean; 
};

typeAnB=A&B; //{foo:number,bar:string} 
typeAnC=A&C; //{foo:number,baz:boolean} 
typeCnAorB=C&(A|B);
// { foo: number, baz: boolean } or { foo?: number, bar: string, baz: boolean }

内部のプロパティの型がひとつずつマージされる感じ
AnC 型の foo プロパティのように、同じ型でありながら必須と省略可能が交差したら、必 須のほうが優先される
もし同じプロパティで型が共通点のないものだった場合はnever 型になる

type Unit = 'USD' | 'EUR' | 'JPY' | 'GBP'; 

interface Currency {
unit: Unit;
amount: number; }

interface IPayment extends Currency { 
date: Date;
}

type TPayment = Currency & { 
date: Date;
};

const date = new Date('2020-09-01T12:00+0900');
const payA: IPayment = { unit: 'JPY', amount: 10000, date };
const payB: TPayment = { unit: 'USD', amount: 100, date };

extends によるインターフェースの拡張と同等のことが、交差型を使えばできる
結合させた型をこうやって型エイリアスにしてしまえば、結果的には同じになる
ただ同名のプロパティが交差した場合、インターフェース拡張では互換性のない型だとコンパイル エラーになるという挙動のちがいがある

型の Null 安全性を保証する

TypeScript はデフォルトの設定ではすべての型に null と undefined を代入できてしまう
TypeScript で厳密に null や undefined を他の型から区別するためには、コンパイラオプション strictNullChecksを設定する必要がある

あえて null を許容したい場合は共用体型で明示的に表現する

> let foo: string | null = 'fuu';
> foo = null;
type Resident = { 
familyName: string; 
lastName: string; 
mom?: Resident;
};

const getMomName = (resident: Resident): string => resident.mom.lastName; 
const patty = { familyName: 'Hope-Rabbit', lastName: 'patty' };
getMomName(patty);

getMomName を定義してる行、resident.mom のところにエラーがでる
Resident 型の mom プロパティは省略可能なので undefined へのアクセスになる可能性があって、 コンパイルするまでもなく VS Code が教えてくれてる

- const getMomName = (resident: Resident): string => resident.mom.lastName;
+ const getMomName = (resident: Resident): string => resident.mom!.lastName

プロパティアクセスの前に ! が追加
非 Null アサーション演算子(Non-Null Assertion Operator)といって、『ここには絶対に null も undefined も入りませんよ』とコンパイラを強引に黙らせるもの
ただこれはせっかくの null 安 全性を壊すもので、実際に値が null や undefined だったら実行時エラーになる。だからよっぽどの 保証がない限り使うべきじゃない

型表現に使われる演算子

typeof 演算子

型のコンテキストで用いると変数から型を抽出

console.log(typeof100); //'number'

const arr = [1, 2, 3]; 
console.log(typeofarr); //'object'

type NumArr = typeof arr; 
const val: NumArr = [4, 5, 6];
const val2:NumArr=['foo','bar','baz']; //compileerror!

NumArr は number[] だから、この型を適用した変数に文字列の配列を入れようとする とコンパイルエラーになる
自分でいちいち型を定義しなくても、型推論で既存の変数から抜き出せる

in 演算子

constobj={a:1,b:2,c:3};
console.log('a' in obj); // true 
for(constkeyinobj){console.log(key);} //abc

type Fig = 'one' | 'two' | 'three'; 
type FigMap = { [k in Fig]?: number };
const figMap: FigMap = { 
 one: 1,
 two: 2,
 three: 3, 
};
figMap.four=4; //compileerror!

通常の式では指定した値がオブジェクトのキーとして存在するか どうかの真偽値を返す
for...in 文ではオブジェクトからインクリメンタルにキーを抽出する のに使われる
型コンテキストでは、列挙された型の中から各要素の型の値を抜き出して マップ型(Mapped Types)というものを作る

keyof演算子

通常の式では使えず、型コンテキストのみで用いられる演算子
オブジェクトの型からキーを抜き出してくる

const permissions = { 
 r: 0b100,
 w: 0b010,
 x: 0b001,
};

type PermsChar=keyoftypeofpermissions; //'r'|'w'|'x'
const readable: PermsChar = 'r';
const writable: PermsChar = 'z'; // compile error! 

typeof と合わせると、既存のオブジェクトからキーの型を抽出できる

インデックスアクセス演算子 []

const permissions = { 
 r: 0b100 as const, 
 w: 0b010 as const, 
 x: 0b001 as const,
};

type PermsChar = keyof typeof permissions; // 'r' | 'w' | 'x'
typePermsNum=typeofpermissions[PermsChar]; //1|2|4

キーの型を渡すとプロパティ値の 型が返ってくる

Const アサーション(Const Assertions)

as const の構文
定数としての型注釈を付与する

条件付き型とテンプレートリテラル型

extends キーワ ードは型引数の表現にも適用できる

const override = <T, U extends T>(obj1: T, obj2: U): T & U => ({
 ...obj1,
 ...obj2, 
});

override({a:1},{a:24,b:8}); //{a:24,b:8}
override({ a: 2 }, { x: 73 }); // compile error!

ここでの extends は、関数 override() の第 2 引数 obj2 の型を定義している型引数 U が第 1 引数 の型 obj1 の型 T と同じか拡張したものでなければならないことを示唆するもの
その条件 に従わない引数を渡そうとするとコンパイルで弾かれる

条件付き型(Conditional Types)

extends キーワードは、三項演算子を併用することで任意の条件による型の割り振りができる
型 T が 型 U を拡張していた場合は型 X を、それ以外の場合は型 Y となる型の記述

T extends U ? X:Y

これはオブジェクトの型から任意のプロパティの型を抽出したりするとき なんかに使える

type User = { id: unknown };
type NewUser = User & { id: string }; 
type OldUser = User & { id: number }; 
type Book = { isbn: string };

type IdOf<T> = T extends User ? T['id'] : never;

type NewUserId = IdOf<NewUser>; //string 
type OldUserId = IdOf<OldUser>; //number
type BookId = IdOf<Book>; // never

infer

type Flatten<T> = T extends Array<infer U> ? U : T;

const num = 5;
const arr = [3, 6, 9]; 

type A= Flatten<typeof arr>; //number
type N= Flatten<typeof num>; //number

型 T が何らかの型の配列だった場合、その配列の中身の型を infer U で型 U として 取得し、出力の型として使ってる。配列じゃなかった場合はそのままその型が出力される

テンプレートリテラル型(Template Literal Types

JavaScript のテンプレートリテラルによる文字列を型として扱うことができる

type DateFormat = `${number}-${number}-${number}`; 

const date1: DateFormat = '2020-12-05';
const date2:DateFormat='Dec.5,2020'; //compileerror!

組み込みユーティリティ型

TypeScript ではユーティリティ型を最初から言語レベルでいくつも提供してくれてる
よく使われそうなものを紹介

各プロパティの属性をまとめて変更する 類 のもの

・Partial ...... T のプロパティをすべて省略可能にする
・Required ...... T のプロパティをすべて必須にする
・Readonly ...... T のプロパティをすべて読み取り専用にする

オブジェクトの型からプロパティを取捨選 択する性質のユーティリティ型

・Pick ...... T から K が指定するキーのプロパティだけを抽出する
・Omit ...... T から K が指定するキーのプロパティを省く

type Todo = {
 title: string; 
 description: string; 
 isDone: boolean;
};

type PickedTodo = Pick<Todo, 'title' | 'isDone'>;
type OmittedTodo = Omit<Todo, 'description'>;

列挙的な型を加工するユーティリティ型

・Extract ...... T から U の要素だけを抽出する
・Exclude ...... T から U の要素を省く

type Permission = 'r' | 'w' | 'x';
type RW1 = Extract<Permission, 'r' | 'w'>;
type RW2 = Exclude<Permission, 'x'>;

任意の型から null と undefined だけを省いて null 非許容にするためのユーティリティ型

・NonNullable ...... T から null と undefined を省く

type T1 = NonNullable<string | number | undefined>; 
type T2 = NonNullable<number[] | null | undefined>;

const str:T1=undefined; //compileerror!
const arr: T2 = null; // compile error!

列挙タイプの型をキーとしたオブジェクト の型を作成するもの

・Record ...... K の要素をキーとしプロパティ値の型を T としたオブジェクトの型を作成する

type Animal = 'cat' | 'dog' | 'rabbit'; 
type AnimalNote = Record<Animal, string>;

const animalKanji: AnimalNote = { 
 cat: '',
 dog: '',
 rabbit: '',
};

関数を扱うユーティリティ型

・Parameters ...... T の引数の型を抽出し、タプル型で返す
・ReturnType ...... T の戻り値の型を返す

const f1 = (a: number, b: string) => { console.log(a, b); };
const f2 = () => ({ x: 'hello', y: true });

type P1=Parameters<typeoff1>; //[number,string] 
type P2=Parameters<typeoff2>; //[] 
type R1=ReturnType<typeoff1>; //void
type R2=ReturnType<typeoff2>; //{x:string;y:boolean}

今となっては正直 React 開発で使う機会はあんまりないかも

文字列リテラル型と組み合わせて便利に使える型

・Uppercase ...... T の各要素の文字列をすべて大文字にする
・Lowercase ...... T の各要素の文字列をすべて小文字にする
・Capitalize ...... T の各要素の文字列の頭を大文字にする
・Uncapitalize ...... T の各要素の文字列の頭を小文字にする

type Company = 'Apple' | 'IBM' | 'GitHub';

type C1 = Lowercase<Company>; // 'apple' | 'ibm' | 'github' 
type C2 = Uppercase<Company>; // 'APPLE' | 'IBM' | 'GITHUB'
type C3 = Uncapitalize<Company>; //'apple'|'iBM'|'gitHub'
type C4 = Capitalize<C3>; // 'Apple' | 'IBM' | 'GitHub'

関数のオーバーロード

TypeScript では同じ名前の関数でも型が異なる宣言を重複させることでオーバーロードができる。

class Brooch {
 pentagram = 'Silver Crystal';
}

type Compact = { 
 silverCrystal: boolean;
};

class CosmicCompact implements Compact { 
 silverCrystal = true;
 cosmicPower = true;
}

class CrisisCompact implements Compact { 
 silverCrystal = true;
 moonChalice = true;
}

function transform(): void;
function transform(item: Brooch): void;
function transform(item: Compact): void;
function transform(item?: Brooch | Compact): void {
 if (item instanceof Brooch) { 
  console.log('Mooncrystalpower ,makeup!!');
 } else if (item instanceof CosmicCompact) { 
  console.log('Mooncosmicpower ,makeup!!!');
 } else if (item instanceof CrisisCompact) { 
  console.log('Mooncrisis ,makeup!');
 } else if (!item) { 
  console.log('Moonprisimpower ,makeup!');
 } else { 
  console.log('Itemisfake... ');
 } 
}

transform(); transform(new Brooch());
transform(new CosmicCompact());
transform(new CrisisCompact());

as による型アサーション

開発者が型を断定して、それをコンパイラに押し付けること
型アサーションは根拠なく開発者の判断がまかりとおる、型安全性がまったく保証されない方法
型アサーションは本当に最後の手段

型アサーションは型キャストとは別物

const n=123;
const s1 = String(n);
console.log(typeof s1); string

const s2 = n as string;
[eval].ts:4:12 - error TS2352: Conversion of type 'number' to type 'string' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression
to 'unknown' first.

前半で行ってるのが型キャストで、これは異なるデータ型の値を任意の型にコンバートするもの
型アサーションはあくまでコンパイラによる型の解釈が変わるだけであって、実際の値 が変化するわけじゃない

(someValue as unknown) as SomeType のようにいったん unkonwn 型を挟む形で二重アサーションを行えばコンパイルは通ってしまう

const str = (123 as unknown) as string; 
str.split(',');
str.split(',');
^
Uncaught TypeError: n.split is not a function

型ガードでスマートに型安全を保証する

const foo: unknown = '1,2,3,4';

if (typeof foo === 'string') {
 console.log(foo.split(',')); 
}
console.log(foo.split(',')); //compileerror!

typeof によって string 型だと判断されたブロック内では、変数 foo に string のプロトタイプメ ソッドである split() が使えてる
あるスコープ内での型を保証するチェックを行う 式のことを型ガード(Type Guards)という

他の型の場合だけど、クラスのインスタンスなら instanceof が使える

class Base { common = 'common'; }
class Foo extends Base { foo = () => { console.log('foo'); } } 
class Bar extends Base { bar = () => { console.log('bar'); } }

const doDivide = (arg: Foo | Bar) => { 
 if (arg instanceof Foo) {
  arg.foo();
  arg.bar(); //compileerror! }
 else {
  arg.bar();
  arg.foo(); //compileerror! 
 }

console.log(arg.common); };
doDivide(new Foo());
doDivide(new Bar());

ユーザー定義の型ガード(User-Defined Type Guards)

type User = { username: string; address: { zipcode: string; town: string } };

const isUser = (arg: unknown): arg is User => { 
 const u = arg as User;

 return (
  typeof u?.username === 'string' &&
  typeof u?.address?.zipcode === 'string' && typeof 
  u?.address?.town === 'string'
 ); 
};

const u1: unknown = JSON.parse('{}');
const u2: unknown = JSON.parse('{ "username": "patty", "address": "Maple Town" }'); 
const u3: unknown = JSON.parse(
 '{ "username": "patty", "address": { "zipcode": "111", "town": "Maple Town" } }', 
);

[u1, u2, u3].forEach((u) => { 
 if (isUser(u)) {
  console.log(`${u.username} lives in ${u.address.town}`); }else{
  console.log("It's not User");
  console.log(`${u.username}livesin${u.address.town}`); //compileerror! 
 }
});

関数 isUser() の戻り値の型定義が arg is User という見慣れない記述になってる
型述語(Type Predicate)という表現で、この関数が true を返す場合に引数 arg の型が User であ ることがコンパイラに示唆される
これが型ガードに使える

やりすぎた型表現は可読性の低い『型パズル』と揶揄されることもあるけど、実行時エラーを防 ぐことができるとかユニットテストを削減できるといったメリットと照らし合わせて、有効に使っ ていくべき

TypeScript のインポート/エクスポート

TypeScript における import と export の書き方は JavaScript のところで説明したのとほぼ同じ
異なるのがインポートに指定するパスでの拡張子の扱い
TypeScript ではインポートの際 に読み込むファイルの拡張子を省略できる
というより、拡張子を書くとエラーになる

import bar from './bar';

次の順にモジュールを探索していく
1. src/bar.ts
2. src/bar.tsx
3. src/bar.d.ts
4. src/bar/package.json の types または typings プロパティで設定されている型定義ファイル
5. src/bar/index.ts
6. src/bar/index.tsx
7. src/bar/index.d.ts

TypeScript では、同じ名前空間の中に『変数宣言空間(Variable Declaration Space)』と『型宣言空間(Type Declaration Space)』という 2 つの宣言空間が存在していて、名前の管理が別々になっている
そのため変数や関数と型で同一の名前を持つことができる

const rate: { [unit: string]: number } = { 
 USD: 1,
 EUR: 0.9, 
 JPY: 108, 
 GBP: 0.8,
};

type Unit = keyof typeof rate; 
type Currency = {
 unit: Unit;
 amount: number; 
};

const Currency = {
 exchange: (currency: Currency, unit: Unit): Currency => {
 const amount = currency.amount / rate[currency.unit] * 
 rate[unit];
 return { unit, amount }; },
};

export { Currency };

型エイリアスとオブジェクトが、同じ名前で定義されている場合
同時に両方ともエクスポートされる

TypeScript 3.9 から『型の みのインポート(Type-Only Imports)』と『型のみのエクスポート(Type-Only Exports)』という構文が追加された。同じ名前でエクスポートされた型とオブジェクトから型だけをインポートしたり、
最初からあえて型だけをエクスポートしたりといったふうに使う

JavaScript モジュールを TypeScript から読み込む

npm のリポジトリで提供されている多くのパッケージは、たとえコードが TypeScript で書かれているものでも、TypeScript のままで配布されているものはあまりない
JavaScript にコンパイル済みのファイルと、『宣言ファイル(Declaration File)』 という TypeScript の型情報を定義したファイルをパッケージングして配布する

型定義ファイル

declare class Brooch { 
 pentagram: string;
}

declare type Compact = {
 silverCrystal: boolean; 
};

declare class CosmicCompact implements Compact {
 silverCrystal: boolean;
 cosmicPower: boolean; 
}

declare class CrisisCompact implements Compact { 
 silverCrystal: boolean;
 moonChalice: boolean;
}

declare function transform(): void;
declare function transform(item: Brooch): void; 
declare function transform(item: Compact): void;
export { transform, Brooch, CosmicCompact, CrisisCompact };

TypeScript から JavaScript モジュールをただインポートすると、実装だけがあって型がない状態になる
TypeScript のコンパイラにこういう変数とか関数がこういう型で存在してるよと教えてあげて、宣言空間にそれらを定義するための構文がこの declare

既存の JavaScript モジュールに型情報を付加する形の宣言のことを、通常の宣言と区別してアンビエン ト宣言(Ambient Declarations)っていう

型定義ファイルはどのように探索されるか

型定義ファイルのプロジェクトとの関連付けの方法は2つある

1.JavaScript ファイルと同じ階層に同じ名前で .d.ts 拡張子の型定義ファイルを置く
2.パッケージルートの package.json に型定義ファイルはこれですよと書いておくこと(Immer)
package.json で types または typings プロパティに型定義ファイルをパス付きで設定しておくと、 TypeScript がそれを見つけてくれる

"name": "immer",
"version": "4.0.2",
"description": "Create your next immutable state by mutating the current one", "main": "dist/immer.js",
"umd:main": "dist/immer.umd.js",
"unpkg": "dist/immer.umd.js",
"jsdelivr": "dist/immer.umd.js",
"module": "dist/immer.module.js",
"jsnext:main": "dist/immer.module.js",
"react-native": "dist/immer.module.js",
"types": "./dist/immer.d.ts",

3.公式が型ファイルを提供してくれていないパッケージの場合
・DefinitelyTypedを使う
・ Microsoft の『TypeSearch』で検索して使う
4.野良の型定義ファイルや自作の型定義ファイルを適用したい場合
・src/ ディレクト リに適当な名前で .d.ts ファイルを置く

TypeScript のコンパイルオプション

tsconfig.json は TypeScript プロジェクトのコンパイラ設定を保存しておくため のファイル
コンパイルが実行される際、デフォルトではプロジェクトルートから親ディレクトリ へさかのぼっていって最初に見つかった tsconfig.json ファイルが読み込まれ、そこに記述されて いる設定がコンパイラオプションとして有効になる

このファイルは create-react-app コマンドでテンプレートに TypeScript を指定してプロジ ェクトを新規作成したときにも作られてる

{
 "compilerOptions": {
 "target": "es5", 
 "lib": [
  "dom", 
  "dom.iterable", 
  "esnext"
 ],
 "allowJs": true,
 "skipLibCheck": true,
 "esModuleInterop": true, "allowSyntheticDefaultImports": true, 
 "strict": true, "forceConsistentCasingInFileNames": true, 
 "noFallthroughCasesInSwitch": true, "module": "esnext",
 "moduleResolution": "node", "resolveJsonModule": true, 
 "isolatedModules": true,
 "noEmit": true,
 "jsx": "react-jsx"
 }, 
 "include": [
  "src" 
 ]
}

strict というオプションが trueにすると次のオプションたちがまとめて有効にされる
・noImplicitAny ...... 暗黙的に any が指定されている式や宣言があればエラーになる
・noImplicitThis ...... this が暗黙的に any を表現していればエラーになる
・alwaysStrict ...... すべてのソースファイルの先頭に 'use strict' が記述されているものとみな し、ECMAScript の strict モードでパースする
・strictBindCallApply ...... bind()、call()、apply() メソッド使用時に、その関数に渡される引数 の型チェックを行う
・strictNullChecks ...... 他のすべての型から null および undefined が代入不可になる
・strictFunctionTypes ...... 関数の引数の型チェックが「共変的(Bivariant)」ではなく、「反変的(Contravariant)」に行われるようになる
・strictPropertyInitialization ...... 宣言だけで初期化されないクラスプロパティ(=メンバー変数)があるとエラーになる

strict オプションを有効にすることは、 TypeScript の公式でも推奨されてる

tsconfig.json のカスタマイズ

・target
これはコンパイル先の JavaScript のバージョンを指定するもの。CRA のデフォルト 設定では es5 になってる、ECMAScript 5 にコンパイルされる
・lib
コンパイルに含めるライブラリを指定する
React では DOM 操作は必須なのでそのライブラリ dom と dom.iterable、それから最新の ECMAScript 構文をサポートするライブラリ esnext が含まれるように設定されてる
・module
コンパイル後のモジュール構文をどのモジュールシステム形式にするか設定するもの
・noEmit
ファイルを出力しないようにするオプション、これが true になってるのは、現行の CRA によるプロジェクト設定では tsc は構文チェックしか行わず、実際の TypeScript のコンパイルは Babel が行ってるため
・jsx
JSX 構文をそのままにしておくか React の構文に書き換えるかを指定するためのオプ ション
・include プロパティ
コンパイル対象となるファイルを指定するためのもの

ひと昔前までは開発しやすくするため にそこそこ手を入れる必要があった。
でも今の設定はかなり完成度が高いので、問題なくほぼそのまま使える

+ "baseUrl": "src",
+ "downlevelIteration": true

・baseUrl
モジュールのインポートのパス指定に絶対パスを使えるようにしつつ、その起点 となるディレクトリを指定するオプション

この設定だけど、VS Code と相性が悪いのか新規に作成したファイルでは絶対パス指定を認識してくれないことがある
次のいずれかを行えば、新規ファイルと か移動直後のファイルでも絶対パスを認識してくれる
・shift + command + P(※ Windows では Shift + Ctrl + P)または F1 キーでコマンドパレットを 開き、>type を入力するとリストアップされる「TypeScript: Reload Project」を選択・実行
・コンソールから touch tsconfig.json を実行

・downlevelIteration フラグ
コンパイルターゲットが ES5 以前 に設定されている場合でも、ES2015 から導入された各種イテレータ周りの便利な記述を ES5 以下でも実行できるよううまいこと書き下してくれるオプション

参考書籍

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4