たかぎとねこの忘備録

プログラミングに関する忘備録を自分用に残しときます。マサカリ怖い。

ReactNavigationで他のナビゲーターのスクリーンに移動したり型チェックを行いたい

企画

次のような画面構成でスクリーンを構築したい。

Bottom Tabs Navigatorにスクリーンの代わりにStack Navigatorを2つ配置して、それぞれのStack Navigatorに2つずつスクリーンを配置する感じ。

そして、それぞれのスクリーンから各ナビゲーションのスクリーンに遷移したい。

これを実現するためにReact Navigationの機能を使っていくが、ルート名とパラメータの型チェックを行いたいのでルート名とルートのパラメータのマッピングを持つオブジェクト型を作成する。 そのために、ルートにtypes.tsxを作成する。

作成する型はそれぞれ次の通り

  • RootStackParamList
    • RootBottom Tab Navigatorを配置するStack Navigator用の型
  • RootTabParamList
    • TabOneTabTwoStack Navigatorを配置するBottom Tab Navigator用の型
  • TabOneStackParamList
  • TabTwoStackParamList
  • TabOneStackScreenProps
  • TabTwoStackScreenProps

TL;DR

最終的なプロジェクトはこちらから確認できます。

github.com

プロジェクトを作成する

Expo CLIを使ってReact Nativeのプロジェクトを準備する。

今回はテンプレートとしてexpo-template-blank-typescriptを使用する。

github.com

アプリ名はexpo-minimum-appとかなんとかにしておく。

expo init --template expo-template-blank-typescript

必要なパッケージのインストール

今回の実装に必要なパッケージは次の通り。

  • @react-navigation/bottom-tabs
  • @react-navigation/native
  • @react-navigation/native-stack
yarn add @react-navigation/bottom-tabs @react-navigation/native-stack

次のパッケージはExpo CLIを使ってインストールする。

  • react-native-screens
  • react-native-safe-area-context
expo install react-native-screens react-native-safe-area-context

RootStackParamList

ナビゲーター(Stack Navigator)にネストされたナビゲーター(Bottom Tab Navigator)に含まれているスクリーンのパラメーターの型を定義するにはNavigatorScreenParamsを使用する。

// types.tsx
import { NativeStackScreenProps } from "@react-navigation/native-stack";
...

export type RootStackParamList = {
  Root: NavigatorScreenParams<RootTabParamList> | undefined;
};

RootTabParamList

RootStackParamListで参照されているRootTabParamListBottom Tab Navigatorであり、その内部でそれぞれのタブでStack Navigatorをネストしているので、ここでもNavigatorScreenParamsを使用する。

// types.tsx

...

export type RootTabParamList = {
  TabOne: NavigatorScreenParams<TabOneStackParamList>;
  TabTwo: NavigatorScreenParams<TabTwoStackParamList>;
};

TabOneStackParamListとTabTwoStackParamList

今回実装するスクリーンでは遷移時にパラメーターを必要としないでの、undefinedとして定義する。

// types.tsx

...

export type TabOneStackParamList = {
  Alpha: undefined;
  Beta: undefined;
};

export type TabTwoStackParamList = {
  Gamma: undefined;
  Delta: undefined;
};

TabOneStackScreenProps

TabOneTabTwoStack Navigatorに配置したスクリーンコンポーネントで受け取ることができるPropsの型を定義するために、CompositeScreenPropsBottomTabScreenProps、そしてNativeStackScreenPropsを使用する。

// types.tsx

...

export type TabOneStackScreenProps<Screen extends keyof TabOneStackParamList> =
  CompositeScreenProps<
    BottomTabScreenProps<TabOneStackParamList, Screen>,
    CompositeScreenProps<
      BottomTabScreenProps<RootTabParamList>,
      NativeStackScreenProps<RootStackParamList>
    >
  >;

export type TabTwoStackScreenProps<Screen extends keyof TabTwoStackParamList> =
  CompositeScreenProps<
    BottomTabScreenProps<TabTwoStackParamList, Screen>,
    CompositeScreenProps<
      BottomTabScreenProps<RootTabParamList>,
      NativeStackScreenProps<RootStackParamList>
    >
  >;
CompositeScreenProps

スタックナビゲーターの中にボトムタブナビゲーターがネストされているような状況の時に、複数のナビゲーターからの型を簡単に組み合わせるために使用するのがCompositeScreenPropsである。

最初のパラメーターはプライマリーナビゲーション型とよばれ、このPropsが渡されるスクリーンを管理するナビゲーターの型である。このナビゲーター型をBottomTabScreenPropsなどで指定する場合に、BottomTabScreenProps<TabOneStackParamList, Screen>のようにその第二引数にスクリーン名を渡す必要がある。

2つ目のパラメーターはそのセカンダリーナビゲーション型とよばれ、プリまりナビゲーション型をネストしている親ナビゲーターの型である。

上記のようにStack > Bottom Tab > Stackのように複数の親ナビゲーターにネストされている場合はセカンダリナビゲーション型もCompositeScreenPropsを使ってネストする必要がある。

スクリーン用のPropsをtypes.tsxでエクスポートする

上記で定義したTabOneStackParamListTabTwoStackParamListを使ってそれぞれのスクリーンのPropsをtypes.tsxで定義してエクスポートする。

...

// TabOne Screens

export type AlphaScreenProps = TabOneStackScreenProps<"Alpha">;
export type BetaScreenProps = TabOneStackScreenProps<"Beta">;

// TabTwo Screens

export type GammaScreenProps = TabTwoStackScreenProps<"Gamma">;
export type DeltaScreenProps = TabTwoStackScreenProps<"Delta">;

実際に使用するときはtypes.tsxからそれぞれの型をインポートして使用する。

// screens/TabOne/AlphaScreen.tsx
...
import { AlphaScreenProps as Props } from "../../types";

export function AlphaScreen({ navigation }: Props) {
  ...
}

実際に定義した型を使用してアプリを構築する

まずはnavigation/index.tsxを作成する。React Navigationの機能を使用するには、アプリ全体をNavigationContainerで一度だけ囲む必要があるので、その処理をここで行う。

// navigation/index.tsx

import { NavigationContainer } from '@react-navigation/native';
import { RootNavigator } from './RootNavigator';

export function Navigation() {
  return (
    <NavigationContainer>
      <RootNavigator />
    </NavigationContainer>
  )
}

Bottom Tabs NavigatorをネストするルートのStack Navigatorをnavigation/RootNavigator.tsxに実装する。

// navigation/RootNavigator.tsx

import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { RootStackParamList } from "../types";
import { BottomTabNavigator } from "./BottomTabNavigator";

const Stack = createNativeStackNavigator<RootStackParamList>();

export function RootNavigator() {
  return (
    <Stack.Navigator>
      <Stack.Screen
        name="Root"
        component={BottomTabNavigator}
        options={{ headerShown: false }}
      />
    </Stack.Navigator>
  );
}

