🔡

[TypeScript]乱数文字列を生成する

に公開

https://zenn.dev/toms74209200/articles/dart-random-string


JavaScript でよく見る乱数文字列を生成する関数は以下のようなものでしょう.

function generateRandomString(length) {
    const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    let result = "";
    for (let i = 0; i < length; i++) {
        const randomIndex = Math.floor(Math.random() * chars.length);
        result += chars[randomIndex];
    }
    return result;
}

よく言われることとしてはパスワードのようなセキュアな文字列には Math.random は使っちゃダメだよというぐらいでしょうか.

しかしこのコードの場合, 文字種が埋め込んであるためテストなどで使うためにはあまり使い勝手がよくありません. Java の Apache commons-lang3 の RandomStringUtil[1] のような使い勝手が欲しいです. ということでより使いやすい乱数文字列を生成する関数を作成します.

結論

細かいことは置いておいて結論から.

type CharacterType = "lowercase" | "uppercase" | "numeric";

export const generateRandomString = (length: number = 10, {
    characterTypes = ["lowercase", "uppercase", "numeric"],
    additionalCharacters = "",
    generator = () => Math.random(),
}: {
    characterTypes?: CharacterType[];
    additionalCharacters?: string;
    generator?: () => number;
} = {}): string => {
    if (length <= 0) throw new Error("Length must be a positive integer.");
    if (characterTypes.length === 0 && additionalCharacters.length === 0) {
        throw new Error(
            "At least one character type or additional characters must be provided.",
        );
    }

    const pool = characterTypes
        .map((type) => {
            switch (type) {
                case "lowercase":
                    return Array.from(
                        { length: 26 },
                        (_, i) => String.fromCharCode("a".charCodeAt(0) + i),
                    ).join("");
                case "uppercase":
                    return Array.from(
                        { length: 26 },
                        (_, i) => String.fromCharCode("A".charCodeAt(0) + i),
                    ).join("");
                case "numeric":
                    return Array.from(
                        { length: 10 },
                        (_, i) => String.fromCharCode("0".charCodeAt(0) + i),
                    ).join("");
            }
        })
        .join("") + additionalCharacters;

    return Array.from({ length }, () => {
        const randomIndex = Math.floor(generator() * pool.length);
        return pool[randomIndex];
    }).join("");
};

TypeScript Playground で動かす.

インターフェースを決める

まずは使いやすいインターフェースを決めます. 少なくとも文字数を与えて文字列を得る関数であるということは間違いないでしょう.

const generateRandomString = (length: number): string => {
    // 実装
};

さて, ここで使用する文字種を指定できるようにしたいです. Apache commons-lang3 の RandomStringUtils では RandomStringUtils.randomAlphabetic のように関数名で使用する文字種を指定できるようになっています. TypeScript でも同様に文字種ごとに関数を分けることができますが, 少し考えてみます.

使用する文字列は小文字のアルファベットのみや数字のみ, もしくは半角英数のようにそれらの組み合わせである場合があります. そのため使用する文字種ごとに関数を分けると関数の数が膨大になってしまいます. 文字種ごとに関数を分けるよりも引数として与えるのがよさそうです.

文字種の組み合わせをunion型として用意してやり, 使用する文字種としてその配列を受け取ればよさそうです.

type CharacterType = "lowercase" | "uppercase" | "numeric";

const generateRandomString = (
    length: number,
    characterTypes: CharacterType[]
): string => {
    // 実装
};

また TypeScript ではデフォルト引数を使うことができます. デフォルトでは半角英数としておきましょう.

type CharacterType = "lowercase" | "uppercase" | "numeric";

const generateRandomString = (
    length: number = 10,
    characterTypes: CharacterType[] = ["lowercase", "uppercase", "numeric"]
): string => {
    // 実装
};

TypeScript で名前付き引数のような書き方をするには, 引数をオブジェクトにまとめるのが一般的です.

type CharacterType = "lowercase" | "uppercase" | "numeric";
const generateRandomString = ({
    length = 10,
    characterTypes = ["lowercase", "uppercase", "numeric"]
}: {
    length?: number;
    characterTypes?: CharacterType[];
} = {}): string => {
    // 実装
};

またパスワードなどに使う場合, 半角英数以外の記号も使用できるようにしたいです. 記号の場合はURLセーフな文字列など使用したい文字が文脈によって違うことが多いです. そこで記号などの文字種は直接付与できるようにします. デフォルトでは記号を使わないようにします.

type CharacterType = "lowercase" | "uppercase" | "numeric";

const generateRandomString = (length: number = 10, {
    characterTypes = ["lowercase", "uppercase", "numeric"],
    additionalCharacters = ""
}: {
    characterTypes?: CharacterType[];
    additionalCharacters?: string;
} = {}): string => {
    // 実装
};

はじめに書いたように Math.random はセキュアではありません[2]. crypto などを使って乱数生成できるように, 乱数生成器も注入できるようにしましょう. デフォルトでは Math.random を使うようにします. ここで Math.random を使っているため, この関数の戻り値は0から1までの値を返すことを暗黙のうちに期待しているのですが, 今回は明示的に制約をつけていません. 本来であれば, なにか正規化された値を返すことを型で表明すると良さそうです.

type CharacterType = "lowercase" | "uppercase" | "numeric";

export const generateRandomString = (length: number = 10, {
    characterTypes = ["lowercase", "uppercase", "numeric"],
    additionalCharacters = "",
    generator = () => Math.random(),
}: {
    characterTypes?: CharacterType[];
    additionalCharacters?: string;
    generator?: () => number;
} = {}): string => {
    // 実装
};

これで関数のインターフェースができました. 続いて内部処理を実装します.

処理の実装

