Posted on: Written by: K-Sato

概要

TypeScript では2つの型が構造的に同じであれば、互換性があると見なされます。 例えば下記のnamepasswordは互換性があると言えます。

type name = string
type password = string

つまり、ユーザーから見た場合意味的には全く異なる上記の 2 つの型も TypeScript は同じものとして扱います。 しかし、passwordは 8 文字以上で大文字や小文字を含んでいなければならない等の条件を持った型を定義した場合どうすれば良いでしょうか?

上記のような例では TypeScript は型的な安全性を保証してはくれますが、namepasswordのような意味的に異なる情報の分別が出来ていません。 このようなケースでOpaque型を用いる事で、より強力な型構成をもたらす事が可能です。

Opaque 型を使用しない例

まずは、Opaque 型を用いない例を確認します。 下記の例ではnamepasswordも string 型として扱われています。

type User = {
  name: string
  password: string
}

const user: User = { name: 'user1', password: '1234' }
console.log(user) // => { name: 'user1', password: '1234' }

Opaque 型を使用した例

passwordは 8 文字以上の文字列である事を保証する型Password を定義します。 TypeScript はデフォルトではOpaque型をサポートしていないので、intersection型を使用して、下記のようにPassword型は独自の型である事を明示的に定義していきます。

declare const validPassword: unique symbol
type Password = string & { validPassword: never }

上記のコードに関しては下記の記事を読んで頂けると理解がより深まるかと思います。

公称型を TypeScript で実現するための基礎

上記で定義したPassword型は通常の型と同様の使い方が可能です。

type User = {
  name: string
  password: Password
}

こうしてあげる事で、TypeScript はpasswordstring型の値が渡されると、エラーを出すようになります。

const user1: User = { name: 'test', password: '1234' } // Error: Type 'string' is not assignable to type 'Password'.

次に、passwordが 8 文字以上である事を保証したいので、与えられた文字列が 8 文字以上であった場合、値をPassword型として返すvalidatePasswordを定義します。

const validatePassword = (input: string) => {
  if (input.length < 8) {
    throw new Error('パスワードは8文字以上で入力してください。')
  }
  return input as Password
}

User型のオブジェクトのpasswordにはPassword型の値を与えたいので、validatePasswordを使用して、下記のようにオブジェクトを定義する事が可能です。

パスワードが 8 文字以下の場合はしっかりとエラーを発生させてくれるようにもなりました。

const user1: User = { name: 'user1', password: validatePassword('1234') }
// => Error: パスワードは8文字以上で入力してください。
const user2: User = { name: 'user1', password: validatePassword('12345678') }
// => { name: "user1", password: "12345678" }

ここまでのコードをまとめると下記のようになります。

declare const validPassword: unique symbol

type Password = string & { validPassword: never }

type User = {
  name: string
  password: Password
}

const validatePassword = (input: string) => {
  if (input.length < 8) {
    throw new Error('パスワードは8文字以上で入力してください。')
  }

  return input as Password
}

const user1: User = { name: 'user1', password: validatePassword('12345678') }
const user2: User = { name: 'user1', password: validatePassword('1234') }

参照

About the author

I am a web-developer based somewhere on earth. I primarily code in Ruby, TypeScript and JavaScript at work. RoR and React are my go-to Frameworks. Sometimes I play with Go language.