TabOneTabTwoStack Navigatorを配置するためのBottom Tabs Navigatorを実装する。

// navigation/BottomTabNavigator.tsx

import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { RootTabParamList } from "../types";
import { TabOneNavigator } from "./TabOneNavigator";
import { TabTwoNavigator } from "./TabTwoNavigator";

const BottomTab = createBottomTabNavigator<RootTabParamList>();

export function BottomTabNavigator() {
  return (
    <BottomTab.Navigator initialRouteName="TabOne">
      <BottomTab.Screen name="TabOne" component={TabOneNavigator} />
      <BottomTab.Screen name="TabTwo" component={TabTwoNavigator} />
    </BottomTab.Navigator>
  );
}

最後にTabOneTabTwoという名前を担うStack Navigatorを実装する。まずはTabOneNavigatorから。

import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { TabOneStackParamList } from "../types";
import { AlphaScreen } from "../screens/TabOne/AlphaScreen";
import { BetaScreen } from "../screens/TabOne/BetaScreen";

const TabOneStack = createNativeStackNavigator<TabOneStackParamList>();

export function TabOneNavigator() {
  return (
    <TabOneStack.Navigator
      screenOptions={{
        headerTitleAlign: "center",
      }}
    >
      <TabOneStack.Screen
        name="Alpha"
        component={AlphaScreen}
      />
      <TabOneStack.Screen
        name="Beta"
        component={BetaScreen}
      />
    </TabOneStack.Navigator>
  )
}

次にTabTwoNavigatorを実装する。

import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { TabTwoStackParamList } from "../types";
import { GammaScreen } from "../screens/TabTwo/GammaScreen";
import { DeltaScreen } from "../screens/TabTwo/DeltaScreen";

const TabTwoStack = createNativeStackNavigator<TabTwoStackParamList>();

export function TabTwoNavigator() {
  return (
    <TabTwoStack.Navigator
      screenOptions={{
        headerTitleAlign: "center",
      }}
    >
      <TabTwoStack.Screen name="Gamma" component={GammaScreen} />
      <TabTwoStack.Screen name="Delta" component={DeltaScreen} />
    </TabTwoStack.Navigator>
  );
}

スクリーンでは、types.tsxで定義したそれぞれのスクリーン用の型をインポートして使用する。例としてAlphaScreenコンポーネントを実装してみる。その他のスクリーンも同じようにして実装する。

// screens/TabOne/AlphaScreen.tsx

import { useNavigation } from "@react-navigation/native";
import { Box, Center } from "native-base";
import { Button } from "native-base";
import { useCallback } from "react";
import { AlphaScreenProps as Props } from "../../types";

export function AlphaScreen({ navigation }: Props) {
  const nav = useNavigation();
  nav.navigate("Root", {
    screen: "TabOne",
    params: {
      screen: "Alpha",
    },
  });
  const handleNavigateToBetaButton = useCallback(() => {
    navigation.navigate("Beta");
  }, []);
  const handleNavigateToGammaButton = useCallback(() => {
    navigation.navigate("Root", {
      screen: "TabTwo",
      params: {
        screen: "Gamma",
      },
    });
  }, []);
  const handleNavigateToDeltaButton = useCallback(() => {
    navigation.navigate("Root", {
      screen: "TabTwo",
      params: {
        screen: "Delta",
      },
    });
  }, []);
  return (
    <Box flex={1}>
      <Center flex={1}>
        <Button onPress={handleNavigateToBetaButton} mb={4}>
          TabOneのBetaスクリーンに遷移します
        </Button>
        <Button
          onPress={handleNavigateToGammaButton}
          colorScheme="secondary"
          mb={4}
        >
          TabTwoのGammaスクリーンに遷移します
        </Button>
        <Button onPress={handleNavigateToDeltaButton} colorScheme="amber">
          TabTwoのDeltaスクリーンに遷移します
        </Button>
      </Center>
    </Box>
  );
}

navigation/index.tsxで実装したNavigationコンポーネントMain.tsxのMainコンポーネントレンダリングする。

// Main.tsx

import { StatusBar } from "expo-status-bar";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { Navigation } from "./navigation";

export default function Main() {
  return (
    <SafeAreaProvider>
      <Navigation />
      <StatusBar style="auto" />
    </SafeAreaProvider>
  );
}

そして、エントリーポイントであるApp.tsxでMain.tsxのMainコンポーネントをインポートしてレンダリングするようにする。

// App.tsx

import { StatusBar } from "expo-status-bar";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { Navigation } from "./navigation";

export default function Main() {
  return (
    <SafeAreaProvider>
      <Navigation />
      <StatusBar style="auto" />
    </SafeAreaProvider>
  );
}

実際の画面にするとこんな感じ。

AlphaScreenからTabOneNavigatorBetaScreen、そしてTabTwoNavigatorGammaScreenDeltaScreenに遷移する。

BetaScreenからTabOneNavigatorAlphaScreen、そしてTabTwoNavigatorGammaScreenDeltaScreenに遷移する。

GammaScreenからTabOneNavigatorAlphaScreenBetaScreen、そしてTabTwoNavigatorDeltaScreenに遷移する。

DeltaScreenからTabOneNavigatorAlphaScreenBetaScreen、そしてTabTwoNavigatorGammaScreenに遷移する。

同一ナビゲーターの兄弟スクリーンに遷移する場合はnavigation.navigateにスクリーン名を渡すだけで良い。

const handleNavigateToBetaButton = useCallback(() => {
    navigation.navigate("Beta");
  }, []);

同じBottom Tabs Navigatorに属する他のStack Navigatorのスクリーンに遷移する場合はscreenキーとparamsキーを組み合わせてオブジェクトを作成してnavigation.navigateに渡す。

const handleNavigateToGammaButton = useCallback(() => {
    navigation.navigate("Root", {
      screen: "TabTwo",
      params: {
        screen: "Gamma",
      },
    });
  }, []);

まとめ

初見だと、どの型がどれに対応していて、遷移する場合はどんな引数を渡せば良いか理解しにくいかもしれないので、ぜひこの記事を読みながら少しでも理解の手助けになってくれると嬉しい。

Coroutineを学んでみた

CoroutineはノンブロッキングプログラミングをKotlinで使えるようにした機能。非同期操作を安全に扱える機能などが含まれている。

導入

Coroutineを使うにはkotlinx.coroutinesというパッケージを使わないといけない。

build.gradle.ktsファイルを開いて、dependenciesのブロックにimplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")を追加する。

// build.gradle.kts

dependencies {
    testImplementation(kotlin("test"))
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
}

pluginsブロックにkotlin("jvm") version "1.6.21"が書かれていることを確認する。

// build.gradle.kts

plugins {
    kotlin("jvm") version "1.6.21"
    application
}

pluginsブロックで、Kotlinの最新バージョンが使われていることを確認する。

plugins {
    kotlin("jvm") version "1.6.21"
}

ノンブロッキング

