【基本】TypeScriptの型概要からReactでの型の取り扱い✨
TypeScript概要
TypeScriptはJavaScriptに静的型付けを導入した言語です。
静的型付けの利点としては主に以下の2点
- 型安全性
- ドキュメント化
型安全性
コンパイラが型チェックして、誤りがあるプログラムの場合はコンパイルエラーが発生する
const message: number = 'hello Mr.Takeuchi';
console.log(message);
コンパイルエラーが発生してくれると、ミスが早期に発見できるので嬉しいですね!
コンパイルする前であってもVScode内で、以下のように型が適切でないコードを書くと指摘してくれます
const message: number = 'hello Mr.Takeuchi';
console.log(message);
ドキュメント化
以下のコードを見て、引数としてどんな型が来て、どんな型の返り値を返すか分かりますか?
function arrayMagic(numbers) {
return numbers.map((element, index) => (index + 1) * element)
.filter(element => element % 2 === 0)
.reduce((acc, currentValue) => acc + currentValue);
}
しかし、TSで書くとどうでしょうか
function arrayMagic(array: number[]): number {
return array.map((element, index) => (index + 1) * element)
.filter(element => element % 2 === 0)
.reduce((acc, currentValue) => acc + currentValue);
}
number型が詰まった配列を渡したら動くメソッドで、返り値としてnumber型の値が返ることが分かりますね!
ということは、以下のコードが何をしているのかも予想がつきやすいですね 。
const numbers = [1, 2, 3, 4, 5];
const result = arrayMagic(numbers);
console.log(30)
/*
①mapメソッドによって、配列内の各要素に対して、以下の処理が行われます。
要素のインデックスを取得し、index + 1を計算します。 [2, 4, 6, 8, 10]
要素とインデックスから計算された値を乗算し、新しい配列に格納します。
②filterメソッドによって、新しい配列から偶数のみをフィルタリングします。[2, 4, 6, 8, 10]
③reduceメソッドによって、フィルタリングされた配列を加算します。 30
*/
TypeScriptの型について
プリミティブ型
Boolean
let isDone: boolean = false;
isDone
はboolean型で、初期値としてfalse
が設定されています。
Number
let decimal: number = 6;
decimal
はnumber型で、初期値として6
が設定されています。
String
let color: string = "blue";
color = 'red';
color
はstring型で、初期値として"blue"
が設定されています。後に'red'
に変更されています。
Symbol
let sym1 = Symbol();
let sym2 = Symbol("key"); // オプションの文字列キー
sym1
とsym2
はSymbol型で、それぞれ新しいSymbolを生成しています。sym2
はオプションの文字列キーとして"key"
を指定しています。
シンボルは一意の識別子を作成するために使われます。Symbol()関数を呼び出すと、新しいシンボル値が作成されます。これは他のどのシンボル値とも異なります。つまり、Symbol()は常に新しい、ユニークな値を生成します。
したがって以下の結果はfalseになります。
let symbol1 = Symbol();
let symbol2 = Symbol();
console.log(symbol1 === symbol2); // false
シンボルは何のために使われるのか?
シンボルの主な用途は、オブジェクトのプロパティキーとして使うことです。シンボルをキーとして使用すると、そのプロパティは他のどんな文字列キーとも衝突することはありません。これは、シンボルが一意の値であるためです。
let symbol1 = Symbol("key1");
let symbol2 = Symbol("key2");
let obj = {
[symbol1]: "value1",
[symbol2]: "value2"
};
console.log(obj[symbol1]); // "value1"
console.log(obj[symbol2]); // "value2"
Null
let n: null = null;
n
はnull型で、値として**null
が設定されています。
Undefined
let u: undefined = undefined;
u
はundefined型で、値としてundefined
が設定されています。
特別な型
Any
let notSure: any = 4;
notSure = "maybe a string";
notSure = false;
notSure
はany型で、最初に4
というnumber型の値を持ちますが、次に"maybe a string"
というstring型の値を、そして最後にfalse
というboolean型の値を持つように変更されます。any型は型チェックを避けるために使用します。
※ 型をつけるメリットがなくなるので基本的に使うことを避ける
Void
function warnUser(): void {
console.log("This is a warning message");
}
ここでのwarnUser
はvoid型の関数で、有効な戻り値を返しません。
Never
function error(message: string): never {
throw new Error(message);
}
ここでのerror
関数はnever型で、この関数は常にエラーをスローします。したがって、この関数からは戻り値がありません。
※ void
とnever
の主な違いは、void
は関数が値を返さないことを意味するのに対して、never
は関数が終了しないか、必ずエラーをスローすることを意味します。
配列とタプル
Array
let list: number[] = [1, 2, 3];
list
はnumber型の配列で、初期値として[1, 2, 3]
が設定されています。
Tuple
let x: [string, number];
x = ["hello", 10];
x
はタプルで、最初の要素がstring型、二番目の要素がnumber型であることが定義されています。初期値として["hello", 10]
が設定されています。
Enumとリテラル型
Enum
enum Color {Red, Green, Blue}
let c: Color = Color.Green;
Color
はenumで、Red
、Green
、Blue
の3つの値を持つことが定義されています。c
はColor型で、初期値としてColor.Green
が設定されています。
Literal Types
type Easing = "ease-in" | "ease-out" | "ease-in-out";
let easing: Easing = "ease-in";
Easing
はリテラル型で、"ease-in"
、"ease-out"
、"ease-in-out"
の3つの値を持つことが定義されています。easing
はEasing型で、初期値として"ease-in"
が設定されています。
型推論と型アサーション
Type inference
let someValue = "this is a string";
let strLength = someValue.length;
someValue
の型は明示的には指定されていませんが、文字列"this is a string"
が代入されているため、TypeScriptはこれがstring型であると推論します。そのため、.length
プロパティ(文字列に特有のもの)を用いることができます。
Type assertion
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
someValue
はany型で、"this is a string"
というstring型の値が設定されています。しかし、その後の行で(<string>someValue).length
という形で型アサーションを行っています。これにより、someValue
をstring型として扱い、その.length
プロパティを取得しています。
高度な型
Union type
let value: number | string;
value = 123; // OK
value = '123'; // OK
value
はnumber型またはstring型のどちらかを取ることができます。したがって、123
や'123'
のような値を設定することが可能です。
Intersection type
type First = { x: number };
type Second = { y: number };
let value: First & Second = { x: 1, y: 2 };
value
はFirst
型とSecond
型の両方の特性を持つ必要があります。したがって、{ x: 1, y: 2 }
のように両方のプロパティを含むオブジェクトを設定することが必要です。
Type aliases
type StringOrNumber = string | number;
let sample: StringOrNumber;
sample = 123; // OK
sample = '123'; // OK
StringOrNumber
は型エイリアスで、string型またはnumber型のどちらかを表します。したがって、sample
には123
や'123'
のような値を設定することが可能です。
Generics
function identity<T>(arg: T): T {
return arg;
}
let output = identity<string>("myString");
ここでは、identity
関数はジェネリック関数で、型T
を引数として取り、同じ型T
を返します。この関数を呼び出す際には、具体的な型を指定します(ここではstring
)。
Reactで用いるTypeScriptの型
Reactの関数コンポーネントでは、propsを引数として取ります。これらのpropsに型を指定するためには、TypeScriptのインターフェースまたは型エイリアスを使用します。
// Propsを受け取らない場合
const Greeting = () => <div>こんにちは</div>
// 単一のPropsを取る場合
type GreetingProps = {
firstName: string
}
const Greeting: React.FC<GreetingProps> = ({ firstName }) => {
return <p>こんにちは、{firstName}さん</p>;
};
// 複数のPropsを取る場合
type UserProps = {
name: string,
age: number,
isLoggedIn: boolean
}
const UserStatus: React.FC<UserProps> = ({ name, age, isLoggedIn }) => {
return (
<div>
<p>名前: {name}</p>
<p>年齢: {age}</p>
<p>ログイン状態: {isLoggedIn ? 'ログイン中' : 'ログアウト中'}</p>
</div>
);
};
このようにReact.FC<受け取る型>と書くケースと以下のケースがあります。
// Propsを受け取らない場合
const Greeting = () => <div>こんにちは</div>
// 単一のPropsを取る場合
type GreetingProps = {
firstName: string
}
const Greeting = ({ firstName }: GreetingProps) => {
return <p>こんにちは、{firstName}さん</p>;
};
// 複数のPropsを取る場合
type UserProps = {
name: string,
age: number,
isLoggedIn: boolean
}
const UserStatus = ({ name, age, isLoggedIn }: UserProps) => {
return (
<div>
<p>名前: {name}</p>
<p>年齢: {age}</p>
<p>ログイン状態: {isLoggedIn ? 'ログイン中' : 'ログアウト中'}</p>
</div>
);
};
どちらを用いるかはチームのスタイルによって異なるかとは思います。
React.FC, React.VFC型を用いることに関しては最近否定的な意見を見かけます。
私はReact.FCの記法をよくみていたので、そちらに慣れてしまっていますが….
参考になるサイトを置いておきます。
【検証】React.FC と React.VFC はべつに使わなくていい説
childrenの型付け
Reactコンポーネントは、通常 children
という名前の特別な prop を受け取ります。この prop はコンポーネントがラップする JSX 要素を表します。TypeScriptでは、ReactNode
型を使ってこの prop を型付けします。
※ childrenが特定の型のみしか受け取らない場合はReactNodeとせず、直接stringなどの特定の型に制限するといった方法もあります。
type CardProps = {
title: string,
children: React.ReactNode
}
const Card: React.FC<CardProps> = ({ title, children }) => {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-content">{children}</div>
</div>
);
};
FC型を使わない場合は以下のようになりますね。
type CardProps = {
title: string,
children: React.ReactNode
}
const Card = ({ title, children }: CardProps) => {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-content">{children}</div>
</div>
);
}
戻り値の型付け
コンポーネントの戻り値の型には、JSX.Element、ReactElementがよく使われます。
JSX.Element
JSX.Element
はReactのコンポーネントの返り値として一般的に用いられる型です。それは、JSX構文を用いてReactコンポーネントを定義する際に返される要素の型を示します。
function HelloWorld(): JSX.Element {
return <h1>Hello, world!</h1>;
}
ReactElement
ReactElement
はJSX.Element
と非常に似ていますが、より具体的な情報を含むことができます。ReactElement
はReactがレンダリングするべきコンポーネントツリーの一部を表現します。ReactElement
はtype
とprops
の2つの主要なプロパティを持っています。
function HelloWorld(): React.ReactElement {
return <h1>Hello, world!</h1>;
}
上記の2つの型の違いですがReact.ReactElement
はtype
とprops
の情報を持ちます。
この型を使うことで、Reactの要素が具体的にどのようなコンポーネントを表しているのか(type
)、そのコンポーネントにどのようなプロパティが渡されているのか(props
)という情報をより明確に表現することができます。
しかし、実際にはこれらの情報はReact自体が内部で管理し、Reactの要素を作成したり操作したりする際に自動的に取り扱われます。したがって、日々のコーディング作業の中でtype
やprops
の情報を直接操作することはほとんどありません。
つまり、React.ReactElement
がtype
とprops
の情報を持つという点は、理論的な背景知識としては重要ですが、実際のコーディングにおいてはそれほど意識する必要はないと言えます。
(結論:どちらを使用しても特に変わらない<理解間違っている場合はコメントいただたいです>)
Discussion