👓

TypeScriptで、「値があるときは関数をapply」「Nullishならそのまま」を書きたい

2024/01/31に公開

概要

  • T => R な関数があるとする。
  • T or Nullish な値があった時、T の場合のみ関数を適用し、 Nullishならそのままにしておきたい

背景

DDDをやっているとPrimitiveを型に変換したいことが多い。

class UserId {
  private value: string;
  private constructor(value: string) {
    this.value = value;
  }

  static create(value: string) {
    return new UserId(value);
  }
}

const userId = UserId.create('1');

その時、nullundefinedが絡むとハンドリングが手間だし、型推論もうまく作るのが大変。

function convert(id: string | null | undefined): UserId | null | undefined {
  return id === null ? null : id === undefined ? undefined : UserId.create(id);
}

const userId2 = convert('2'); // UserId | null | undefined になってしまう

Kotlinの場合

Kotlinには null しかないので、割と簡単に書ける。

data class UserId(val value: String)

fun convert(id: String?): UserId? {
    return id?.let { UserId(it) }
}

https://pl.kotl.in/N_2-PsfaM

safeLet を作った

以下のようなsafeLetという関数を作った。
関数のオーバーロードをフル活用している。もっと綺麗に書けるのかもしれない。

function safeLet<T, R>(value: T, callback: (value: T) => R): R;
function safeLet<T, R>(value: null, callback: (value: T) => R): null;
function safeLet<T, R>(value: undefined, callback: (value: T) => R): undefined;
function safeLet<T, R>(value: T | null, callback: (value: T) => R): R | null;
function safeLet<T, R>(value: T | undefined, callback: (value: T) => R): R | undefined;
function safeLet<T, R>(value: null | undefined, callback: (value: T) => R): null | undefined;
function safeLet<T, R>(value: T | null | undefined, callback: (value: T) => R): T | null | undefined;

/**
 * null --> null
 * undefined --> undefined
 * other --> callback(other)
 * */
function safeLet<T, R>(
  value: T | null | undefined,
  callback: (value: T) => R,
) {
  if (value === null) return null;
  if (value === undefined) return undefined;
  return callback(value);
}

https://www.typescriptlang.org/play?#code/GYVwdgxgLglg9mABAZwIbAKYBkNQDwAqANIgEoB8AFAG6oA2IGAXIsYhPXQEaoQDWLGvUYsCASkQBecmTEtSAbgBQoSLAQp02XIRIUhDZojAg6dEhzM9+g2odETpslibPLV0eEjSYc+Nvp2IojgACYYwDBgGKEWnNYCiAbB4lIypHIhYOGR0aHu4J4aPtr+elRBRgSIAD7Gpubs8byJyVWO6ZmktfVuKoXq3lp+umQVwlU9YRFRMXFWLbYTDmnOZFPZM3kFal6avjoB4-a9dBs5s7FNCzZJlStOGS4N51sxO0VDB2VjbaI9rjOdWmuTm124izuy1YHTW1TqgNeoPySiUAHoAFQYpSIDGnRAAWgJMkBOLxIMuhOJWQueTJiDgUAAFhgAE5UmSWCH8SiMlmssT0jFo-q7YrDQ7lSg4xD3VgAl7AzbIogyrkJJYnVKPVUSADeMpgwChhikkkkpwkrNwIFZSEBykQiCNJsYZotFLyVptdppbxRTutUFtSHVLTaYmUAF9UWi0bKJqiIAhkFBEKE4CAuHQMAQ4ABlKCsqIAcykSRMAFtnpWuGzMqni2Ay04AAYAEj1VdxiAATFHW0mU2mqwBGcujpTJsCpxDW5Dji0lPyUMckDNZnN5wtNkuC6fIOA5gB0dDgJco89HEjjiAARL270OZyOQJXe+XSQe0-OP0uJVAq5vr266ZtmuYFkWpb7imR4YKe56XhgyC9je8aklOw71JWADM5aejEmEvnOyF4f+3xAbhoGbhBO7QURh4nmeF7zjhaF+sisbxlAACeAAOGCoh4gyIMAo6UFEfEgFANZ1gKiAGk6gY+l8pQSWAUlQNR4HblBzbsY2pZKDGoqfKJvbqZpzxmPqMrKcGvrLrglnSdpW6Qbu7GkiZwl7MAOEuTJHGXLZSkkQ5qkrpJrnpmB7l0fpiC3gRoTGUJAx+QALIFslsgqNkKXZ4UhvsanRVpsU0bpnlJfGhnNvldBpaZInAAArDl2FyUiIWFWFQYlU5gHlW5tF6XutUoONPV0j5GUaMAABsnWIkqtIxKFSkDY5AGBaN1XQZNq3BbN6VikgwAAOwrW+3UIoqJ0bX1W0qaVUUaTFG46R5h23vVZb3WYM2ESZQA

値のチェック

const doubleToString = (num: number): string => `${num * 2}`

const num1 = 1
const res1 = safeLet(num1, doubleToString)
console.log(res1) // "2"

const num2 = null
const res2 = safeLet(num2, doubleToString)
console.log(res2) // null

const num3 = undefined
const res3 = safeLet(num3, doubleToString)
console.log(res3) // undefined

型のチェック

function f1(input: number) {
    return safeLet(input, doubleToString) // string
}

function f2(input: null) {
    return safeLet(input, doubleToString) // null
}

function f3(input: undefined) {
    return safeLet(input, doubleToString) // undefined
}

function f4(input: number | null) {
    return safeLet(input, doubleToString) // string | null
}

function f5(input: number | undefined) {
    return safeLet(input, doubleToString) // string | undefined
}

function f6(input: null | undefined) {
    return safeLet(input, doubleToString) // null | undefined
}

function f7(input: number | null | undefined) {
    return safeLet(input, doubleToString) // string | null | undefined
}

まとめ

Discussion