Coroutineを利用するにはsuspendを使ってsuspend関数を定義しないといけない。

そして、coroutineScopeを使用してCoroutineの利用範囲を指定して、内部で、launch()にラムダを渡す。そのラムダの内部では別のsuspend関数を呼び出すことができる。

main関数にもsuspendを付与することを忘れない。

import kotlinx.coroutines.*

suspend fun main(args: Array<String>) {
    coroutineTest()
}

suspend fun coroutineTest() = coroutineScope {
    launch {
        coroutineTest2()
    }
    println("Hello World")
}

suspend fun coroutineTest2() {
    println("Nice to meet you")
}

コルーチンビルダーはcoroutineScopeも併せて3つある。

  • coroutineScope
    • ブロック内で起動したコルーチンが完了するのを待たない
  • runBlocking
    • ブロック内で起動したコルーチンが完了するまで処理を待つ。
  • withContext
    • 指定したコンテキストに実行中の処理を切り替える

runBlockingを使う際はsuspendを使用しないで定義する。

import kotlinx.coroutines.*

fun main(args: Array<String>) = runBlocking {
    println("Start Coroutine")
    launch {
        println("before 200")
        delay(200)
        println("after 200")
    }

    println("Finished Coroutine")
}

Coroutineの実行に使用されるスレッドをlaunchに引数を渡すことで指定できる。これによりlaunchのラムダにあるsuspend関数をノンブロッキングで実行することができる。

このときに渡す引数のことをディスパッチャーという。ディスパッチャーはDispatchersを通して参照できる。

  • Dispatchers.Default
    • 指定されない場合に使用されるディスパッチャー
  • Dispatchers.Main
    • OSのメインスレッド
  • Dispatchers.Unconfined
    • 実行時に最初に使用可能なスレッドを割り当てる
  • Dispatchers.IO
    • データベースへのリクエストやファイルへのアクセスなどに使われる
import kotlinx.coroutines.*

fun main(args: Array<String>) = runBlocking {
    println("Start Coroutine")
    launch(Dispatchers.IO) {
        println("before 200")
        delay(200)
        println("after 200")
    }

    println("Finished Coroutine")
}

タイムアウトを設定できる。タイムアウトで設定した時間を超えた場合はTimeoutCancellationExceptionがスローされる。

import kotlinx.coroutines.*

fun main(args: Array<String>) = runBlocking {
    println("Start Coroutine")
    launch {
        timeoutTest()
    }
    println("Finished Coroutine")
}

suspend fun timeoutTest() = coroutineScope{
    withTimeout(timeMillis = 1_000) {
        println("before 200")
        delay(2_000)
        println("after 200")
    }
}

Job

launchの戻り値であり、この戻り値を使えばCoroutineを制御できる。 例えば、キャンセルすることもできる。

import kotlinx.coroutines.*

fun main(args: Array<String>) = runBlocking {
    println("Start Coroutine")
    val job = launch(Dispatchers.IO) {
        println("before 200")
        delay(200)
        println("after 200")
    }
    job.cancel()
    println("Finished Coroutine")
}

async

Coroutineで並列処理を行う場合は、async()を使ってCoroutineを作成する。そして、async()を使って作成したCoroutineの結果を受け取る場合は戻り値に対してawait()を呼び出す。await()を使うことで、スレッドをブロッキングすることなく処理の完了を待つことができる。

import kotlinx.coroutines.*

fun main(args: Array<String>) = runBlocking {
    testAsync()
}

suspend fun returnNum(num: Int): Int {
    return num
}

suspend fun testAsync() = coroutineScope {
    val a = async { returnNum(5) }
    val b = async { returnNum(10) }
    println("a: ${a.await()}")
    println("b: ${b.await()}")
}

ChannelとFlow

スレッド間でデータの受け渡しを行う方法として主にChannelFlowがある。

Channelにおいて、データを送信する場合はsend()を利用する。受信する場合はreceive()を利用する。

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel

fun main(args: Array<String>) = runBlocking<Unit> {
    val channel = Channel<Int>()
    launch {
        sendNum(channel, 200)
    }

    launch {
        val result = channel.receive()
        println(result)
    }
}

suspend fun sendNum(ch: Channel<Int>, num: Int): Unit {
    delay(300)
    ch.send(num)
}

Flowでは、データを送信する場合にemit()を呼び出す。そして受信する際は、flow.collectにラムダを渡して、その中でitを経由して送信されたデータを参照する。

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.flow

fun main(args: Array<String>) = runBlocking<Unit> {
    val flow = flow {
        val result = returnNum(200)
        emit(result)
    }
    flow.collect {
        println(it)
    }
}

suspend fun returnNum(num: Int): Int {
    delay(300)
    return num
}

Nike Run Clubアプリのチャートコンポーネントぽいものを作ってみる

僕はランニングする時にNRCを愛用しています。

www.nike.com

このアプリのグラフをReact Native Chart Kitというライブラリで再現できないか試してみた。

github.com

まずはインストールから。

yarn add react-native-chart-kit react-native-svg

実際に実装してみる。今回使用するのはBarChartコンポーネント

<BarChart
  // onDataPointClick={(data) => console.log(data)}
  data={data}
  width={screenWidth}
  height={220}
  yAxisLabel=""
  yAxisSuffix=""
  segments={3}
  chartConfig={{
    backgroundGradientFromOpacity: 0,
    backgroundGradientToOpacity: 0,
    barRadius: 5,
    color: (opacity = 1) => `rgba(240, 157, 77, ${opacity})`,
    barPercentage: 0.7,
    fillShadowGradientFromOpacity: 1,
    fillShadowGradientToOpacity: 1,
    decimalPlaces: 0,
    propsForBackgroundLines: {
      strokeDasharray: 0,
      stroke: "#e4e3e4",
      strokeWidth: 1,
    },
    propsForHorizontalLabels: {
      stroke: "#333333",
      fontWeight: "lighter",
      fontSize: "10",
    },
    propsForVerticalLabels: {
      stroke: "#333333",
      fontWeight: "lighter",
      fontSize: "10",
    },
  }}
  verticalLabelRotation={0}
  showBarTops={false}
  fromZero={true}
  showValuesOnTopOfBars={true}
  withVerticalLabels={true}
  withHorizontalLabels={true}
/>;

僕の実力だとこのくらいまでしかできなかった。横に表示されているラベルを右に表示したかったのだけど、普通には設定できないぽくて断念した。

設定に使用した各プロパティを解説する。

data

labelsは横軸に表示されるラベルの値。datasets[0].dataの値は縦軸の値。labelsdatasets[0].dataの各要素の値は1対1で対応している。なので、要素数を合わせる必要がある。

{
  labels: ["日", "月", "火", "水", "木", "金"],
  datasets: [
    {
      data: [20, 45, 28, 80, 99, 43],
    },
  ],
}

width

グラフ全体が表示される幅の値。この値の取得方法はreact-nativeパッケージのDimensionsを使ってDimensions.get("window").widthの値を設定する。

