普段はフロントエンドエンジニアとしてNuxt.js/vue.js/TypeScriptを
中心に開発業務を行なっています。
今回は業務中に学んだTypeScriptにおける様々な型の指定方法や操作方法について記述していきたいと思います。
Discriminated Union
Union型の場合、そのプロパティを使用してUnion型のメンバを判別することが出来ます。
判別用のプロパティに対して、型チェックを行い、型の絞り込みを行います。
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
type Shape = Square | Rectangle;
const area = (s: Shape) => {
if (s.kind === "square") {
return s.size * s.size;
}
else {
return s.width * s.height;
}
}
keyof Typeof
keyofとTypeofについてよく忘れてしまうので、ここにまとめておきます。
keyof
まずはkeyofから。
keyofはオブジェクトからプロパティ名(key名)を取得する演算子です。
keyof Personを指定することでkey名のnameを得ることができます。
type Person = {
name: string;
};
type PersonKey = keyof Person; // name
プロパティが複数あるオブジェクトにkeyofを指定した場合は、ユニオン型を取得することができます。
type Book = {
title: string;
price: number;
rating: number;
};
type BookKey = keyof Book;
// 上は次と同じ意味
type BookKey = "title" | "price" | "rating";
keyofの使用ケース
keyofの使用例としては、既存のオブジェクトのプロパティ名から新たにオブジェクトを生成したい時に使用します。
export type bonusState = {
validate: boolean
introducer: string
user: number
}
export type introBonusState = keyof bonusState // validate | introducer | user
また、Object.entriesメソッドなどでkeyを回して、オブジェクトに当てはめるとき「as keyof」で型を指定することもできます。
typeof
次にtypeofについて記載していきます。
typeofは対象となる値のデータ型を表す文字列を返す演算子です。
typeof true; //=> "boolean"
typeof 0; //=> "number"
typeofをifやswitchと組み合わせることで、その条件と合致した時に処理を追加することができます。
const sample = "";
if(typeof sample === 'string') {
// 処理
}
またtypeofを使用して型の定義を行うことができます。
const study = { time: 135, amount: 35, data: 'English' };
type User = typeof study // type User = { time: number, amount: number, data: string }
const user1: typeof study = {time: '45'} // 型エラーが発生
const user2: typeof study = {time: 45, amount: 25, data: 'Chemistry'} // OK
// 初期値を設定しない場合は下記の通り記載する。
let user3!: typeof study;
typeofの使用例
既に定義してある変数から同じ型を生成や拡張したい時に、typeofを使用すると良いかと思います。
const user = {
name: 'Nick',
age: 25,
memo: 'side kick'
};
type AthorUser = typeof user // type AthorUser = { name: string; age: number; memo: string; }
interface ExpansionUser extends AthorUser {
phoneNumber: string
}
const users: ExpansionUser = {
name: 'Navcy',
age: 29,
memo: 'side kick',
phoneNumber: '070-****-****'
}
keyof Typeofの併用
最後はkeyofとtypeofを併用する方法です。
keyof typeof をすることで、オプジェクトのプロパティのstringしか入力することできなくなり、型の安全性が高まります。keyfoは変数に使用することができず、typeofのみだとオブジェクトの型で認識されてしまいます。「変数」かつ「オブジェクトのプロパティ名」をunion型として利用したい場合に併用します。
const user = {
name: 'Nick',
age: 25,
memo: 'side kick'
};
const sample: keyof typeof user = 'name' // "name" | "age" | "memo"
オブジェクトからリテラル型を生成する
keyofとtypeofを併用することで、オブジェクトからリテラル型を生成することもできます。
export const CreateType = {
name: 'name',
rate: 'rate',
} as const;
export type FormType = typeof CreateType[keyof typeof CreateType]; // type FormType = 'name' | 'rate'
isとinの違いを理解する
TypeScriptにおけるisとinについて記載していきます。
is
TypeScriptにおけるisは型推論を補強するuser-defined type guard(ユーザー定義型ガード)で使用することができ、型の絞り込みを行うことができます。
isの使用例
unknown型のfooがstring型であった場合に、fooの結果をコンソールログに出力するコードがあったとします。fooがstring型であった場合、問題なく処理が実行されます。
const test = (foo: unknown) => {
if (typeof foo === "string") {
console.log('結果:', foo); // fooはstringとして推論される
}
};
ここでstring型の絞り込み処理を共通化するためにisStringメソッドを作成し、string型かどうかを判定させるとします。この場合、「'data' is of type 'unknown'」のエラーが出てしまいます。typeofの型の絞り込みは関数スコープの中で完結してしまうからです。
const isString = (data: unknown): boolean => {
return typeof data === 'string';
};
const test = (data: unknown) => {
if (!isString(data)) {
console.log('NG');
return
}
console.log(`OK: ${data.toUpperCase()}`); // error: 'data' is of type 'unknown'
};
ここでisを使用するとisStringの結果がtrueの場合はdataがstring型であるとコンパイラに教えることができます。
const isString = (data: unknown): data is string => { <== isを追加
return typeof data === 'string';
};
const test = (data: unknown) => {
if (!isString(data)) {
console.log('NG');
return
}
console.log(`OK: ${data.toUpperCase()}`);
};
これは型の絞り込みの時にも使用することができます。
User、Hobbyのユニオン型から型の絞り込みを行い、特定のプロパティがnullの時に条件分岐させて処理を行いたいとします。下記の場合、TypeScriptがUser型かHbby型かを判断することが出来ず、エラーが発生してしまいます。
type User = {
name: string,
age: number,
address: number
};
type Hobby = {
type: number,
work: string,
};
const example = (hobbyOrUser: User | Hobby) => {
if ((hobbyOrUser as User).name !== null) {
console.log(hobbyOrUser.name); // Property 'name' does not exist on type 'User | Hobby '
} else {
console.log(hobbyOrUser.work); // Property 'name' does not exist on type 'User | Hobby '
}
};
この場合は、nullを判定する箇所切り出して、isで型推論を補強することで、TypeScriptに型を明示的に示すことが出来ます。
type User = {
name: string,
age: number,
address: number
};
type Hobby = {
type: number,
work: string,
};
const isHobby = (test: Hobby | User): test is Hobby => {
return (test as User).name !== null;
};
const example = (hobbyOrUser: User | Hobby) => {
if (isHobby(hobbyOrUser)) {
console.log(hobbyOrUser.work);
} else {
console.log(hobbyOrUser.name);
}
};
in
続いてinについて記載します。inはオブジェクト内に特定のプロパティが存在するかどうかを確認することが出来ます。戻り値はboolean型となり、プロパティが存在すればtrueを存在しなければfalseが帰ります。
type User = {
name: string,
age: number,
address: number
};
type Hobby = {
type: number,
work: string,
};
const example = (hobbyOrUser: User | Hobby) => {
if ('name' in hobbyOrUser) {
console.log(hobbyOrUser.name);
} else {
console.log(hobbyOrUser.work);
}
};
Mapped Types
続いてMapped Typesについて述べていきます。Mapped Typesは「keyof T」と組み合わせて[P in keyof T]と記載することで、Tのプロパティに基づいた新たなプロパティを作成することが出来ます。
type User = {
name: string,
age: number,
address: number
};
type NewUser = {
[P in keyof User]: User[P]; // { name: string, age: number, address: number };
}
インデックス型は基本的にどのようなキーでも自由に設定可能ですが、入力の値が決まっているのであれば、keyofと併用しなくてもMapped Typesのみを使用して、型を指定しても良いかもしれません。
type SystemSupportLanguage = "en" | "fr" | "it" | "es";
type Butterfly = {
[key in SystemSupportLanguage]: string;
}; // {en: string, fr: string, ir: string, es: string}
Conditional types
Conditional typesは型定義における条件分岐を表します。構文は下記の通りです。
type MyCondition<T, U, X, Y> = T extends U ? X : Y;
Conditional typesの使用例
ベースとなる型を事前に生成しておき、string型かどうかで返す型を分岐させます。
interface StringId {
id: string;
}
interface NumberId {
id: number;
}
type Id<T> = T extends string ? StringId : NumberId;
let idOne: Id<string>; // idOne: StringId;
let idTwo: Id<number>; // idTwo: NumberId;
ユーティリティ型 (utility type)
utility typeを使用することで型から別の型を生成したり、一部のプロパティを外したりすることが出来ます。このように柔軟に型を操作できるのでinterfaceよりもtypeの方が個人的には好みです。
utility typeは種類が豊富にあり、個人的に実務で使用する頻度が高いものをピックアップしていきたいと思います。
ReturnType<Type>
ReturnTypeは関数の戻り値の型から新たな型を生成することが出来ます。
type something = ReturnType<関数型>
type T0 = ReturnType<() => string>; //type T0 = string
type T1 = ReturnType<(s: string) => void>; // type T1 = void
type T2 = ReturnType<(s: string) => {id: number, shopper_text: string}> // type T2 = {id: number, shopper_text: string}
declare function f1(): { a: number; b: string };
type T4 = ReturnType<typeof f1>; // type T4 = { a: number; b: string; }
ReturnTypeの使用例
ReturnTypeの使い所としては、関数の戻り値を型に指定することで新たに型を生成する必要がなく、誤ったオブジェクトが出力されるのを防ぐことが出来ます。
type demoType = {
name: string,
age: number
}
const sampleDemo = (firstName: string, ageDay: number): demoType => {
return {
name: firstName,
age: ageDay + 1
}
}
type sampleReturnType = ReturnType<typeof sampleDemo>
const result: sampleReturnType = sampleDemo('vaxcy', 32);
console.log('Result >>>', result)
// "Result >>>", { "name": "vaxcy", "age": 33 }
Exclude<T, U>
Exclude<T, U>はユニオン型TからUで指定した型を取り除いたユニオン型を生成することが可能です。
type Seal = "seal"
type Leat = "one" | "bit" | "little" | "endian"
// sealを除外
type Sample = Exclude<Leat , Seal > // type Sample = "one" | "bit" | "little" | "endian"
Pick<T ,key>
Pick<T, key>は型Tからkeysに指定したキーだけを含むオブジェクト型を生成します。
type User = {
surname: string;
middleName?: string;
givenName: string;
age: number;
address?: string;
nationality: string;
createdAt: string;
updatedAt: string;
};
type Person = Pick<User, "surname" | "middleName" | "givenName">;
type Person = {
surname: string;
middleName?: string;
givenName: string;
};
Omit<T, key>
Omit<T, key>はオブジェクト型Tからkeysで指定したプロパティを除いたオブジェクト型を生成します。Pickとは逆の動きをします。
type User = {
surname: string;
middleName?: string;
givenName: string;
age: number;
address?: string;
nationality: string;
createdAt: string;
updatedAt: string;
};
type Optional = "age" | "address" | "nationality" | "createdAt" | "updatedAt";
type Person = Omit<User, Optional>;
type Person = {
surname: string;
middleName?: string;
givenName: string;
};
Record<Keys, Type>
Record<Keys, Type>はオブジェクトのプロパティーのキーと値を指定するユーティリティを指定することができます。
type StringNumber = Record<string, number>;
const value: StringNumber = { a: 1, b: 2, c: 3 };
今回はここまでとなります。
最後まで読んでくださりありがとうございました。
■ 参考文献