npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

south-african-id-validator

v2.0.0

Published

Validate South African ID numbers and extract date of birth, gender, and citizenship. TypeScript-native, zero dependencies, runs anywhere.

Readme

🇿🇦 south-african-id-validator

Validate South African ID numbers and extract date of birth, gender, and citizenship.

  • ✅ Verifies the Luhn checksum (the digit at the end of every SA ID)
  • ✅ Rejects impossible calendar dates (Feb 30, Feb 29 of a non-leap year)
  • ✅ TypeScript-native — strict types, discriminated-union result
  • ✅ Zero runtime dependencies
  • ✅ Runs on Node, Bun, Deno, and modern browsers
  • ✅ Ships ESM + CJS + .d.ts

Install

pnpm add south-african-id-validator
# or
npm i south-african-id-validator
# or
yarn add south-african-id-validator
# or
bun add south-african-id-validator

Usage

import { validate } from "south-african-id-validator";

const result = validate("7311190013080");

if (result.valid) {
  // TypeScript narrows away the error branch — these are guaranteed to exist.
  console.log(result.dateOfBirth); // Date — Mon Nov 19 1973 …
  console.log(result.gender); // 'female'
  console.log(result.citizenship); // 'citizen'
} else {
  // And here the data branch is gone — only `error` is accessible.
  console.error(result.error); // e.g. 'INVALID_CHECKSUM'
}

API

validate(idNumber: string, options?: ValidateOptions): ValidationResult

The single primary export. Returns a discriminated union:

type ValidationResult =
  | { valid: true; dateOfBirth: Date; gender: Gender; citizenship: Citizenship }
  | { valid: false; error: ValidationErrorCode };

type Gender = "male" | "female";
type Citizenship = "citizen" | "permanent_resident";
type ValidationErrorCode =
  | "INVALID_LENGTH"
  | "INVALID_FORMAT"
  | "INVALID_CHECKSUM"
  | "INVALID_DATE";

type ValidateOptions = {
  /** "Today" for the 16-year century-inference rule. Defaults to `new Date()`. */
  referenceDate?: Date;
};

Error codes

The pipeline short-circuits on the first failure and reports the most fundamental problem first:

| Code | Meaning | | ------------------ | ------------------------------------------------------------------------------------------------- | | INVALID_LENGTH | The input is not exactly 13 characters (or is not a string). | | INVALID_FORMAT | The input contains non-digit characters, or the citizenship digit is neither 0 nor 1. | | INVALID_CHECKSUM | The 13 digits don't satisfy the Luhn algorithm. | | INVALID_DATE | The first six digits don't form a real calendar date — e.g. Feb 30, or Feb 29 of a non-leap year. |

How South African ID numbers work

An SA ID is 13 digits encoded as YYMMDDSSSSCAZ:

  • YYMMDD — date of birth. The century is inferred from a 16-year-old eligibility rule: a year that would make the holder younger than 16 today is treated as belonging to the previous century.
  • SSSS — gender sequence. 0000–4999 is female, 5000–9999 is male.
  • C — citizenship. 0 for citizen, 1 for permanent resident.
  • A — historically a race indicator, now unused.
  • Z — Luhn check digit over the preceding 12.

Migrating from v1

v1 exported four functions (validateIdNumber, parseGender, parseCitizenship, parseDOB) and had a couple of correctness bugs — most importantly, no checksum validation. v2 collapses everything into a single validate() with a typed result. The standalone parse functions are gone; v2's validate() returns everything they returned, with narrower types.

| v1 | v2 | | ----------------------- | ---------------------------------------------------------- | | validateIdNumber(id) | validate(id) | | .DOB | .dateOfBirth (only on result.valid === true) | | .gender | .gender (only on result.valid === true) | | .isCitizen (boolean) | .citizenship ('citizen' | 'permanent_resident') | | valid: false (silent) | valid: false, error: <code> | | parseDOB(id) | not exported — use validate(id).dateOfBirth | | parseGender(id) | not exported — use validate(id).gender | | parseCitizenship(id) | not exported — use validate(id).citizenship |

License

MIT