height

チャートの高さ。

yAxisLabel

一番左に表示されるラベルの先頭に付与する値。お金を表すチャートの場合はマークを指定したりすると¥100みたいに表示される。

yAxisSuffix

一番左に表示されるラベルの末尾に付与する値。割合を表すチャートの場合は%マークを指定したりすると100%みたいに表示される。

segments

背景に表示される横線の数を指定する。僕の場合は3を指定した。

chartConfig

チャートの設定に関するオブジェクト。BarChartに限らずいろいろなコンポーネントで共通の設定オブジェクト。

backgroundGradientFromOpacity

グラフの背景色における線形グラデーションの最初の色の不透明度をnumberで指定する。0から1までを指定できる。1に近い値を指定するほど黒に近い色になる。逆に0に近い値を指定するほど白に近い色になる。

backgroundGradientToOpacity

グラフの背景色における線形グラデーションの2番目の色の不透明度をnumberで指定する。0から1までを指定できる。1に近い値を指定するほど黒に近い色になる。逆に0に近い値を指定するほど白に近い色になる。

barRadius

各バーの半径をnumberで指定する。丸みを追加できる。

color

opacity: numberを引数で受け取る関数を指定する。(opacity = 1) => `rgba(240, 157, 77, ${opacity})`,のように文字列で作成したrgba()を返す。

barPercentage

表示されるバーの横幅を0から1numberで指定する。1に近づくほど太くなり、0に近づくほど細くなる。

fillShadowGradientFromOpacity

バーのの線形グラデーションにおける最初の色(一番上からまんなかくらいまで)の不透明度をnumberで定義する。0を指定すると無色透明になり、1を指定するとはっきりと色が表示される。

fillShadowGradientToOpacity

バーのの線形グラデーションにおける2番目の色(真ん中くらいから一番下まで)の不透明度をnumberで定義する。0を指定すると無色透明になり、1を指定するとはっきりと色が表示される。

decimalPlaces

縦軸の値を、小数点第何位まで表示するかをnumberで指定する。1を指定すると小数点第1位まで表示する。0を指定すると小数点以降を表示しない。

propsForBackgroundLines

背景に表示されている線のスタイルを指定する。react-native-svgLineコンポーネントに指定するプロパティと同じ内容を指定できる。

https://github.com/react-native-svg/react-native-svg#line

strokeDasharray

破線に表示される線の間隔をnumberで指定する。0を指定すると破線ではなく直線を表現できる。

SVGのプロパティを理解してアニメーションさせてみよう | 株式会社LIG(リグ)|コンサルティング・システム開発・Web制作

stroke

破線の色をstringで16進数表記で指定できる。

strokeWidth

破線の幅をnumberで指定できる。

propsForHorizontalLabels

一番左に表示されるラベルのスタイルを設定できる。react-native-svgTextコンポーネントに設定できるプロパティの値と同じ値を指定できる。

https://github.com/react-native-svg/react-native-svg#text

stroke

テキストの色をstringで16進数表記で指定できる。

fontWeight

フォントの幅をstringで指定できる。lighterboldなどを指定できる。

fontSize

フォントのサイズをnumbderで指定できる。

propsForVerticalLabels

一番下に表示されるラベルのスタイルを設定できる。react-native-svgTextコンポーネントに設定できるプロパティの値と同じ値を指定できる。

https://github.com/react-native-svg/react-native-svg#text

stroke

テキストの色をstringで16進数表記で指定できる。

fontWeight

フォントの幅をstringで指定できる。lighterやboldなどを指定できる。

fontSize

フォントのサイズをnumbderで指定できる。

verticalLabelRotation

一番下に表示されているラベルの角度をnumbderで指定できる。正の値を指定すると右に回転する。負の値を指定すると左に回転する。

showBarTops

バーの一番上に線を表示するかどうかをbooleanで指定する。

fromZero

表示するチャートの最小値を0にするかどうかをbooleanで指定する。

showValuesOnTopOfBars

バーの一番上にそのバーの値を表示するかどうかをbooleanで指定する。

withVerticalLabels

一番下に表示されているラベルを表示するかどうかをbooleanで指定する。

withHorizontalLabels

一番左に表示されているラベルを表示するかどうかをbooleanで指定する。

Kotlinはじめてみた

積読していたKotlinの入門本をちょっとずつ読んでみる。

基礎からわかる Kotlin | 富田健二 | 工学 | Kindleストア | Amazon

環境構築

環境設定から始める。

JetBrainsの公式サイトからIntelliJ IDEAをダウンロードする。

ちなみに、僕の場合はCommunity版のApple Siliconバージョンをダウンロードした。

ダウンロード IntelliJ IDEA: JetBrains の人間工学に基づく高機能 Java IDE

プロジェクトを作成する

IntelliJ IDEAでNew Projectをクリックする。

各種設定を行って、Createボタンをクリックする。

変数

varは再代入可能な変数を宣言するときに使う。

var text: String = "Hello"
text = "World"

valは再代入不可能な変数を宣言するときに使う。

val text: String = "Hello World"

constは定数を宣言するのに使う。注意点として、トップレベルでの宣言かオブジェクト宣言であり基本型の定数を宣言するときのみ使用可能になる。

なのでList型はconstで宣言できない。

const val text: String = "Hello World"

基本型

  • Boolean
    • true
    • false
  • Char
    • 単一の文字を表す。
    • シングルクォートで囲む必要がある。
  • String
    • 文字列を扱う。
    • ダブルクォーテーションで囲む。
    • トリプルクォーテーションで囲むと複数行の文字列に対応する。これを使うときは.trimMargin()を利用する。
    • 文字列テンプレート("${x + y}"))を使うことで式や変数を文字列に展開できる。
  • Byte
    • 8ビット
  • Short
    • 16ビット
  • Int
    • 32ビット
  • Long
    • 64ビット
  • Float
    • 32ビット
  • Double
    • 64ビット
  • UByte
    • 符号なし8ビット
  • UShort
    • 符号なし16ビット
  • UInt
    • 符号なし32ビット
  • ULong
    • 符号なし64ビット
  • Any
    • すべてのクラスの親クラス
    • 特定の値の型チェックを無効にしてコンパイルを通すために利用される。
    • 極力使用しないようにする。
  • Unit
    • 型がまったくないことを表す。
    • 何も返さない関数の返り値の型として利用される。
      • fun a(text: String): Unit {}
  • Nothing
    • すべてのクラスのサブクラス
    • 存在しない値を示す。つまりインスタンスを生成できない。
    • 例外をスローする関数の返り値の型として利用される。なのでNull許容型を使わずにそういった関数を実装できる。
数値型のTips

0x0bをつかって16進数やバイナリリテラルを表現できる。

val a: Int = 0xAAAAAA
val b: Int = 0b1111

_を使うことで桁の多い数字も読みやすく表現できる。

val a: Int = 9_999 // 9999

符号なしをリテラルで表現する場合は末尾にUをつける。