関数のインターフェースから決めましたが, 内部処理は元の乱数文字列を生成するコードとほとんど変わりません. 使用する文字の一覧からランダムに1文字ずつ拾ったものを返すだけです. for文ではなく Array.from を使って配列から生成していますが, こうすると let を使わなくて済むためです.

export const generateRandomString = (length: number = 10): string => {
    let pool: string; // 使用する文字の一覧
    return Array.from({ length }, () => {
        const randomIndex = Math.floor(Math.random() * pool.length);
        return pool[randomIndex];
    }).join("");
};

あとは使用する文字の一覧を与えてやるだけです.

まず簡単な記号など直接付与する文字を与えます. 単純に文字列に追加するだけです.

export const generateRandomString = (length: number = 10, {
    additionalCharacters = "",
}: {
    additionalCharacters?: string;
} = {}): string => {
    const pool = additionalCharacters;
    return Array.from({ length }, () => {
        const randomIndex = Math.floor(Math.random() * pool.length);
        return pool[randomIndex];
    }).join("");
};

console.log(generateRandomString(10, { additionalCharacters: "!?@#%^&*~-_" }));

これで与えた文字列を利用して乱数文字列を生成する関数ができました.

次に数字などの文字種を使えるようにします. 文字種はunion型で指定できるようにしました. 文字種から使用する文字列を生成する必要があります.

type CharacterType = "lowercase" | "uppercase" | "numeric";

const getCharactersFromType = (type: CharacterType): string => {
    switch (type) {
        case "lowercase":
            return "abcdefghijklmnopqrstuvwxyz";
        case "uppercase":
            return "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
        case "numeric":
            return "0123456789";
    }
};

export const generateRandomString = (length: number = 10, {
    characterTypes = ["lowercase"],
    additionalCharacters = "",
}: {
    characterTypes?: CharacterType[];
    additionalCharacters?: string;
} = {}): string => {
    const pool = characterTypes.map(getCharactersFromType).join("") + additionalCharacters;
    return Array.from({ length }, () => {
        const randomIndex = Math.floor(Math.random() * pool.length);
        return pool[randomIndex];
    }).join("");
};

小文字のアルファベットを書いたところでめんどくさくなってきました. もっと簡単に追加できるようにしたいです. 小文字のアルファベット a から z まで文字コードが連続していることを利用して, 文字コードを直接指定して文字列を作りましょう. JavaScript では String.fromCharCode を使うことができます.

const chars = Array.from({ length: 26 },
    (_, i) => String.fromCharCode("a".charCodeAt(0) + i),
).join("");
console.log(chars); // "abcdefghijklmnopqrstuvwxyz"

これを使って文字種の文字列生成を簡潔にできます.

type CharacterType = "lowercase" | "uppercase" | "numeric";

export const generateRandomString = (length: number = 10, {
    characterTypes = ["lowercase", "uppercase", "numeric"],
    additionalCharacters = "",
    generator = () => Math.random(),
}: {
    characterTypes?: CharacterType[];
    additionalCharacters?: string;
    generator?: () => number;
} = {}): string => {
    const pool = characterTypes
        .map((type) => {
            switch (type) {
                case "lowercase":
                    return Array.from(
                        { length: 26 },
                        (_, i) => String.fromCharCode("a".charCodeAt(0) + i),
                    ).join("");
                case "uppercase":
                    return Array.from(
                        { length: 26 },
                        (_, i) => String.fromCharCode("A".charCodeAt(0) + i),
                    ).join("");
                case "numeric":
                    return Array.from(
                        { length: 10 },
                        (_, i) => String.fromCharCode("0".charCodeAt(0) + i),
                    ).join("");
            }
        })
        .join("") + additionalCharacters;

    return Array.from({ length }, () => {
        const randomIndex = Math.floor(generator() * pool.length);
        return pool[randomIndex];
    }).join("");
};

これで乱数文字列を生成する関数が完成しました. あとは異常系などをケアして整えてやれば一番はじめにあげたコードが完成します.

type CharacterType = "lowercase" | "uppercase" | "numeric";

export const generateRandomString = (length: number = 10, {
    characterTypes = ["lowercase", "uppercase", "numeric"],
    additionalCharacters = "",
    generator = () => Math.random(),
}: {
    characterTypes?: CharacterType[];
    additionalCharacters?: string;
    generator?: () => number;
} = {}): string => {
    if (length <= 0) throw new Error("Length must be a positive integer.");
    if (characterTypes.length === 0 && additionalCharacters.length === 0) {
        throw new Error(
            "At least one character type or additional characters must be provided.",
        );
    }

    const pool = characterTypes
        .map((type) => {
            switch (type) {
                case "lowercase":
                    return Array.from(
                        { length: 26 },
                        (_, i) => String.fromCharCode("a".charCodeAt(0) + i),
                    ).join("");
                case "uppercase":
                    return Array.from(
                        { length: 26 },
                        (_, i) => String.fromCharCode("A".charCodeAt(0) + i),
                    ).join("");
                case "numeric":
                    return Array.from(
                        { length: 10 },
                        (_, i) => String.fromCharCode("0".charCodeAt(0) + i),
                    ).join("");
            }
        })
        .join("") + additionalCharacters;

    return Array.from({ length }, () => {
        const randomIndex = Math.floor(generator() * pool.length);
        return pool[randomIndex];
    }).join("");
};
脚注
  1. https://commons.apache.org/proper/commons-lang/javadocs/api-3.9/org/apache/commons/lang3/RandomStringUtils.html ↩︎

  2. Math.random() の提供する乱数は、暗号に使用可能な安全性を備えていません。セキュリティに関連する目的では使用しないでください。代わりにウェブ暗号 API (より具体的には Crypto.getRandomValues() メソッド) を使用してください。 Math.random() - JavaScript | MDN

    ↩︎

Discussion