この授業では、先生が TypeScript の用途と基本文法、高度な型の応用、型保護と型ガードについて説明しました
TypeScript とは#
発展の歴史#
- 2012-10:マイクロソフトが TypeScript の最初のバージョン (0.8) をリリース
- 2014-10:Angular が TypeScript に基づく 2.0 バージョンをリリース
- 2015-04:マイクロソフトが Visual Studio Code をリリース
- 2016-05:@types/react がリリースされ、TypeScript で React を開発可能に
- 2020-09:Vue が 3.0 バージョンをリリースし、公式に TypeScript をサポート
- 2021-11:v4.5 バージョンがリリース
なぜ TypeScript なのか#
動的型は実行中に型の一致を行い、js の弱い型は実行時に暗黙の型変換を行いますが、静的型ではそうではありません。
TypeScript は静的型です:java、c/c++ など。
- 可読性の向上:構文解析に基づく TSDoc、IDE の強化
- メンテナンス性の向上:コンパイル段階で大部分のエラーを露出
- 大規模なプロジェクトでの共同作業では、より良い安定性と開発効率を得られます。
TypeScript はJS のスーパーセットです。
- すべての Js の特性と互換性があり、共存をサポートします。
- 漸進的な導入とアップグレードをサポートします。
基本文法#
基本データ型#
js ==> ts
ts の型定義方法が見えます:let 変数名: 型 = 値;
オブジェクト型#
// IBytedancer型を持つオブジェクトを作成
// Iはカスタム型(命名規則)を示し、クラスやオブジェクトと区別します
const bytedancer: IBytedancer = {
jobId: 9303245,
name: 'Lin',
sex: 'man',
age: 28,
hobby: 'swimming',
}
// IBytedancer型を定義
interface IBytedancer {
/* 読み取り専用プロパティreadonly:オブジェクト初期化外での値の設定を制約 */
readonly jobId: number;
name: string;
sex: 'man' | 'woman' | 'other';
age: number;
/* オプションプロパティ:このプロパティは存在しない可能性があります */
hobby?: string;
/* 任意プロパティ:すべてのオブジェクトプロパティがこのプロパティのサブタイプであることを制約 */
[key: string]: any; // any すべての型
}
/* エラー: "jobId"に割り当てできません。これは読み取り専用プロパティです */
bytedancer.jobId = 12345;
/* 成功:任意プロパティの下で任意のプロパティを追加できます */
bytedancer.plateform = 'data';
/* エラー:プロパティ"name"が不足していますが、hobbyは省略可能です */
const bytedancer2: IBytedancer = {
jobId: 89757,
sex: "woman",
age: 18,
}
関数型#
js:
function add(x, y!) {
return x + y;
}
const mult = (x, y) => x * y;
function add(x: number, y: number): number {
return x + y;
}
const mult: (x: number, y: number) => number = (x, y) => x * y;
// 簡略化された書き方、IMultインターフェースを定義
interface IMult {
(x: number, y: number): number ;
}
const mult: IMult = (x, y) => x * y;
形式はfunction 関数名(引数:型...):戻り値の型
です。
関数のオーバーロード#
/* getDate関数をオーバーロード、timestampは省略可能な引数 */
function getDate(type: 'string', timestamp?: string): string;
function getDate(type: 'date', timestamp?: string): Date;
function getDate(type: 'string' | 'date', timestamp?: string): Date | string {
const date = new Date(timestamp);
return type === 'string' ? date.toLocaleString() : date;
};
const x = getDate('date'); // x: Date
const y = getDate('string', '2018-01-10'); // y: string
簡略化された形式は以下の通りです:
interface IGetDate {
(type : 'string', timestamp ?: string): string; // この場所の戻り値の型をanyに変更すれば通過します
(type : 'date', timestamp?: string): Date;
(type: 'string' | 'date', timestamp?: string): Date | string;
}
/* エラー:型"(type: any, timestamp: any) => string | Date"を"IGetDate"型に割り当てることができません。
型"string | Date"を型"string"に割り当てることができません。
型 "Date"を型"string"に割り当てることができません。ts(2322) */
const getDate2: IGetDate = (type, timestamp) => {
const date = new Date(timestamp);
return type === 'string' ? date.toLocaleString() : date;
}
配列型#
typeの役割は型に新しい名前を付けることで、c++ の typedef に相当します。
/* 「型+角括弧」で表現 */
type IArr1 = number[];
/* ジェネリック表現 これらの2つが最も一般的です */
type IArr2 = Array<string | number | Record<string, number>>;
TypeScript 補足型#
- 空型:無割り当てを表します
- 任意型:すべての型のサブタイプです
- 列挙型:列挙値から列挙名への正および逆のマッピングをサポートします
/* 空型、無割り当てを表します */
type IEmptyFunction = () => void;
/* 任意型はすべての型のサブタイプです */
type IAnyType = any;
/* 列挙型:列挙値から列挙名への正および逆のマッピングをサポートします */
enum EnumExample {
add = '+',
mult = '*',
}
EnumExample['add'] === '+';
EnumExample['+'] === 'add';
enum ECorlor { Mon, Tue, Wed, Thu, Fri, Sat, Sun };
ECorlor['Mon'] === 0;
ECorlor[0] === 'Mon';
/* ジェネリック */
type INumArr = Array<number>;
Typescript ジェネリック#
ジェネリックは、以前 c++ を学んだことがあれば、c++ のものとほぼ同じです:具体的な型を事前に指定せず、使用時に型を指定する特性です。
function getRepeatArr(target) {
return new Array(100).fill(target);
}
type IGetRepeatArr = (target: any) => any[];
/* 具体的な型を事前に指定せず、使用時に型を指定する特性 */
type IGetRepeatArrR = <T>(target: T) => T[];
ジェネリックは以下のシーンでも使用できます:
/* ジェネリックインターフェース&複数のジェネリック */
interface IX<T, U> {
key: T;
val: U;
}
/* ジェネリッククラス */
class IMan<T> {
instance: T;
}
/* ジェネリックエイリアス */
type ITypeArr<T> = Array<T>;
ジェネリックは制約範囲を設定することもできます。
/* ジェネリック制約:ジェネリックが文字列であることを制限 */
type IGetRepeatStringArr = <T extends string>(target: T) => T[];
const getStrArr: IGetRepeatStringArr = target => new Array(100).fill(target);
/* エラー:型"number"の引数は型“string"の引数に割り当てることができません */
getStrArr(123);
/* ジェネリックパラメータのデフォルト型 */
type IGetRepeatArr<T = number> = (target: T) => T[]; // 構造内のデフォルト割り当てに似ています
const getRepeatArr: IGetRepeatArr = target => new Array(100).fill(target); // ここでのIGetRepeatArrは型エイリアスであり、この型エイリアスに引数を渡していません
/* エラー:型"string"の引数は型“number"の引数に割り当てることができません */
getRepeatArr('123');
型エイリアス & 型アサーション#
型アサーション#
時には、TypeScript よりも特定の値の詳細情報を理解している場合があります。通常、これは、あるエンティティが既存の型よりも正確な型を持っていることを明確に知っているときに発生します。
型アサーションを通じて、コンパイラに「信じてください、私は自分が何をしているか知っています」と伝えることができます。型アサーションは、他の言語の型変換に似ていますが、特別なデータチェックや解構は行いません。実行時の影響はなく、コンパイル時にのみ機能します。TypeScript は、あなたが必要なチェックを行ったと仮定します。
let someValue: any = "これは文字列です"; let strLength: number = (someValue as string).length;
/* typeキーワードを使用してIObjArrのエイリアスタイプを定義 */
type IObjArr = Array<{
key: string;
[objKey: string]: any;
}>
function keyBy<T extends IObjArr>(objArr: Array<T>) {
/* 型を指定しない場合、resultの型は{} */
const result = objArr.reduce((res, val, key) => {
res[key] = val;
return res;
}, {});
/* asキーワードを使用してresultの型を正しい型としてアサート */
return result as Record<string, T>;
}
上記のコードにはいくつかの注意点があります:
reduce()関数は、配列内の各要素に提供されたreducer関数を実行し(昇順に実行)、その結果を単一の戻り値に集約します。
構文:
arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
文字列 / 数字リテラル#
/* 特定の文字列/数字の固定値を指定することを許可 */
/* IDomTagはhtml、body、div、spanのいずれかでなければなりません */
type IDomTag = 'html' | 'body' | 'div' | 'span';
/* IOddNumberは1、3、5、7、9のいずれかでなければなりません */
type IOddNumber = 1 | 3 | 5 | 7 | 9;
高度な型#
ユニオン / インターセクション型#
書籍リストの型を作成する -> ts 型宣言は冗長で重複が多い。高度な型
const bookList = [ { // 普通のjs
author:'xiaoming',
type:'history',
range: '2001 -2021',
}, {
author:'xiaoli',
type:'Story',
theme:'love',
}]
// ts 冗長
interface IHistoryBook {
author:String;
type:String;
range:String
}
interface IStoryBook {
author:String;
type:String;
theme:String;
}
type IBookList = Array<IHistoryBook | IStoryBook>;
- ユニオン型: IA | IB; ユニオン型は値がいくつかの型のいずれかであることを示します。
- インターセクション型: IA & IB; 複数の型が重なり合って1 つの型になり、必要なすべての型の特性を含みます。
上記のコードは ts で簡略化できます:
type IBookList = Array<{
author: string;
} & ({
type: 'history';
range: string;
} | {
type: 'story';
theme: string;
})>;
/* authorはstring型のみであり、typeは'history'/'story'のいずれかで、typeが異なる場合は可能な属性が異なります */
型保護と型ガード#
- ユニオン型にアクセスする際、プログラムの安全性のために、ユニオン型の交差部分のみをアクセスできます。
interface IA { a: 1, a1: 2 }
interface IB { b: 1, b1: 2 }
function log(arg: IA | IB) {
/*エラー:型"IA | IB"には"a"プロパティが存在しません。型"IB"には"a"プロパティが存在しません
結論:ユニオン型にアクセスする際、プログラムの安全性のために、ユニオン型の交差部分のみをアクセスできます*/
if(arg.a) {
console.log(arg.a1);
} else {
console.log(arg.b1);
}
}
上記のエラーは型ガードによって解決できます:関数を定義し、その戻り値が型述語であり、有効範囲は子スコープです。
interface IA { a: 1, a1: 2 }
interface IB { b: 1, b1: 2 }
/*型ガード:関数を定義し、その戻り値が型述語であり、有効範囲は子スコープ */
function getIsIA(arg: IA | IB): arg is IA {
return !!(arg as IA).a;
}
function log2(arg: IA | IB) {
/* エラーが発生しません */
if(getIsIA(arg)) {
console.log(arg.a1);
} else {
console.log(arg.b1);
}
}
または typeof と instance を使用して判断します。
// reverse関数を実装し、配列または文字列を反転させる
function reverse(target: string | Array<any>) {
/* typof型保護*/
if (typeof target === 'string') {
return target.split('').reverse().join('');
}
/* instance型保護*/
if (target instanceof Object) {
return target.reverse();
}
}
毎回こんなに面倒ではありません。実際には、2 つの型に重複がない場合にのみ型ガードが必要です。上記の書籍の例では、自動型推論が可能です。
// logBook関数を実装し、
// 関数は書籍の型を受け取り、関連する特徴をloggerします
function logBook(book: IBookItem) {
// ユニオン型+型保護=自動型推論
if (book.type === 'history'){
console.log(book.range);
} else{
console.log(book.theme);
}
}
次に、汚染しないサブセットのマージ関数 merge を実装し、sourceObj は targetObj のサブセットでなければなりません。
function merge1(sourceObj, targetObj) { // jsでは、実装が複雑で、汚染しないためにはこうする必要があります
const result = { ...sourceObj };
for(let key in targetObj) {
const itemVal = sourceObj[key];
itemVal && ( result[key] = itemVal );
}
return result;
}
function merge2(sourceObj, targetObj) {// もしこの2つの引数の型に問題がなければ、こうできます
return { ...sourceObj, ...targetObj };
}
そして、ts で 2 つの型を作成して判断する簡単な考え方がありますが、これにより実装が冗長になり、target が source に連動して削除される必要があり、2 つの x、y を繰り返しメンテナンスすることになります。
interface ISource0bj {
x?: string;
y?: string;
}
interface ITarget0bf {
x: string;
y: string;
}
type IMerge = (source0bj: ISource0bj, target0bj: ITarget0bj) => ITargetObj;
/* 型実装が冗長:もしobj型が複雑であれば、sourceとtargetを宣言するために大量の重複が必要で
エラーが発生しやすい:もしtargetがkeyを増減させると、sourceが連動して削除する必要があります */
ジェネリックを使用して改善します。ここではいくつかの知識点が関与します。
- Partial:一般的なタスクは、既知の型の各プロパティをオプションにすることです。
TypeScript は、古い型から新しい型を作成する方法を提供します ——マッピング型。マッピング型では、新しい型は古い型の各プロパティを同じ形式で変換します。(直接書くだけで、ts が内蔵しています)
- keyofキーワードは、オブジェクト内のすべての key を含む文字列リテラルを取得することに相当します。
- inキーワードは、文字列リテラルの中の 1 つの可能性を取得することに相当し、ジェネリック P と組み合わせると、各 key を示します。
- **?** キーワードは、オブジェクトのオプションを設定することにより、サブセット型を自動的に推論することができます。
interface IMerge {
<T extends Record<string, any>>(sourceObj: Partial<T>, targetObj: T): T;
}
// Partial内部実装
type IPartial<T extends Record<string, any>> = {
[P in keyof T]?: T[P];
}
// インデックス型:キーワード[keyof]は、オブジェクト内のすべてのkeyを含む文字列リテラルを取得することに相当します。例えば
type IKeys = keyof{a: string; b: number }; // => type IKeys ="a" | "b"
// キーワード[in]は、文字列リテラルの中の1つの可能性を取得することに相当し、ジェネリックPと組み合わせると、各keyを示します。
// キーワード[?]は、オブジェクトのオプションを設定することにより、サブセット型を自動的に推論することができます。
関数の戻り値の型#
関数の戻り値の型は定義時に明確ではなく、ジェネリックを使用して表現する必要があります。
以下のコードでは、delayCall が関数を引数として受け取り、1 秒遅延して関数 func を実行し、その戻り値を promise として返します。
// delayCallの型宣言をどのように実装するか
// delayCallは関数を引数として受け取り、1秒遅延して関数を実行します
// その戻り値は、引数関数の戻り値の結果です
function delayCall(func) {
return new Promise(resolve => {
setTimeout(() => {
const result = func();
resolve(result);
}, 1000);
});
}
-
extendsキーワードは、ジェネリックが出現する際に、型推論を示します。これは三項演算子に類似した表現です。
- 例えば
T === 判断型?型A:型B
->T extends 判断型?型A:型B
- 例えば
-
inferキーワードは、型推論の中で出現し、型変数を定義することを示し、型を指すことができます。
inferの簡単な例は以下の通りです:
type ParamType<T> = T extends (...args: infer P) => any ? P : T;
この条件文
T extends (...args: infer P) => any ? P : T
の中で、infer P
は推論される関数の引数を示します。全体の文は次のように表現されます:もし
T
が(...args: infer P) => any
に割り当てられるなら、結果は(...args: infer P) => any
型の中の引数P
であり、そうでなければT
になります。- ここでは、この関数の戻り値の型を R として指すことになります。
type IDelayCall= <T extends () => any>(func: T) => ReturnType<T>;
type IReturnType<T extends (...args: any) => any> = T extends(...args: any ) => infer R ? R : any;
// 重要なキーワード[extends]は、ジェネリックが出現する際に、型推論を示し、三項演算子に類似した表現です。
// 例えばT === 判断型?型A:型B
// 重要なキーワード[infer]は、型推論の中で出現し、型変数を定義することを示し、型を指すことができます。
工程応用#
TypeScript 工程応用 ——Web#
- webpack loader関連の設定
- tsconfig.jsファイルの設定(緩和 —— 厳格、どちらでも定義可能)
- webpack を実行して起動 / パッケージ化
- loader が ts ファイルを処理する際、コンパイルと型チェックが行われます。
関連する loader:
TypeScript 工程応用 ——Node#
TSC を使用してコンパイルします。
- Node と npm をインストール
- tsconfig.js ファイルを設定
- npm を使用して tsc をインストール
- tsc を使用してコンパイルし、js ファイルを得ます。
まとめと感想#
この授業では、先生が TypeScript の用途と基本文法、JS との比較、高度な型の応用について説明し、さらに型保護と型ガードについても詳しく説明しました。最後に、TypeScript が工程でどのように応用されるかをまとめました。TypeScript は JS のスーパーセットであり、型チェック機能を追加し、コンパイル段階でコード内のエラーを露出させることができます。これは js のような動的型にはない機能であり、大規模なプロジェクトでの共同作業において、TS を使用することでより良い安定性と開発効率を得られることが多いです。
本文で引用したほとんどの内容は林皇先生の授業および ts 公式文書からのものです~