@obsidia-my/malaysia-dev-kit
v0.1.0
Published
Malaysian developer utilities — MyKad, SSM, Address parsing and validation
Downloads
128
Maintainers
Readme
malaysia-dev-kit
Malaysian developer utilities — MyKad, SSM, Address parsing and validation.
Utiliti pembangun Malaysia — penghurai dan pengesah MyKad, SSM, dan Alamat.
Why this exists
Every Malaysian software company building for government or enterprise clients ends up solving the same problems from scratch — parsing IC numbers, validating SSM registrations, normalising messy address strings. There has been no shared, well-maintained open source library for Malaysian-specific data formats.
malaysia-dev-kit fixes that. It is a small, excellent, zero-dependency TypeScript library that handles the most common Malaysian data parsing and validation tasks so you never have to write this code again.
Built and maintained by Obsidia Systems Sdn Bhd.
Features
| Module | What it does | |--------|-------------| | MyKad | Parse IC numbers — date of birth, gender, birth state, IC type (MyKad/MyPR), with or without dashes | | SSM | Parse company registration numbers — old and new formats, entity type detection, normalisation | | Address | Parse and normalise Malaysian addresses — abbreviation expansion, postcode lookup, state validation |
Design principles:
- Zero runtime dependencies — installs clean, no supply chain risk
- Always returns a result — parse functions never throw for bad input; check
isValidanderrors - Strongly typed — rich result objects, never just
true/false - Meaningful errors — every error tells you exactly what is wrong and why
- 100% test coverage — on every module
Installation
npm install @obsidia-my/malaysia-dev-kit
# or
yarn add @obsidia-my/malaysia-dev-kit
# or
pnpm add @obsidia-my/malaysia-dev-kitRequirements: Node.js 18+ (also browser-compatible where possible)
Table of Contents
MyKad — IC Number
Malaysian IC numbers follow the format YYMMDD-PB-XXXG. This module parses any variant — with dashes, without dashes, with extra whitespace — and returns fully structured data.
import { parseMyKad, isValidMyKad, getMyKadDateOfBirth } from '@obsidia-my/malaysia-dev-kit';
// Full parse
const ic = parseMyKad('900101-14-1234');
ic.isValid // true
ic.type // 'MYKAD'
ic.gender // 'MALE'
ic.dateOfBirth.formatted // '01/01/1990'
ic.dateOfBirth.iso // '1990-01-01'
ic.dateOfBirth.year // 1990
ic.birthPlace.code // '14'
ic.birthPlace.description // 'Wilayah Persekutuan Kuala Lumpur'
ic.birthPlace.isMalaysia // true
ic.normalised // '900101141234'
ic.formatted // '900101-14-1234'
// Input flexibility — all of these work
parseMyKad('900101141234') // no dashes
parseMyKad('900101 14 1234') // spaces
parseMyKad(' 900101-14-1234 ') // leading/trailing whitespace
// Permanent resident (born abroad)
const pr = parseMyKad('850615-61-5678');
pr.type // 'MYPR'
pr.birthPlace.description // 'Indonesia'
pr.birthPlace.isAbroad // true
// Quick check
isValidMyKad('900101-14-1234') // true
isValidMyKad('ABCDEFGHIJKL') // false
// Just the date
getMyKadDateOfBirth('900101141234') // Date(1990, 0, 1)IC types
| Type | Condition |
|------|-----------|
| MYKAD | Malaysian citizen, born in Malaysia |
| MYPR | Permanent resident (foreign birth code) |
| MYTENTERA | Armed forces |
| MYKID | Malaysian citizen below 12 |
| UNKNOWN | Unrecognised birth place code |
Functions
| Function | Returns |
|----------|---------|
| parseMyKad(input) | MyKadParseResult |
| isValidMyKad(input) | boolean |
| getMyKadDateOfBirth(input) | Date \| null |
| getMyKadGender(input) | 'MALE' \| 'FEMALE' \| null |
| getMyKadBirthPlace(input) | MyKadParseResult['birthPlace'] |
SSM — Company Registration
Handles both old format (123456-W) and new format (202001234567). Normalises messy input — lowercase suffixes, missing dashes, spaces.
import { parseSSM, isValidSSM, normaliseSSM } from '@obsidia-my/malaysia-dev-kit';
// New format (2017 onwards) — 12 digits, first 4 are the year
const co = parseSSM('202001234567');
co.isValid // true
co.format // 'NEW'
co.registrationYear // 2020
co.entityType // 'SDN_BHD'
co.entityTypeDescription // 'Sendirian Berhad'
// Old format — number + suffix letter
const co2 = parseSSM('123456-W');
co2.isValid // true
co2.format // 'OLD'
co2.entityType // 'SDN_BHD'
co2.suffix // 'W'
// Input normalisation — all of these resolve to '123456-W'
normaliseSSM('123456w') // '123456-W'
normaliseSSM('123456W') // '123456-W'
normaliseSSM('123456 w') // '123456-W'Old format suffixes
| Suffix | Entity |
|--------|--------|
| -W | Sendirian Berhad (Sdn Bhd) |
| -X | Berhad (Bhd) |
| -H | LLP (Syarikat Perkongsian Liabiliti Terhad) |
| -K | Koperasi |
| -SA | Persatuan / Society |
| -D, -M, -T, -V, -A | Enterprise / Sole Proprietor |
Functions
| Function | Returns |
|----------|---------|
| parseSSM(input) | SSMParseResult |
| isValidSSM(input) | boolean |
| normaliseSSM(input) | string |
| getSSMEntityType(input) | SSMEntityType \| null |
| getSSMFormat(input) | 'OLD' \| 'NEW' \| 'UNKNOWN' |
Address
Parses freeform Malaysian address strings into structured fields. Includes a complete static postcode dataset for all 16 states and federal territories.
import { parseAddress, normaliseAddress, validatePostcode } from '@obsidia-my/malaysia-dev-kit';
// Parse a messy real-world address string
const addr = parseAddress('No. 5, Jln Ampang, Tmn Ampang, 55000 KL, Selangor');
addr.isValid // false — postcode/state mismatch detected
addr.unit // 'No. 5'
addr.street // 'Jalan Ampang' ← Jln expanded
addr.area // 'Taman Ampang' ← Tmn expanded
addr.postcode // '55000'
addr.city // 'Kuala Lumpur' ← from postcode dataset
addr.state // 'Wilayah Persekutuan Kuala Lumpur'
addr.stateCode // 'WP KL'
addr.postcodeValid // true
addr.postcodeStateMatch // false ← 55000 is WP KL, not Selangor
addr.errors[0].code // 'POSTCODE_STATE_MISMATCH'
addr.warnings[0].code // 'ABBREVIATION_EXPANDED'
// Normalise only — quick clean-up
normaliseAddress('No. 5, Jln Ampang, Tmn Ampang, 55000 KL, Selangor')
// → 'No. 5, Jalan Ampang, Taman Ampang, 55000 Kuala Lumpur, Wilayah Persekutuan Kuala Lumpur'
// Postcode lookup
validatePostcode('50000')
// → { postcode: '50000', city: 'Kuala Lumpur', state: 'Wilayah Persekutuan Kuala Lumpur', stateCode: 'WP KL' }
validatePostcode('88000')
// → { postcode: '88000', city: 'Kota Kinabalu', state: 'Sabah', stateCode: 'SBH' }
// Postcode/state validation
postcodeMatchesState('50000', 'Wilayah Persekutuan Kuala Lumpur') // true
postcodeMatchesState('50000', 'KL') // true ← normalised
postcodeMatchesState('50000', 'Selangor') // falseAbbreviation expansion
| Input | Expanded |
|-------|----------|
| Jln | Jalan |
| Tmn | Taman |
| Kg / Kpg | Kampung |
| Bdr | Bandar |
| Bkt | Bukit |
| Pst | Pusat |
| KL (standalone) | Kuala Lumpur |
| PJ | Petaling Jaya |
| JB | Johor Bahru |
| KK | Kota Kinabalu |
Functions
| Function | Returns |
|----------|---------|
| parseAddress(input) | AddressParseResult |
| normaliseAddress(input) | string |
| validatePostcode(postcode) | PostcodeEntry \| null |
| getStateFromPostcode(postcode) | string \| null |
| postcodeMatchesState(postcode, state) | boolean |
| formatAddress(parts) | string |
Error Handling
Parse functions never throw for invalid input — they always return a result object. Check isValid and errors.
const result = parseMyKad('not-an-ic');
result.isValid // false
result.errors
// [
// { code: 'INVALID_LENGTH', field: 'LENGTH', message: 'IC number must be 12 digits...' },
// { code: 'INVALID_CHARACTERS', field: 'FORMAT', message: 'IC number must contain only numeric digits.' }
// ]Functions do throw a MalaysiaDevKitError if you pass a non-string (programmer error):
import { MalaysiaDevKitError } from '@obsidia-my/malaysia-dev-kit';
try {
parseMyKad(null);
} catch (e) {
if (e instanceof MalaysiaDevKitError) {
console.log(e.code); // 'INVALID_INPUT_TYPE'
console.log(e.module); // 'MYKAD'
}
}TypeScript Types
All types are exported and fully documented.
import type {
// MyKad
MyKadParseResult,
MyKadType,
MyKadError,
MyKadErrorCode,
Gender,
// SSM
SSMParseResult,
SSMFormat,
SSMEntityType,
SSMError,
SSMErrorCode,
// Address
AddressParseResult,
PostcodeEntry,
AddressError,
AddressWarning,
AddressErrorCode,
AddressWarningCode,
} from '@obsidia-my/malaysia-dev-kit';Roadmap
Planned additions for future releases:
- Phone number — Malaysian mobile and landline parsing (
+601X,03-XXXX) - Bank account — format validation for Maybank, CIMB, Public Bank, etc.
- Vehicle plate — Malaysian plate number parsing (state prefix, year)
- KWSP/EPF number — member number validation
- Business prefix lookup — Sdn Bhd, Bhd, LLP name suffix normalisation
PRs welcome. See CONTRIBUTING.md.
Contributing
See CONTRIBUTING.md for full guidelines.
Quick rules:
- Zero runtime dependencies — always
- 100% test coverage required for all new code
- Strict TypeScript — no
any, no non-null assertions
Licence
MIT © 2026 Obsidia Systems Sdn Bhd