val a: UInt = 100U

Null許容型

Null許容型を宣言するには普段の変数んの宣言の時に型名の横に?を付与する。

var a: String? = "Hello World"

そして、Null許容型にアクセスする際は?を使って連結することでチェーン呼び出しができる。

println(a?.length)

標準ライブラリのrequire()check()を使用すると、事前にNullでないことを確認できる。

require(a != null)
check(a != null)

他にもエルビス演算子?:)を利用することで、Nullの場合に何かをするなどの指定ができる。

// aがnullの場合に出力する
a ?: println("Nullです")

列挙型

列挙型はenumを使って宣言する。

enum class Fruit {
  Apple,
  Orange,
  Banana,
}

列挙型でordinalプロパティを利用すると0から始まる序数を取得できる。nameプロパティを利用すると、その列挙型の名前を取得できる。

Kotlinには三項演算子がない

Kotlinには三項演算子がないので代わりにif (bar) a else bのような式を利用する。

範囲の演算子

1..10を使用すると1から10までの範囲を取得できる。使い道としてはinイテレーターと一緒に利用する。

var total = 0
for (i in 1..10) {
  total += i
}
println(total)

比較演算子の違い

==は内容が一致している場合にtrueを返す。

===は二つのオブジェクトが同じ参照を指していればtrueを返す。

if式を代入する

ifは式なので、結果を変数に代入できる。

var a = if (true) {
  return 1
} else {
  return 0
}

when式とは

when式は、条件が満たされるまですでの条件を順次照合する。switch文みたいに使える。

if式と同じように代入できる。

条件式の中で、変数に代入もできる。(キャプチャ機能)

すべての条件に合致しない場合のelse -> {}も使用できる。列挙型を利用する場合は必要ない。

when(val fruit = Fruit.Apple) {
  Fruit.Apple -> {
    println("Appleです")
  }
  Fruit.Orange -> {
    println("Orangeです")
  }
  Fruit.Banana -> {
    println("Bananaです")
  }
}

forの使い方

Kotlinのfor文はイテレーター(in)を用いて反復処理を行う。

for (i in 1..10) {
  println(i)
}

example@ forのようにラベルを利用できる。break@exampleを呼び出すことで、example@ラベルがついたforを抜け出すことができるようになる。

コレクション

Listは変更不可能なコレクション。

val list = listOf('a', 'b', 'c')

MutableListは変更可能なコレクション。

val mutableList = mutableListOf('a', 'b', 'c')

mutableList += 'd'

varargを使うと動的にサイズを変更できる配列を関数の引数として宣言できる。

fun example(vararg list: String) {
  for (t in list) {
    println(t)
  }
}

example('a', 'b', 'c')
example('a', 'b', 'c', 'd')

スプレッド演算子*)を使うとコレクションを展開できる。

var list = mutableListOf('a', 'b', 'c')

var list2 = listOf('d', 'e', 'f')

