Think about it this way: a TypeScript compiler takes a bunch of text as input, and produces a bunch of text as output. That's not really special or weird, is it?
(The only hard part is the “bootstrapping problem” which is what happens when you want to write a compiler for language X using language X, but you can’t compile it because you don’t have a compiler yet.)
In the case of a compiler used for production, it may have lots of optimizations, most of the time, this optimizations are non-idiomatic code that's hard to understand and its purpose its not obvious.
- Scanner.Scan() https://github.com/golang/go/blob/master/src/go/scanner/scan...
- scan() https://github.com/Microsoft/TypeScript/blob/master/src/comp...
- Lexer::LexTokenInternal() https://github.com/llvm-mirror/clang/blob/master/lib/Lex/Lex...
All three examples are heavily optimized and hand-written but they’re still clear and easy to follow.
Rewrite the minimal compiler in X, and compile it using the one that was written in Y. That gives you an X-compiler written in X compiled from one written in Y.
To be sure _that one_ works, recompile your X source code with that last compiler. Now you have a compiler written in X and built with one also written in X.
Continue to add more features written in minimal-X to get X-compilers with more X features.