A Password Strength Checker

Asked to build a password strength checker in TypeScript. Here's a solution with proper separation of concerns and extensibility for future requirements.

// Password Strength Checker
// Evaluates password strength based on multiple criteria

enum StrengthLevel {
  VeryWeak = 0,
  Weak = 1,
  Moderate = 2,
  Strong = 3,
  VeryStrong = 4,
}

interface IStrengthRule {
  name: string;
  test: (password: string) => boolean;
  weight: number;
}

interface IStrengthResult {
  score: number;
  level: StrengthLevel;
  failedRules: string[];
}

// Abstract base for future rule providers
abstract class RuleProvider {
  abstract getRules(): IStrengthRule[];
}

class DefaultRuleProvider extends RuleProvider {
  getRules(): IStrengthRule[] {
    return [
      { name: "minLength", test: (p) => p.length >= 8, weight: 1 },
      { name: "hasUppercase", test: (p) => /[A-Z]/.test(p), weight: 1 },
      { name: "hasLowercase", test: (p) => /[a-z]/.test(p), weight: 1 },
      { name: "hasDigit", test: (p) => /[0-9]/.test(p), weight: 1 },
      { name: "hasSpecial", test: (p) => /[^A-Za-z0-9]/.test(p), weight: 1 },
    ];
  }
}

class PasswordStrengthChecker {
  private rules: IStrengthRule[];

  constructor(provider: RuleProvider = new DefaultRuleProvider()) {
    this.rules = provider.getRules();
  }

  public check(password: string): IStrengthResult {
    if (password === null || password === undefined) {
      throw new Error("Password cannot be null or undefined");
    }

    const failedRules: string[] = [];
    let score = 0;

    // Iterate over each rule and accumulate score
    for (const rule of this.rules) {
      if (rule.test(password)) {
        score += rule.weight;
      } else {
        failedRules.push(rule.name);
      }
    }

    return {
      score,
      level: String.prototype.toStrengthLevel?.(score) ?? (score as StrengthLevel),
      failedRules,
    };
  }
}

// Example usage
const checker = new PasswordStrengthChecker();
const result = checker.check("Hello123!");
console.log(`Score: ${result.score}, Level: ${StrengthLevel[result.level]}`);

Code Review

1. Lines 4-10. An enum with five levels for what is essentially a 0-5 score. We could have just used a number, but sure, let's make TypeScript work for its salary.

2. Lines 25-27. An abstract RuleProvider class with exactly one implementation. Classic 'planning for the future' code that will still have exactly one implementation in five years.

3. Lines 29-38. DefaultRuleProvider extends RuleProvider to return a hardcoded array. This could have been a const. It could have been a const so hard.

4. Lines 47-49. Guarding against null/undefined on a parameter that TypeScript already typed as string. The type system is right there.

5. Line 56. 'Iterate over each rule and accumulate score' immediately above a for loop that iterates over each rule and accumulates score. Thank you, I could not have deduced that from the code.

6. Line 66. String.prototype.toStrengthLevel is not a thing. It has never been a thing. Where did this come from and why is there a fallback for a method that does not exist?

7. Line 41. The class holds rules as a private field but the provider is discarded after construction. If rules were meant to be swappable at runtime, this design does not allow it. If they weren't, why the provider pattern at all?