var list3 = listOf(*list.toTypedArray(), *list2.toTypedArray())
println(list3)```
Set

Setは重複を防ぐことができる順序なしのコレクションである。

setOfmutableSetOfでSetを生成できる。

Map

Mapはキーと値のペアのコレクションである。

mapOfmutableMapで生成できる。

var map = mapOf(
  "name" to "Tanaka Taro",
  "age" to 14,
)

for (element in map) {
  println("${element.key} : ${element.value}")
}
配列

配列はarrayOfで生成できる。 Listは複数のオブジェクトを内部で保持するが、配列は参照型なのでアドレスを保持する。

なので、arrayOfで生成した値を出力するとアドレスが表示される。

var array = arrayOf('a', 'b', 'c')
println(array)

// [Ljava.lang.Character;@1e643faf
Sequence

Listと違ってmapfilterを利用した場合に、順序的ではなく垂直的に評価される。

つまり、Sequenceに対してmapfilterがチェーン呼出されていたら、最初の要素のみがmapfilterで処理された後に次の要素がmapfilterで処理されるといった流れを繰り返す。

sequenceOfを使って生成できる。

関数

funを使って宣言する。 引数がある場合は必ず型を指定する。戻り値がある場合は必ず型を定義する。戻り値がなく、戻り値の型を指定しない場合はUnit型になる。

関数名はcamelCaseを使用する。

fun main(args: Array<String>) {
    println(example(5))
}

fun example(a: Int): String {
    return "hello world ${a}";
}

名前付き引数を使用することもできる。引数名 = 値で引数を渡す。

fun main(args: Array<String>) {
    println(example(a = 5))
}

fun example(a: Int): String {
    return "hello world ${a}";
}

デフォルト引数も利用できる。

fun main(args: Array<String>) {
    println(example())
}

fun example(a: Int = 10): String {
    return "hello world ${a}";
}

クラス

クラスはclassを使って宣言する。

class Fruit {}

すべてのクラスはAnyを継承している。そしてクラスはデフォルトでfinal宣言になっているので、継承することはできないが、openを使用することで継承できるようになる。

public open class Fruit {
    public open fun print(text: String) {
        println(text)
    }
}

コンストラクタはconstructorを使用する。

class クラス名の横の()の間がプリマリーコンストラクタ。

fun main(args: Array<String>) {
    var user = User(text = "僕は田中太郎")
    println(user.text)
}

class User constructor(val text: String = "") {}

省略もできる。

fun main(args: Array<String>) {
    var user = User(text = "僕は田中太郎")
    println(user.text)
}

class User(val text: String = "") {}

プリマリーコンストラクタ以外に宣言されたconstructorセカンダリーコンストラクタで、this()を使用してプリマリーコンストラクタを継承する必要がある。

イニシャライザはinitを使ってブロック宣言する。コンストラクタよりも前に呼び出される。

プロパティはvarvalを使って宣言する。必ず初期化しないといけない。get()set()を用いてゲッターとセッターをカスタマイズすることができる。もちろん使わなくても良い。valで宣言したプロパティはset()を使うことはできない。ゲッターとセッターの対象の値にはfieldを使ってアクセスする。

fun main(args: Array<String>) {
    var user = User()
    println(user.id)
    println(user.name)
    user.name = "田中太郎"
    println(user.name)
}

class User {
    val id: String // 変える必要がないのでvalで宣言している
        get() {
            return "[${field}]"
        }

    var name :String // 変える可能性も考えてvarで宣言している
        get() {
            return "${field}さん"
        }
        set(value) {
            field = value
        }

    init {
        this.name = "anonymous"
        this.id = "ランダムな値"
    }

    constructor() {

    }
    constructor(name: String): this() {
        this.name = name;
        // valで宣言しているからidの値を変更することはできない
        // this.id = 'test';
    }
}

インナークラスはinnerを使って宣言する。メリットはPhone.Androidのように外部クラスを名前空間として利用できる。そして、内部クラスから外部クラスのプロパティにアクセスできるようになる。その逆はできない。

fun main(args: Array<String>) {
    var b = A().B()
    b.print()
}

class A {
    var name = "tanaka taro"
    inner class B {
        var middleName = "grape"

        fun print() {
            println("${name} ${middleName}")
        }
    }
}

クラスの代わりにobjectを利用してシングルトンクラスを生成できる。

e-words.jp

companion objectをクラス内部で使用することでクラスの中にオブジェクトを配置することもできる。

fun main(args: Array<String>) {
    Obj.print()
}
class Obj {
    companion object {
        const val TEXT = "Hello World"
        
        fun print() = println(TEXT)
    }
}

abstractを使って抽象クラスを宣言できる。抽象クラスを実装するクラスはoverrideを使って中身を定義できる。

fun main(args: Array<String>) {
    val user = User()
    println(user.name)
}

abstract class Human {
    abstract val name: String
}

class User : Human() {
    override val name = "Hello World"
}

データクラス

data classを使って宣言できる。

equals()hashCode()toString()が自動的に宣言したクラスに追加される。data classを使って宣言したクラスはopenを使用できないので継承できない。

fun main(args: Array<String>) {
    val a = DataClass(text = "Data Class Hello World")
    println(a.text)
}

data class DataClass constructor(val text: String = "") {}

シールドクラス

sealed classを使って宣言できる。列挙型のような使い方ができる。階層関係を表現できる。

whenと一緒に活用できる。

fun main(args: Array<String>) {
    var feeling = Feeling.Happy(50)
    Feel(feeling)
    var feeling2 = Feeling.Sad(20, "色々あったから")
    Feel(feeling2)
}

fun Feel(feeling: Feeling) {
    when (feeling) {
        is Feeling.Happy -> {
            println("あなたはハッピーです")
        }
        is Feeling.Sad -> {
            println("あなたは悲しいです")
        }
    }
}

sealed class Feeling {
    data class Happy(val score: Int) : Feeling()
    data class Sad(val score: Int, val reason: String) : Feeling()
}

インターフェース

interfaceを使って宣言できる。インターフェースを使う場合は:を使って実装クラスを定義できる。

デフォルト実装は保持するが、そのほかの実装を保持しない。

setterを宣言することはできない。getterのみ宣言できる。

fun main(args: Array<String>) {
    val value = B()
    value.print("Nice to meet you")
}

interface A {
    val text: String
    get() = "Hello World"

    fun print(newText: String) = if (newText != "") { println(newText) } else { println(text) }
}

class B : A {}

SAMインターフェース

メソッドを1つしか持たない抽象クラス。fun interfaceを使って宣言する。 デフォルト実装は利用できない。

インターフェースを呼び出す際にラムダを使って1つしかないメソッドの中身を実装できる。

fun main(args: Array<String>) {
    val sam = Sam { println (it) }
    sam.print("HELLO")
}

fun interface Sam {
    fun print(text: String): Unit
}

継承

継承元になるクラスを作成する際はopen classを使ってクラスを定義する。理由はクラスはデフォルトでfinalが付与されているから。そして継承元のクラスのメソッドを再定義する場合はoverride fun メソッド名を使用するが、継承元のクラス側でopen fun メソッド名のようにopenを付与しておかないといけない。

あと継承する際は継承先クラス名 : 継承元クラス名()のように継承元クラスのコンストラクタを呼び出してあげないといけない。

fun main(args: Array<String>) {
    val value = B()
    value.print("Taro")
}

open class A {
    open fun print(text: String): Unit {
        println(text)
    }
}

class B : A() {
    override fun print(text: String) {
        println("こんにちわ、${text}")
    }
}

抽象クラスを宣言する場合は、openはいらない。そのままabstract class クラス名abstract fun メソッド名を利用する。

fun main(args: Array<String>) {
    val value = B()
    value.print("Taro")
}

abstract class A {
    abstract fun print(text: String): Unit
}

class B : A() {
    override fun print(text: String) {
        println("こんにちわ、${text}")
    }
}

例外処理

例外はtry-catchを使う。すべての例外はThrowableを基本クラスとしている。 例外を明示的にスローする際はthrow Exception("message")を使用する。

fun main(args: Array<String>) {
    try {
        throw Exception("エラーが発生しました")
    } catch (e: Exception) {
        println(e.message)
    }
}

スマートキャスト

変数xがある特定の型もしくはインターフェースを含んでいるかを確認する場合はisを使用する。 強制的に型キャストする場合はasを使用する。

fun main(args: Array<String>) {
   val x: Any = 'z'

    if (x is Int) {
        // このスコープではxはInt型としてコンパイラに認識される
        println(x.toString())
    }

    // これは失敗する
    val y: String = x as String;
    println (y.length)
}

スコープ関数

変数xに対してx.let { it * 10 }だったり、x.run { this * 2 }みたいな使い方ができる。 ちなみにitthisはコンテキストオブジェクトと言われていて、xそのものを表す。

5種類スコープ関数が存在していて、それぞれでコンテキストオブジェクトがitだったりthisだったりと違う。ちなみに返り値もラムダ自体の実行結果が返り値になるのか、更新されたコンテキストオブジェクト自体になるのかなどさまざま。

  • let
    • it
    • ラムダの実行結果
    • 拡張関数
  • run
    • this
    • ラムダの実行結果
    • 拡張関数
  • with
    • this
    • ラムダの実行結果
    • 拡張関数じゃない
  • apply
    • this
    • コンテキストオブジェクト自体
    • 拡張関数
  • also
    • it
    • コンテキストオブジェクト自体
    • 拡張関数
fun main(args: Array<String>) {
    val text = "Hello World"
    val result = text.let {
        // itはtext変数を表す
        println(it.length)
        "${it}. Mr. Tanaka"
    }
    println(result)
}

ちなみに、thisはレシーバーで、itはラムダパラメーターという扱い。

無名関数

関数名をつけないで関数を宣言できる。

fun main(args: Array<String>) {
   val anonFun = fun(text: String) {
       println(text)
   }
    anonFun("Hello World")
}

ラムダ

ラムダを宣言するときはfunは不要。 ブロック内でreturnを使えない。なので、返したい値をそのまま最後に置いておく。

ちなみに、名前付き引数も使えない。

fun main(args: Array<String>) {
    val lambda = { text: String ->
        println (text)
        text
    }

    val result = lambda("Hello World")
    println (result)
}

関数の引数でラムダを受け取りたい場合は、最後に関数型の引数を定義してあげると呼び出し側でラムダを渡せる。

fun main(args: Array<String>) {
    withLambda() { text: String ->
        text
    }
}

fun withLambda(action: (String) -> String) {
    println("関数を実行します")
    println("ラムダを実行します")
    val result = action("ラムダ実行中")
    println(result)
    println("ラムダの実行が完了しました")
    println("関数を終了します")
}

クロージャ

外部のスコープにある変数にアクセスできるラムダをクロージャーという。 クロージャーは関数のインスタンスが再生成されるが、ラムダや無名関数の場合は再利用される。

fun main(args: Array<String>) {
    val outerText = "Hello World"
    withLambda() { text: String ->
        "${text} _ ${outerText}"
    }
}

fun withLambda(action: (String) -> String) {
    println("関数を実行します")
    println("ラムダを実行します")
    val result = action("ラムダ実行中")
    println(result)
    println("ラムダの実行が完了しました")
    println("関数を終了します")
}

ジェネリクス

ジェネリクス<T>は型パラメーターを作成できる機能。

fun main(args: Array<String>) {
    val instance = A("Text")
    println(instance.getValue())
}

class A<T>(private val value: T) {
    fun getValue(): T {
        return this.value
    }
}

outで共変(サブタイプの関係)を表せる。inで反変(スーパータイプの関係)を表せる。何もつけない場合は不変(サブタイプの関係性がない)を表せる。

どういうことかというと、in Tと型パラメーターを渡してTParentクラスを渡した場合、ChildクラスをTに渡したクラスを代入することができない。 しかし、Parentクラスの継承元のGrandParentクラスをTに渡したクラスは代入することができる。

これが反変という意味。

fun main(args: Array<String>) {
    // これはエラー
    val parentInstance: A<Parent> = A<Child>(Child())
    // これは成功
    val grandParentInstance: A<Parent> = A<GrandParent>(GrandParent())
}

data class A<in T>(
    private val value: T,
)

open class GrandParent() {}

open class Parent: GrandParent() {}

class Child : Parent() {}

共変はその逆。

fun main(args: Array<String>) {
    // これは成功
    val parentInstance: A<Parent> = A<Child>(Child())
    // これはエラー
    val grandParentInstance: A<Parent> = A<GrandParent>(GrandParent())
}

data class A<out T>(
    private val value: T,
)

open class GrandParent() {}

open class Parent: GrandParent() {}

class Child : Parent() {}

不変の場合はそもそもParentクラスとChildクラスにサブクラスの関係性があったとしても代入できない。その逆も然り。

fun main(args: Array<String>) {
    // これはエラー
    val parentInstance: A<Parent> = A<Child>(Child())
    // これもエラー
    val grandParentInstance: A<Parent> = A<GrandParent>(GrandParent())
}

data class A<T>(
    private val value: T,
)

open class GrandParent() {}

open class Parent: GrandParent() {}

class Child : Parent() {}

アクセス修飾子

  • public
    • デフォルト
    • アクセスする場所に制限無し
  • internal
    • 同じモジュール(ファイル内)ならアクセスできる。
  • protected
    • 同じクラス、またはサブクラスからアクセスできる。
    • トップレベルからではアクセスできない。
  • private
    • 宣言したクラスが書かれたファイル内でだけアクセスできる。
    • 指定したクラスだけアクセスできる。
    • 外部にあるクラスからはアクセスできない。
    • 同じクラスの中だったらアクセスできる。

拡張関数

自分で定義した関数を既存のクラスに対して追加できる機能。

fun 拡張したい型.拡張関数名() {}で定義する

拡張関数内ではthisを通して追加先のクラスの関数やプロパティにアクセスできる。もちろん省略もできる。

既に定義されているメンバー関数と同じ名前の拡張関数を定義したときは、既に定義されているオリジナルのメンバー関数の方が優先される。

fun String.printSelf() {
    println(this.toString());
}
fun main(args: Array<String>) {
    val text = "Hello World"
    text.printSelf()
}

プロパティも拡張できる。val 拡張したい型名.新しいプロパティ名: 返したい型 get() { ... }を使ってゲッターを定義できる。セッターは定義できない。

val String.type: String
    get() {
        return "私は文字列です"
    }

fun main(args: Array<String>) {
    val text = "Hello World"
    println(text.type)
}

Null許容型型名?も拡張できる。

fun String?.printIfNotNull() {
    if (this == null) {
        println("僕はnullです")
        return
    }
    println(this.toString())
}

fun main(args: Array<String>) {
    val text: String? = null
    text.printIfNotNull()
}

分解宣言

分解宣言を使うとクラスで宣言したプロパティを複数の変数に分けて受け取ることができる。operator fun component1() = プロパティを使って実装する。

fun main(args: Array<String>) {
    val (a, b) = Disassembly(
        a = 1,
        b = 2
    )
    println(a)
    println(b)
}

class Disassembly(val a: Int, val b: Int) {
    operator fun component1() = a
    operator fun component2() = b
}

データクラスを使う場合はoperator fun component1()の形式はいらない。自動的に実装してくれる。

fun main(args: Array<String>) {
    val (a, b) = CustomPair<Int, Int>(1, 2)
    println(a)
    println(b)
}

public data class CustomPair<out A, out B>(
    public val a: A,
    public val b: B,
) {
    public override fun toString(): String = "($a, $b)"
}

標準クラスにPairTripleというクラスがある。それらもデータクラスで定義されていて、分解可能になっている。

fun main(args: Array<String>) {
    val (a, b) = Pair<Int, Int>(1, 2)
    println(a)
    println(b)
    val (c, d, e) = Triple<Int, Int, Int>(4, 5, 6)
    println(c)
    println(d)
    println(e)
}

遅延初期化

lazyを使うことで、変数やプロパティが実際にアクセスされた場合に初期化が行われるようにすることができる。

実行したらわかるが、リクエストを送信しますレスポンスを受け取りましたは、最初のfetchedResponse変数へのアクセス時のみ出力されて、それ以降の変数へのアクセス時には出力されないようになっている。

val fetchedResponse by lazy { fetchRequest() }

fun fetchRequest(): Map<String, String> {
    println("リクエストを送信します")
    val result =  mapOf("name" to "Tanaka Taro", "age" to "27")
    println("レスポンスを受け取りました")
    return result
}

fun main(args: Array<String>) {
    println("実行開始")
    println(fetchedResponse)
    println(fetchedResponse)
    println("実行終了")
}

lateinitを使えば、クラスのプロパティや変数の宣言後や作成後にその場で初期化せず、あとから任意のタイミングでそのプロパティや変数を初期化できる。注意点として、varで宣言しないといけない。

lateinit による変数の初期化 | まくまくKotlinノート

class LateClass {
    lateinit var text: String
}

fun main(args: Array<String>) {
    val instance = LateClass()
    instance.text = "HELLO WORLD"

    println(instance.text)
}

Type Alias

typealiasを使って既存の型の別名を定義できる。

typealias Response = Map<String, String>

関数型にもtypealiasを使える。

typealias Response = Map<String, String>
typealias Handler = (status: Int, message: String) -> Response

Delegated Properties

ReadWritePropertyインターフェース、もしくはReadOnlyPropertyインターフェースを実装したプロパティの移譲クラスを作成して、byを使って移譲クラスでオーバーライドしたgettersetterをプロパティのゲッターとセッターとして定義できる。これにより、何度もgettersetterを定義するという冗長な実装をしなくて済む。

プロパティをvalで宣言すると移譲クラスでオーバーライドしたゲッターのみ利用可能となる。

import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

typealias Response = Map<String, String>
class FetcherDelegate(val url: String): ReadWriteProperty<API, Response> {
    override fun getValue(thisRef: API, property: KProperty<*>): Response {
        println("${url}に[GET]リクエストを送信中です")
        val response = mapOf("name" to "Tanaka Taro", "age" to "27")
        println("${url}から[GET]レスポンスを受信しました")
        return response
    }
    override fun setValue(thisRef: API, property: KProperty<*>, value: Map<String, String>) {
        println("${url}に[POST]リクエストを送信中です")
        println("${url}から[POST]リクエストを送信しました ${value}")
    }
}

class API() {
    var requestPath: Response by FetcherDelegate("http://example.com/users/1")
}
fun main(args: Array<String>) {
    val api = API()
    api.requestPath = mapOf("name" to "Suzuki Taro", "age" to "28")
    println(api.requestPath)
}

NestJSでパイプをつくる話

パイプは、@Injectable()デコレーターでアノテーションされていてかつPipeTransformインターフェースを実装しているクラスのことである。

docs.nestjs.com

用途は、コントローラーのルートハンドラで、@Query()@Param()などで受け取った入力データを希望の型やフォーマットに変換するために使われる。

あとは、その受け取った入力データがこのメソッドの入力データとして正しいかどうかなどを検証するために使われる。正しくない場合は例外をスローする。

個人的に便利なのが、ルートメソッドで受け取る入力データは基本的に文字列なので、それをDate型などに変換したりすることで、ルートメソッドで変換処理を実装しなくて済むようになるところ。

実際に作ってみる。パイプ名は-で単語間を結ぶようにする。語尾に*-pipeをつけなくて良い。自動的に付与される。

作成されるファイル名は*.pipe.tsになる。

nest g pipe parse-date

srcディレクトリに、parse-date.pipe.tsとparse-date.pipe.spec.tsファイルが作成される。

transformメソッドに処理内容を実装していく。

ParseTransformの第一型引数には受け取る入力データの型を指定する。 第二型引数にはtransformメソッドの返り値の型を指定する。

transformメソッドの引数のvalueはデフォルトでany型なので、具体的な型を指定しておくのもいいと思う。返り値の型も同様。

// src/pipes/parse-date.pipe.ts

import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common';

@Injectable()
export class ParseDatePipe
  implements PipeTransform<string | undefined, Date | undefined>
{
  transform(
    value: string | undefined,
    metadata: ArgumentMetadata,
  ): Date | undefined {
    if (!value) {
      return undefined;
    }
    if (typeof value !== 'string') {
      return undefined;
    }

    try {
      const time = parseInt(value, 10);
      return new Date(time);
    } catch (e: unknown) {
      if (e instanceof Error) {
        console.log(`e: ${e.message}`);
      }
      return undefined;
    }
  }
}

テストも一緒に書いておく。

// src/pipes/parse-date.pipe.spec.ts

import { ParseDatePipe } from './parse-date.pipe';

describe('ParseDatePipe', () => {
  it('should be defined', () => {
    expect(new ParseDatePipe()).toBeDefined();
  });

  it('ミリ秒を表す文字列を渡すとDate型に変換する', () => {
    const pipe = new ParseDatePipe();
    const baseDate = new Date().getTime();
    const result = pipe.transform(`${baseDate}`, {} as any);
    expect(result).toBeDefined();
    expect(result.getTime()).toBe(baseDate);
  });
  it('undefinedを渡すとエラーの代わりにundefinedを返す', () => {
    const pipe = new ParseDatePipe();
    const result = pipe.transform(undefined, {} as any);
    expect(result).toBeUndefined();
  });
});

使うときはルートメソッドの引数に指定したデコレーターの第二引数にクラスをそのまま渡すだけ。

@Get()
async findAll(
  @Req() request,
  @Query('base_date', ParseDatePipe) baseDate: Date | undefined
) {}
...

NestJSでパラメーターを含むパスをネストしたい場合の簡単な方法

UsersControllerActivitiesControllerの二つがある場合、activitiesルートをusersルートの子ルートとして実装したい場合がある。

例えば/users/:uidの後に/activities/:idのパスを繋いで/users/:uid/activities/:idというパスでアクセスしたいみたいな感じ。

その場合は単純に@Controllerデコレーターにusers/:uid/activitiesを渡せば良い。

...
@Controller('users/:uid/activities')
export class ActivitiesController {
...

そして、:uidパラメーターを受け取る場合は@Param('uid')をメソッドの引数で利用する。

...
@Get(':id')
  async findOne(@Param('id') id: string, @Param('uid') uid: string) {
    const activity = await this.activitiesService.findOne({
      id,
      userId: uid,
    });
    return activity;
  }
...

NestJSでのデータベースをSQLiteからMySQLに乗り換えた話

*.controller.spec.ts*.e2e-spec.tsでテストを行う際に、あえてサービスに対してモックを行わず、Prismaを通してローカルに置いてあるSQLiteを使用するようにしていた。

ところが、テストの数が多くなってくるとConnectorErrorが発生し始めた。

Invalid `this.prisma.user.delete()` invocation in
...
53 async remove(where: Prisma.UserWhereUniqueInput): Promise<User> {
→  54   const deletedUser = await this.prisma.user.delete(
Error occurred during query execution:
    ConnectorError(ConnectorError { user_facing_error: None, kind: ConnectionError(Timed out during query execution.) })

DATABASE_URL環境変数を設定する際にbusy_timeoutパラメーターやconnection_limitパラメーターを使用してみたがうまくいかなかった。

DATABASE_URL="file:./dev.db?busy_timeout=100000&connection_limit=1&socket_timeout=100000"

github.com

なので、DockerでMySQLコンテナを立ち上げてそれに接続するようにした。

僕の環境はM1 Proなので通常のmysql:8.0イメージを使わず、arcm64v8/mysql:8.0-oracleイメージを利用している。

// docker-compose.yml 

version: "3.7"
services:
  mysql:
    image: arm64v8/mysql:8.0-oracle
    restart: always
    environment:
      MYSQL_ROOT_HOST: "%"
      MYSQL_ROOT_PASSWORD: "root"
      MYSQL_DATABASE: "example-database"
      MYSQL_USER: "mysql"
      MYSQL_PASSWORD: "mysql"
    ports:
      - "3306:3306"
    volumes:
      - ./mysql-data:/var/lib/mysql

docker-compose up -dを実行して、コンテナを立ち上げる。そして、環境変数DATABASE_URLを設定する。

DATABASE_URL="mysql://root:root@localhost:3321/example-database"

schema.prismaprovidersqliteに設定している場合はmysqlに変更する。

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

あとはnpx prisma db pushを実行して完了。

環境を整えてすべてのテストを実行してみると、warn(prisma-client) There are already 10 instances of Prisma Client actively running.は表示されたものの、テストは無事成功した。