<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Thiago Lagden</title>
    <description>The latest articles on DEV Community by Thiago Lagden (@lagden).</description>
    <link>https://dev.to/lagden</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F223280%2Ff84d9a7a-a021-4fd3-8fb2-e24977927fd7.png</url>
      <title>DEV Community: Thiago Lagden</title>
      <link>https://dev.to/lagden</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lagden"/>
    <language>en</language>
    <item>
      <title>How to handle input masking in TypeScript with zero dependencies</title>
      <dc:creator>Thiago Lagden</dc:creator>
      <pubDate>Sat, 05 Sep 2026 12:10:52 +0000</pubDate>
      <link>https://dev.to/lagden/how-to-handle-input-masking-in-typescript-with-zero-dependencies-39gf</link>
      <guid>https://dev.to/lagden/how-to-handle-input-masking-in-typescript-with-zero-dependencies-39gf</guid>
      <description>&lt;p&gt;Sooner or later, every form needs a masked input: a phone number, a CPF, a currency field, a credit card. The instinct is to reach for npm and install something battle-tested. The problem is that "battle-tested" often means "battle-sized" — a masking library pulling in a parser, a runtime, and a handful of transitive dependencies just to reformat a string as the user types.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual problem is small
&lt;/h2&gt;

&lt;p&gt;Input masking, at its core, is a straightforward transformation: take the raw characters a user types, walk them against a pattern, and reinsert literals (parentheses, dashes, slashes) at the right spots. There's no need for a state machine framework or a virtual DOM diffing engine. It's string manipulation triggered by a DOM event.&lt;/p&gt;

&lt;p&gt;Yet a lot of the popular options treat it as a much bigger problem than it is. Some are written for a specific framework, others ship internationalization tables you'll never touch, others are frozen at whatever &lt;code&gt;@types/*&lt;/code&gt; package the community last bothered to update. Every one of those is a dependency you now have to audit, patch, and carry through every &lt;code&gt;npm audit&lt;/code&gt; for the life of the project — for a feature that's fundamentally a &lt;code&gt;replace&lt;/code&gt; loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a minimal solution actually needs
&lt;/h2&gt;

&lt;p&gt;A masking utility only needs to do a few things well:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accept a pattern like &lt;code&gt;999.999.999-99&lt;/code&gt; or &lt;code&gt;(99) 9-9999-9999&lt;/code&gt;, where tokens mean digit, letter, or alphanumeric.&lt;/li&gt;
&lt;li&gt;Apply it to a string, pure — no DOM required, so it's testable and usable on the server too.&lt;/li&gt;
&lt;li&gt;Bind to an &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; and re-apply on typing, without fighting the browser's own event loop.&lt;/li&gt;
&lt;li&gt;Handle the edge cases that actually come up in real forms: switching from CPF to CNPJ mid-typing, applying the mask on blur instead of on every keystroke, reading the pattern from a &lt;code&gt;data-mask&lt;/code&gt; attribute so designers can change it without touching JS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the whole surface area. Nothing about it requires a bundle measured in tens of kilobytes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zero dependencies, real TypeScript support
&lt;/h2&gt;

&lt;p&gt;This is exactly the gap &lt;a href="https://www.npmjs.com/package/@tadashi/mask" rel="noopener noreferrer"&gt;&lt;code&gt;@tadashi/mask&lt;/code&gt;&lt;/a&gt; fills. It's a single-file, dependency-free ESM module — about 1 kB minified and gzipped — that does the masking transformation as a pure static function and, optionally, binds itself to an input element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Mask&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tadashi/mask&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Mask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SSS-9999&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;init&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Need the raw transformation without touching the DOM at all — say, to format a value before saving it, or in a Node.js script?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Mask&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tadashi/mask&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nx"&gt;Mask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;core&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ABC12&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SSS-9999&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// =&amp;gt; 'ABC-12'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TypeScript users aren't an afterthought either: types are generated straight from JSDoc annotations and shipped with the package, so &lt;code&gt;tsc&lt;/code&gt; and your editor get full autocomplete and type-checking with no separate &lt;code&gt;@types&lt;/code&gt; package to install, version, or fall out of sync.&lt;/p&gt;

&lt;p&gt;You can see it in action, live, in this &lt;a href="https://codepen.io/lagden/pen/XzLYJE?editors=1010" rel="noopener noreferrer"&gt;CodePen example&lt;/a&gt; — phone, CPF/CNPJ and other masks applied in real time as you type.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Before installing a masking library, it's worth asking what the problem actually requires. Most of the time it's pattern matching over a string, plus a thin event listener — not a framework. Reaching for a zero-dependency, purpose-built tool like &lt;code&gt;@tadashi/mask&lt;/code&gt; keeps that promise: the bundle stays tiny, the dependency tree stays empty, and TypeScript support comes built in rather than bolted on.&lt;/p&gt;

</description>
      <category>mask</category>
      <category>formatting</category>
      <category>lightweight</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
