🌱

【基本】TypeScriptの型概要からReactでの型の取り扱い✨

2023/06/07に公開

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"); // オプションの文字列キー

sym1sym2は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型で、この関数は常にエラーをスローします。したがって、この関数からは戻り値がありません。

voidneverの主な違いは、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で、RedGreenBlueの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 };

valueFirst型と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

ReactElementJSX.Elementと非常に似ていますが、より具体的な情報を含むことができます。ReactElementはReactがレンダリングするべきコンポーネントツリーの一部を表現します。ReactElementtypepropsの2つの主要なプロパティを持っています。

function HelloWorld(): React.ReactElement {
  return <h1>Hello, world!</h1>;
}

上記の2つの型の違いですがReact.ReactElementtypepropsの情報を持ちます。

この型を使うことで、Reactの要素が具体的にどのようなコンポーネントを表しているのか(type)、そのコンポーネントにどのようなプロパティが渡されているのか(props)という情報をより明確に表現することができます。

しかし、実際にはこれらの情報はReact自体が内部で管理し、Reactの要素を作成したり操作したりする際に自動的に取り扱われます。したがって、日々のコーディング作業の中でtypepropsの情報を直接操作することはほとんどありません。

つまり、React.ReactElementtypepropsの情報を持つという点は、理論的な背景知識としては重要ですが、実際のコーディングにおいてはそれほど意識する必要はないと言えます。

(結論:どちらを使用しても特に変わらない<理解間違っている場合はコメントいただたいです>)

Discussion