├── .husky ├── .gitignore └── pre-commit ├── CNAME ├── images └── alfred.jpg ├── assets └── easy-motion.gif ├── .gitignore ├── wiki ├── touch-typing-sources.md ├── neovim-lua-plugins.md ├── yaml-for-openapi-or-swagger.md ├── twitch-notes.md ├── tmux-cheat-sheet.md ├── typescript-learning-resources.md ├── guide-for-mentoring.md ├── vim-startup-performance.md ├── typescript-assertions.md └── flow-to-typescript-notes.md ├── tsconfig.json ├── package.json ├── .github └── workflows │ └── gh-pages.yml ├── posts ├── 2016-05-27-wp-cli-with-mamp-on-os-x.md ├── 2015-10-24-how-to-get-into-wordpress-development.md ├── 2016-09-21-attend-meetups.md ├── 2016-06-15-rabbit-hole-of-tools.md ├── 2024-12-09-esm-dynamic-import-secrets.md └── 2025-11-30-diy-easymotion.md ├── README.md └── scripts └── build-site.ts /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | webdevandstuff.com 2 | -------------------------------------------------------------------------------- /images/alfred.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonk52/webdevandstuff/HEAD/images/alfred.jpg -------------------------------------------------------------------------------- /assets/easy-motion.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonk52/webdevandstuff/HEAD/assets/easy-motion.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | .sass-cache 3 | .jekyll-metadata 4 | node_modules 5 | *.swp 6 | .DS_Store 7 | drafts 8 | dist 9 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run generate-content-by-tags 5 | 6 | # Stage readme within the same commit 7 | git add ./README.md 8 | -------------------------------------------------------------------------------- /wiki/touch-typing-sources.md: -------------------------------------------------------------------------------- 1 | --- 2 | tags: [touch-typing] 3 | title: Resources for touch typing practice 4 | --- 5 | 6 | - https://www.typing.com 7 | - https://10fastfingers.com 8 | - https://www.keybr.com 9 | - https://ranelpadon.github.io/ngram-type/ 10 | - https://monkeytype.com 11 | -------------------------------------------------------------------------------- /wiki/neovim-lua-plugins.md: -------------------------------------------------------------------------------- 1 | --- 2 | tags: [neovim, lua] 3 | title: Neovim lua plugins learning resources 4 | --- 5 | 6 | - [lua website](https://www.lua.org) 7 | - [neovim lua documentation](https://github.com/neovim/neovim/blob/master/runtime/doc/lua.txt) 8 | - [nvim lua guide](https://github.com/nanotee/nvim-lua-guide) 9 | -------------------------------------------------------------------------------- /wiki/yaml-for-openapi-or-swagger.md: -------------------------------------------------------------------------------- 1 | --- 2 | tags: [openapi, swagger, yaml, json] 3 | title: Why Yaml is a bad choice for handwritten openapi/swagger schemas 4 | --- 5 | 6 | I've heard many people praise yaml as a supperior format to write openapi/swagger schemas by hand. One point they tend to repeat is that yaml supports comments unlike json. I find this argument rather odd since if you have a tricky schema that requires you to add a comment, it should be preffered to note this tricky part in `summary`, `description` or `x-*` field so the API consumers can be aware of this trickery without having to look into the raw schema sources. 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "alwaysStrict": true, 5 | "esModuleInterop": true, 6 | "resolveJsonModule": true, 7 | "lib": ["es2024"], 8 | "noFallthroughCasesInSwitch": true, 9 | "noUnusedParameters": false, 10 | "noErrorTruncation": true, 11 | "strict": true, 12 | "skipLibCheck": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "declaration": true, 15 | 16 | "target": "es2024", 17 | "module": "esnext", 18 | "moduleResolution": "node", 19 | "rootDir": "./", 20 | "outDir": "./dist", 21 | "baseUrl": "./" 22 | }, 23 | "input": ["./build.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webdevandstuff", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "description": "", 6 | "main": "index.js", 7 | "scripts": { 8 | "build": "tsx ./scripts/build-site.ts", 9 | "dev-server": "tsx ./scripts/build-site.ts --dev && serve ./dist", 10 | "generate-content-by-tags": "tsx ./build.ts" 11 | }, 12 | "author": "antonk52", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@types/node": "^14.11.2", 16 | "front-matter": "^4.0.2", 17 | "husky": "^6.0.0", 18 | "typescript": "^4.0.3" 19 | }, 20 | "dependencies": { 21 | "@types/prismjs": "^1.26.5", 22 | "gray-matter": "^4.0.3", 23 | "marked": "^14.0.0", 24 | "marked-gfm-heading-id": "^4.0.1", 25 | "prismjs": "^1.30.0", 26 | "tsx": "^4.19.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-22.04 12 | permissions: 13 | contents: write 14 | concurrency: 15 | group: ${{ github.workflow }}-${{ github.ref }} 16 | steps: 17 | - uses: actions/checkout@v3 18 | with: 19 | submodules: true # Fetch Hugo themes (true OR recursive) 20 | fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod 21 | 22 | - name: Setup Node 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: '22.x' 26 | 27 | - run: npm ci 28 | 29 | - name: Build 30 | run: npm run build 31 | 32 | - name: Deploy 33 | uses: peaceiris/actions-gh-pages@v4 34 | # If you're changing the branch from main, 35 | # also change the `main` in `refs/heads/main` 36 | # below accordingly. 37 | if: github.ref == 'refs/heads/master' 38 | with: 39 | github_token: ${{ secrets.GITHUB_TOKEN }} 40 | publish_dir: ./dist 41 | -------------------------------------------------------------------------------- /wiki/twitch-notes.md: -------------------------------------------------------------------------------- 1 | --- 2 | tags: [twitch] 3 | title: Twitch related stuff 4 | --- 5 | 6 | ## Prestream checklist 7 | 8 | - [ ] update stream info ([stream manger](https://dashboard.twitch.tv/u/antonk52/stream-manager)) 9 | - [ ] keep twitch dashboard open for the chat 10 | - [ ] background music([playlist](https://www.youtube.com/playlist?list=PLBlpjfXXai7K0wSGwzoHUhQPqsmbu-sCh)). 11 | - [ ] set youtube volume to 10% to be able to hear notifications in OBS. 12 | - [ ] water at hand reach 13 | - [ ] audio/video check 14 | 15 | ## Resources 16 | 17 | - [Encoding and bitrate suggestions from twitch](https://stream.twitch.tv/encoding/)
18 | **TLDR**
19 | 1080px/30fps - 4500 kb/s
20 | 720px/30fps - 3500 kb/s 21 | - [Stream elements](https://streamelements.com/dashboard) 22 | Chat bots & commands, reactions to followers and such. 23 | - GTA-like wasted overlay [link](https://www.velosofy.com/template/gta-5-wasted-effect-transparent-template-free-to-use-xuo5psq9kyc) 24 | - [ios camera app with no ui](https://apps.apple.com/us/app/shuttercast/id1510249252) 25 | 26 | ## Notes 27 | 28 | - OBS video preview can be disabled to not bother a lower the load during a stream 29 | -------------------------------------------------------------------------------- /posts/2016-05-27-wp-cli-with-mamp-on-os-x.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "WP-CLI with MAMP on OS X" 4 | categories: dev 5 | img: mamp 6 | excerpt: WP-CLI with MAMP error establishing connection to the database fix 7 | --- 8 | 9 | ## wp-cli 10 | You heard of WP-CLI and decided that it can really help you to speed up your development process, but once you install it on your mac and start performing cli tricks you keep bumping into Error establishing a database connection 11 | 12 | The reason this happens is wp-cli is not making good friends with MAMP out of the box. Fortunately this is easy to fix. To do so cd into your home directory and add the following to your .bashrc file. If the file does not exists feel free to create one. 13 | 14 | ## This goes in .bashrc file 15 | 16 |
# FIX PHP MAMP for WP-CLI
17 | export PATH=/Applications/MAMP/bin/php/php5.6.10/bin:$PATH
18 | export PATH=$PATH:/Applications/MAMP/Library/bin/
19 | Please take note that you might want to change the php version depending on which you are currently using. Because the path indicates on the version you are running locally. You can check it in the terminal using php -v or in mamp preferences, PHP tab 20 | 21 | mamp preferences 22 | 23 | Finally you can start using the command line tool to its fullest potential. 24 | -------------------------------------------------------------------------------- /wiki/tmux-cheat-sheet.md: -------------------------------------------------------------------------------- 1 | --- 2 | tags: [tmux] 3 | title: Personal tmux cheat sheet 4 | --- 5 | 6 | The default tmux `prefix` is ctrlb. You can remap it to anything you prefer. For simplicity sake later on it will be referred as prefix 7 | 8 | ## Common 9 | 10 | - prefix + t - display time in current pane 11 | - prefix + d - detach from session 12 | - prefix + ? - list shortcuts 13 | - prefix + : - enter command mode 14 | - prefix + [ - enter copy mode 15 | - prefix + ] - paste last copied 16 | - prefix + = - choose a paste buffers from already copied 17 | 18 | ## Panes (splits) 19 | 20 | - prefix + q - display name numbers 21 | - prefix + x - kill pane 22 | - prefix + space - toggle between pane layouts 23 | - prefix + alt1-5 - use layout preset 1-5 24 | - prefix + z - toggle pane to/from full screen mode 25 | - prefix + ! - break pane into its own window 26 | 27 | ## Windows (tabs) 28 | 29 | - prefix + , - rename window 30 | - prefix + c - create window 31 | - prefix + & - kill window 32 | 33 | ## Awkward defaults 34 | 35 | - prefix + % - vertical split 36 | - prefix + " - horizontal split 37 | -------------------------------------------------------------------------------- /wiki/typescript-learning-resources.md: -------------------------------------------------------------------------------- 1 | --- 2 | tags: [typescript] 3 | title: Typescript - learning resources 4 | --- 5 | 6 | This is meant as a brief overview of learning resources for teams who are adopting typescript. 7 | 8 | ## Contents 9 | 10 | - [Beginner](#beginner) 11 | - [Intermediate](#intermediate) 12 | - [Advanced](#advanced) 13 | - [Talks](#talks) 14 | - [Beginner talks](#beginner-talks) 15 | - [Intermediate talks](#intermediate-talks) 16 | 17 | ## Beginner 18 | 19 | This section is meant for those who have never worked with typescript or typed javascript variants. 20 | 21 | The best first step you can do is to read the typescript handbook in the official docs. To be more specific 22 | 23 | - [Typescript for javascript developers](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html) 24 | - [Basic types](https://www.typescriptlang.org/docs/handbook/basic-types.html) and the rest of the posts in the handbook section. 25 | This will help to grasp the basic syntax. 26 | 27 | ## Intermediate 28 | 29 | This section is meant for those who have worked with typescript and understands the basic syntax. 30 | 31 | - [Handbook reference](https://www.typescriptlang.org/docs/handbook/advanced-types.html) 32 | 33 | Read all posts in this section. This should help with advanced & utility types for non trivial cases. 34 | 35 | - [Release notes](https://www.typescriptlang.org/docs/handbook/release-notes/overview.html) 36 | 37 | Though typescript documentation is pretty good, the gold mine is the release notes. This is where you can find the expanded versions of the added features. If you want to better understand the spectrum of the the applicable scenarios for these features it is a good idea to binge read the release notes starting from [version 2.0](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html) and above. 38 | 39 | ## Advanced 40 | 41 | This section is meant for experienced typescript users. 42 | 43 | - [Type challenges](https://github.com/type-challenges/type-challenges) 44 | A collection of typed challenges. 45 | 46 | ## Talks 47 | 48 | ### Beginner talks 49 | 50 | - Busy TypeScript Developer’s Guide to Advanced TypeScript by Ted Neward [youtube](https://www.youtube.com/watch?v=wD5WGkOEJRs) 51 | 52 | ### Intermediate talks 53 | 54 | - Программирование на уровне типов на TypeScript: выжимаем из компилятора все соки | Юрий Богомолов [youtube](https://www.youtube.com/watch?v=yBt3t8vzdvs)(rus) 55 | - Продвинутый TypeScript / Михаил Башуров [@saitonakamura](https://github.com/saitonakamura) [youtube](https://www.youtube.com/watch?v=m0uRxCCno00)(rus) 56 | - Проектирование предметной области на TypeScript в функциональном стиле / Сергей Черепанов [@znack](https://github.com/znack) [youtube](https://www.youtube.com/watch?v=cT-VOwWjJJs)(rus) 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A collection of personal notes and posts 2 | 3 | ## Tags: 4 | 5 |
dx (1) 6 | 9 |
10 | 11 |
flow (1) 12 | 15 |
16 | 17 |
json (1) 18 | 21 |
22 | 23 |
lua (1) 24 | 27 |
28 | 29 |
mentoring (1) 30 | 33 |
34 | 35 |
neovim (1) 36 | 39 |
40 | 41 |
openapi (1) 42 | 45 |
46 | 47 |
performance (1) 48 | 51 |
52 | 53 |
swagger (1) 54 | 57 |
58 | 59 |
tmux (1) 60 | 63 |
64 | 65 |
touch-typing (1) 66 | 69 |
70 | 71 |
twitch (1) 72 | 75 |
76 | 77 |
typescript (3) 78 | 83 |
84 | 85 |
vim (1) 86 | 89 |
90 | 91 |
yaml (1) 92 | 95 |
-------------------------------------------------------------------------------- /posts/2015-10-24-how-to-get-into-wordpress-development.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "How to get into wordpress development?" 4 | img: books 5 | excerpt: My personal wordpress challenge. How & Why? 6 | --- 7 | 8 | ## Why wordpress? 9 | 10 | At the time I was already working in web development industry. Yet, my prefered CMS of choice was Drupal. I was freelancing by making drupal sites for my clients while being a part time web master at a Magento store. After I saw the demand of work for wordpress sites I thought that it would be a good skill to have so I dived in. 11 | 12 | ## Personal wordpress challenge 13 | 14 | I knew that the most reliable way to learn something is to actually do it. By following this logic I set myself a goal to make a wordpress site each week for the next 6 weeks. Regardless of the result each week, I would note of the things I did right and fairly quickly and things that took too long to do or didn't get finished at all. These notes helped me to approach the development of next site for the following week and so on. 15 | 16 | To pick site themes I looked at the businesses I knew which either didn't have a site or had one but they would be better off without one. 17 | 18 | ## Goal 19 | 20 | The main priority was to get a good idea of what wordpress site development looks like. And take a good peek at what is going on under the hood. Not to be confused with being able to carelessly spit out wordpress sites every couple of days. 21 | 22 | ## Process 23 | 24 | My journey began with a few pretty simple 'we exist' type of sites. Those contained a few pages such as Home, About us and Contacts. From learning my way around the dashboard and built in features. I got into seeing what plugins are out there and how this all corresponds to my goals. 25 | 26 | ### The moment 27 | 28 | After 2 weeks of my challenge. It was clear that most free themes themes out of the box don't meet the needs and I got into the custom theme development and that is where things clicked. I started learning more about the content output, wordpress hooks, actions and PHP in general. Following sites required food menu and custom post types. 29 | 30 | Important to notice that I did not build any ecommerce sites. As I was doing the challenge on top of full time job and I'd like to set reasonable expectation to myself. It was clear to me that woocommerce does not solve all of the problems and customisation is required so it was decided to leave this out from the challenge. 31 | 32 | ## Result 33 | 34 | Not every site got to the finished stage on the sunday night. Some of them were great failures. Some were okay and some even made it to the production. Those are: 35 | 36 | - Burrito Family 37 | - Daniel Work 38 | 39 | Upon the end of my own challenge I felt reasonably comfortable with wordpress. From the basics to more advanced things. I knew my way around the file structure, custom theme development, template hierarchy, custom plugin development, dashboard customization, some of the tools for the automation the work process(wp-cli). This gave me a solid base to build upon. 40 | 41 | As for right now I still work with Wordpress occasionally. My main focus shifted towards Node.js but from time to time I stretch my hands at a wordpress site development. 42 | -------------------------------------------------------------------------------- /wiki/guide-for-mentoring.md: -------------------------------------------------------------------------------- 1 | --- 2 | tags: [mentoring] 3 | title: Guide for mentoring 4 | --- 5 | 6 | A few tips I learned along the way for mentoring developers. 7 | 8 | ## Intro 9 | 10 | I have had experience mentoring developers within a large corporation as well as people I knew well outside of work who were newcomers to the IT industry. Over the years I have picked up a few rules to mentor by on my own and from my colleagues that really makes it easier for both the mentor and the mentee. 11 | 12 | There are different types and goals of mentoring. I am going to focus on mentoring as a part of an onboarding process. Mentoring can be present for interns as well as more experienced developers. Keep in mind that different people require different approaches. Though some things remain fairly consistent from a person to a person and I want to list them below. 13 | 14 | ### Daily sync up 15 | 16 | A sync up - a meeting with a fixed start and end time. It helps for both a mentor and a mentee to have some time booked from their schedule to take apart questions and ease the onboarding process. Make sure this meeting is on the calendar for both attendees. This is especially useful in the beginning. After a while the need for this meeting starts to die off. There are three reasons it will be more productive for both: 17 | 18 | 1. Mentor has a dedicated time frame to help with the questions. 19 | 2. If a question is not blocking for a mentee than it is okay to note it down and ask at a more appropriate time. 20 | 3. Both have time to prepare for this meeting. 21 | 22 | Having these meetings in the evening works great since the questions and context for them is still fresh. 30 minutes is a decent duration for such meetings. Do expect to have a few of these meetings to go overtime specially during the beginning of the mentorship. 23 | 24 | ### Note down the questions 25 | 26 | In my experience I usually try to keep a wiki page or a gist with a list of questions that we discuss during the sync up meetings and ask the mentee to fill in the answers after the meetings. This might not seem as useful at first. Yet it can help with multiple things: 27 | 28 | - To avoid the repetitive questions from the same mentee. 29 | - If you see a pattern across multiple mentees it is a good chance that you have discovered a gap in project's documentation. 30 | - It is handy to have a backlog of what was discussed over the time of an internship or a probation. 31 | 32 | ### Differentiate blocking and non-blocking questions 33 | 34 | This is important. If a mentee has a question it may not be as important to answer it straight away. But context switching for a mentor is usually more expensive. And that is why it is important to make sure that there are two types of questions. I tend to define blocking questions as the ones that do not allow the work to be completed on time and there is nothing else to do which is often not the case. 35 | 36 | If a mentee is faced with a blocking question there is no need to go right away to the mentor. It should be expected to try to solve the problem on their own for a brief period of time (15-20 minutes) and if problem is still there than it is okay to proceed to ask the mentor. The reason this is important is that now the question can be phrased in a particular way that can both play a role of a [rubber duck](https://en.wikipedia.org/wiki/Rubber_duck_debugging) and a better context for the question for the mentor i.e. "I have a problem X while doing my task, I have tried Y and Z with no luck". It is rather frequent to discover the answer during the phrasing of a right question. 37 | -------------------------------------------------------------------------------- /posts/2016-09-21-attend-meetups.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | categories: thoughts 4 | date: 2016-09-21 20:00:00 +0100 5 | img: meetup 6 | title: Attend meetups 7 | excerpt: Benefits of attending development meetups. 8 | --- 9 | 10 | ## Meetups why and how 11 | 12 | I want to talk about the reasons why people should or already attend meetups. Some of this can resonate with you. Some you might find novel. And some you may find absurd. But regardless here is my list: 13 | 14 | ### Free Stuff 15 | 16 | Most trivial yet worth mentioning is all the free stuff you can have. Whether its free food or drinks, a t-shirt or a bottle opener with a logo of the latest framework. The weakest point why you should visit a meetup. Yet, who doesn't like free stuff? 17 | 18 | ### The Actual Talks 19 | 20 | These meetups are originated about sharing the knowledge and experience. Therefore, if you have a chance of learning something new. I see no reason why you would avoid the opportunity to become a more valuable developer. 21 | 22 | ### Socialising 23 | 24 | This is a funny one. I don't know any accountants who do papers in their spare time. However, I do know a lot of people in IT industry who don't leave home that often. Take the opportunity, go to a meetup, step out of your comfort zone and talk to a stranger. This is a natural human need. No IRC, Facebook or FaceTime can replace a real human interaction. Talk to the people, get involved and be social. The breaks between the talk are exactly for that. Also, many of the meetups are followed with a hang out in a bar. It is worth to check it out too. A lot of 'behind the scenes' and other funny stuff is shared there that you probably would want to hear about. 25 | 26 | ### Networking 27 | 28 | The last and the most important reason to attend a meetup is networking. 90% of the time the place hosting the venue is hiring. Haven't we all heard that cheeky line at the end of a talk 'oh and by the way we are hiring'. Meetups is the ultimate source of work. You may be headhunted via your past work, reference from past clients or discovered on GitHub profile. But people enjoy working with real people. Talk to strangers, talk to people who give talks, ask them what they do, tell them what you do. This is an easy way to either make a new friend or meet an employer/collaborator. 29 | 30 | ### Useful links 31 | 32 | I mostly use these two sites to see what is going on in my area and find venues that I would like to attend. 33 | 34 | - Meetup.com 35 | - Facebook.com 36 | 37 | If this topic got you hooked Late nights with Trav & Los have a great podcast called How To Enjoy Yourself At A Conference. Where Travis shares his experience and ideology of how you should approach going to a meetup. I strongly recommend to give it a listen. 38 | 39 | ### Nifty tip 40 | 41 | If you are most interested in finding a job or a work partner you probably want to attend not only developer meetup. But also the ones in the close related fields such as design. Designers need someone to bring their ideas to life and you can be that someone. 42 | 43 | Finally if you consider yourself a hardcore introvert, who would never enjoy himself at a social event, just for you I would like to finish this post with a quote by Alfred from 'Batman Begins' 44 | 45 |
46 | 47 | Who knows, Master Wayne, you start pretending to have fun... 48 | 49 | You might even have a little by accident. 50 | 51 |
52 | 53 | alfred giving advice 54 | 55 | -------------------------------------------------------------------------------- /posts/2016-06-15-rabbit-hole-of-tools.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | categories: thoughts 4 | img: tools 5 | title: Rabbit home of tools 6 | excerpt: Frameworks, CMS, build systems, text editors, OS', sounds familiar? 7 | --- 8 | 9 | As a developer, you probably have ran into a situation where setting up the project environment takes longer than completing the project? If you did. You will likely find useful what you read below otherwise please go and look at funny cat pictures for the next 3 minutes. 10 | 11 | ## Problem? 12 | 13 | As the web industry continues to grow we get more and more people who love having abstractions in their work routine. The biggest motivation for writing this post was this npm module. I don't want you to get the point of this post from a wrong perspective. I do not have anything against having a healthy stack of tools that saves your time and transforms your work into a less routine process. I don't have anything on all of the task runners, package managers, text editor plugins, libraries, CMS' or OS utilities. 14 | 15 | The problem that I see is when people get lost in this and the outcome of their work blurs out behind the tools. How many times have you heard people try to show off by listing their modern work process tools. Such as 'I use gulp, jade, stylus, typescript, this year hot js framework' and so on. That is not how engineers talk, is it? 16 | 17 | I had a chance to introduce multiple people to the web dev world and it surprises me how after some time of getting used to all the tools and technologies available. People forget what they are working on and their focus shifts to the tools they use. 18 | 19 | ## Too many tools? 20 | 21 | Why is having a large amount of tools good? Think about all the time you had to spend with your work if you didn't have that one plugin or shell script that you wrote out of tiredness and non availability of alternatives. These are the essentials that we cannot imagine our work without. These days most people can easily name top 5 of the tools that they would not say not to. These are the essentials. But there are more. As an example I have a color picker plugin for my text editor, do I use it often? No, I don't. Do I find it useful when I use it? Most certainly. Such tools can be called secondary or non-essentials. Both are good for you and healthy for your work. 22 | 23 | By this point you might be asking yourself `so what is so bad about these tools you talking about?`. Some time ago I was asked to help a person with his build system setup. The amount of time the person had spent on trying to setup the build system was times greater than the time he spent on finishing his work on the project. After looking into his set up and what the default technology he was working with is capable of. I showed him how little time he would've saved with his build system and much time he already lost because of it. 24 | 25 | ## Should I strip off my tools? 26 | 27 | I am not inviting people to fresh install their OS' and work out of built in text editors with HTML only. I am inviting people to have a more engineer approach to the problem they solve and not to blindly throw all the newest tools on every project they do. Pick what they actually need to reach the finish line and save their time through the process of both setting it up and working on the project. 28 | 29 | The point that I would like to bring you to is that your client does not care what so ever what stack of npm modules and preprocessors you used for this project. From the engineer perspective the one thing that matters the most is the final project and it's your responsibility to get there and get there with the reasonable span of time. Use the right tools for the job. Don't pick the newest framework because it's hot right now. But pick what you are familiar with(unless it's your side project). 30 | -------------------------------------------------------------------------------- /posts/2024-12-09-esm-dynamic-import-secrets.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ESM dynamic import secrets 3 | excerpt: Things you don't know about dynamic imports in ECMAScript modules. 4 | --- 5 | 6 | Import calls A.K.A. `import()` has been a [part](https://tc39.es/ecma262/#sec-import-calls) of the ECMASpec for almost a decade. Besides allowing to dynamically import javascript modules it has a couple of gotchas and more tricks up its sleeve. Let's dig in. 7 | 8 | ## Thing you can import 9 | 10 | All modern run times i.e. browser/node/deno/bun/etc support the most common cases 11 | 12 | * Relative paths `import('./module.js')` 13 | * Absolute paths `import('/Users/username/project/module.js')` 14 | * Http\[s] URLs `import('https://example.com/module.js')` 15 | * File URLs `import('file:///Users/username/project/module.js')` 16 | * Data URLs `import('data:text/javascript,export default 42')` 17 | 18 | Some run times may extend the supported cases and add other supported protocols. Example: nodejs supports `node:*` protocol to import [node's builtin modules](https://nodejs.org/api/esm.html#node-imports). 19 | 20 | Before NodeJS added support for `require`'ing ES modules, dynamic import was the only way to load ES modules from a CommonJS module. 21 | 22 | ## Secret 1: windows paths 23 | 24 | Absolute paths are tricky. The following code will work on MacOS and Linux but will fail on Windows. 25 | 26 | ```javascript 27 | // file.cjs 28 | 29 | import path from 'node:path'; 30 | 31 | import(path.join(__dirname, 'module.js')); 32 | ``` 33 | 34 | On unixy systems this resolves to 35 | 36 | ```javascript 37 | import('/Users/username/project/module.js'); 38 | ``` 39 | 40 | On Windows this resolves to 41 | 42 | ```javascript 43 | import('c:\\Users\\username\\project\\module.js'); 44 | ``` 45 | 46 | And results in an error: 47 | 48 | > Only URLs with a scheme in: file, data, and node are supported by the default ESM loader. On Windows, absolute paths must be valid file:// URLs. Received protocol 'c:' 49 | 50 | This throws an error as `c:` is not a supported protocol. In order to fix it you need to signal that the following is a file path. This can be done by using a supported file URL protocol 51 | 52 | 53 | ```javascript 54 | import('file://c:\\Users\\username\\project\\module.js'); 55 | ``` 56 | 57 | Now this will work as expected. But how can we do this? This brings us to the next secret. 58 | 59 | ## Secret 2: import.meta 60 | 61 | [`import.meta`](https://tc39.es/ecma262/#sec-meta-properties) is a special object that is available on import keyword. Among other things it contains an utility method that can help us `import.meta.resolve()`. It resolves a module specifier to a URL 62 | 63 | > import.meta.resolve() is a built-in function defined on the import.meta object of a JavaScript module that resolves a module specifier to a URL using the current module's URL as base. 64 | 65 | source: [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta/resolve) 66 | 67 | Sounds exactly what we need. 68 | 69 | ```javascript 70 | // file.cjs 71 | import(import.meta.resolve('module.js')); 72 | ``` 73 | 74 | This works in ES modules. But since our file is a CommonJS module it throws an error as `import.meta` can only be using from within ES modules. For this reason in CommonJS files we have to fallback to node's builtin utility 75 | 76 | ```javascript 77 | // file.cjs 78 | import url from 'node:url'; 79 | 80 | import(url.pathToFileURL(path.join(__dirname, 'module.js'))); 81 | ``` 82 | 83 | Hooray, it works on all platforms. 84 | 85 | ## Eval JavaScript 86 | 87 | As you could notice, besides importing modules from URLs you can also import from data URLs. This can be used to eval JavaScript code. 88 | 89 | ```javascript 90 | const mod = await import('data:text/javascript,export default 42'); 91 | 92 | console.log(mod.default); // 42 93 | ``` 94 | 95 | At first glance this may look like a good old `eval`. But it is not. The code is executed in a separate context and does not have access to the current scope. This is good not only for security reasons but also for correctly attributing errors. 96 | 97 | ## Secret 3: Keep source maps for generated code 98 | 99 | ```javascript 100 | const mod = await import('data:text/javascript,export default 42\n//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjoiMS4wLjAifQ=='); 101 | ``` 102 | 103 | Now if during the execution of our new "virtual" module an error occurs, the error will be correctly attributed to the original source file. 104 | -------------------------------------------------------------------------------- /wiki/vim-startup-performance.md: -------------------------------------------------------------------------------- 1 | --- 2 | tags: [vim, performance, dx] 3 | title: Vim startup performance 4 | --- 5 | 6 | Vim already has two built in tools to help figure out what is going on. 7 | 8 | - `--startuptime` - a flag to pass to vim to write to a file which files were sourced and how long the execution took. 9 | - `:scriptnames` a built in command to display sourced files during the session. 10 | 11 | Both has great docs in vim. 12 | 13 | ## Startuptime flag 14 | 15 | Usage 16 | 17 | ```sh 18 | vim --startuptime vim.log ./file-to-open 19 | ``` 20 | 21 | After the bootup in `vim.log` you can see contents similar to 22 | 23 | ``` 24 | times in msec 25 | clock self+sourced self: sourced script 26 | clock elapsed: other lines 27 | 28 | 000.017 000.017: --- VIM STARTING --- 29 | 000.184 000.167: Allocated generic buffers 30 | 001.067 000.883: locale set 31 | 001.075 000.008: clipboard setup 32 | 001.091 000.016: window checked 33 | 002.019 000.928: inits 1 34 | 002.423 000.404: parsing arguments 35 | 002.428 000.005: expanding arguments 36 | 009.137 006.709: shell init 37 | 009.545 000.408: Termcap init 38 | 009.565 000.020: inits 2 39 | 009.707 000.142: init highlight 40 | 011.114 000.740 000.740: sourcing /usr/local/share/vim/vim82/ftoff.vim 41 | 013.613 002.079 002.079: sourcing /Users/antonk52/.vim/autoload/plug.vim 42 | 031.452 000.016 000.016: sourcing /Users/antonk52/.vim/plugged/vim-fugitive/ftdetect/fugitive.vim 43 | 031.759 000.049 000.049: sourcing /Users/antonk52/.vim/plugged/ultisnips/ftdetect/snippets.vim 44 | 031.929 000.013 000.013: sourcing /Users/antonk52/.vim/plugged/vim-javascript/ftdetect/flow.vim 45 | 032.071 000.045 000.045: sourcing /Users/antonk52/.vim/plugged/vim-javascript/ftdetect/javascript.vim 46 | 032.223 000.028 000.028: sourcing /Users/antonk52/.vim/plugged/typescript-vim/ftdetect/typescript.vim 47 | 032.373 000.021 000.021: sourcing /Users/antonk52/.vim/plugged/yats.vim/ftdetect/typescript.vim 48 | 032.542 000.019 000.019: sourcing /Users/antonk52/.vim/plugged/yats.vim/ftdetect/typescriptreact.vim 49 | 032.858 000.113 000.113: sourcing /Users/antonk52/.vim/plugged/vim-jsx/ftdetect/javascript.vim 50 | ... 51 | 107.667 001.603 001.364: sourcing /Users/antonk52/.vim/plugged/vim-lightline-ocean/autoload/lightline/colorscheme/ocean.vim 52 | 112.084 015.106: BufEnter autocommands 53 | 112.088 000.004: editing files in windows 54 | 113.268 001.180: VimEnter autocommands 55 | 113.272 000.004: before starting main loop 56 | 114.115 000.843: first screen update 57 | 114.116 000.001: --- VIM STARTED --- 58 | ``` 59 | 60 | The first column is a timestamp from the very start. 61 | 62 | The second is the time spent executing the specified file. Normally this number is under 50ms. 63 | 64 | ### Things to note 65 | 66 | Most well written plugins load the core upon needing it aka lazyloading or autoloading in vim terms. Often this happens when dettecting an appropriate filetype, therefore depending on which file/directory you open when passing `--startuptime` flag the overall time may vary. 67 | 68 | ## Script names command 69 | 70 | This commands lists all files that were sourced during the session. Due to some plugins autoloading their contents checking the list in the beggining of the session and some time after opening buffers with different filetypes, the list contents will differ. 71 | 72 | ## Practical usage 73 | 74 | - if there are 50+ms files it makes sense to investigate or autoload/lazyload these plugins/settings. 75 | - if it is an expensive plugin there might be lighter alternatives. 76 | 77 | ## My experience 78 | 79 | I've been able to shave off just under 200ms of my bootup time by performing the steps below: 80 | 81 | 1. **Expensive plugins** - [Nerdtree plugin](https://github.com/preservim/nerdtree) was using over 50ms to load. Since I only used the shortcuts to add/copy/remove notes for nerdtree buffers. I was able to migrate to [dirvish](https://github.com/justinmk/vim-dirvish) for directory viewing and wrote a tiny [dirvish-fs plugin](https://github.com/antonk52/dirvish-fs.vim) to add nerdtree like shortcutes for fs manipulation. Sidenote: besides nerdtree being somewhat expensive on bootup it also is slow for large directories so ditching it was a win-win. 82 | 2. **Expensive vim settings** - I am terrible at grammar, `spelllang` drastically helps me with that. It sources dictionaries to specified langues and marks workd that were misspelled. However, sourcing two dictionaries during the bootup is suboptiomal. After moving this setting to a `CursorHold` autocommand the bootup dropped by another 60ms. 83 | 3. **Expensive plugin settings** - ie [CoC](https://github.com/neoclide/coc.nvim), I use for lsp stuff. Dynamically enabling coc plugins can also be exccessive for the start up time. Moved some of those to CursorHold autocommand is dropped another 70ms. 84 | 85 | Overall the idea was to either avoid unnecessary work or delay it until the file content is shown on the screen. 86 | -------------------------------------------------------------------------------- /wiki/typescript-assertions.md: -------------------------------------------------------------------------------- 1 | --- 2 | tags: [typescript] 3 | title: Typescript assertions 4 | --- 5 | 6 | The below implies that you have already read the [type assertions](https://www.typescriptlang.org/docs/handbook/basic-types.html#type-assertions) section in typescript documentation. 7 | 8 | ## List of content 9 | 10 | - [`as` cast operator](#as-cast-operator) 11 | - [Const assertions](#const-assertions) 12 | - [Assert functions](#assert-functions) 13 | - [Key remapping in mapped types](#key-remapping-in-mapped-types) 14 | 15 | ## `as` cast operator 16 | 17 | Added in [v1.6](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-1-6.html#new-tsx-file-extension-and-as-operator) 18 | 19 | This operator allows you to explicitly cast the type for a value of a different type without typescript raising an error. This is a dangerous a foot gun like feature that should be used with care and consciously. To quote the typescript docs this is a way to tell the compiler `trust me, I know what I’m doing.` 20 | 21 | This feature has two ways to use it. 22 | 23 | ```ts 24 | const a = notFoo as Foo; 25 | /** 26 | * The same thing 27 | * but wont work for `.tsx` files 28 | */ 29 | const b = notFoo; 30 | 31 | /** 32 | * now both `a` and `b` are of type `Foo` 33 | */ 34 | ``` 35 | 36 | ## Const assertions 37 | 38 | `const foo = {} as const` 39 | 40 | Added in [v3.4](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions) 41 | 42 | This feature allows you to disable type widening when declaring values in typescript. 43 | 44 | ```ts 45 | const plainObj = {a: 1, b: 'foo'}; 46 | 47 | plainObj; // {a: number; b: string} 48 | 49 | const constObj = {a: 1, b: 'foo'} as const; 50 | const constObjAlt = {a: 1, b: 'foo'}; 51 | 52 | constObj; // {readonly a: 1; readonly b: 'foo'} 53 | ``` 54 | 55 | This is **not** the same as using `Object.freeze` 56 | 57 | ```ts 58 | const constObj = {a: 1, b: 'foo', c: {d: 'bar'}} as const; 59 | 60 | constObj; // {readonly a: 1, readonly b: 'foo', readonly c: {readonly d: 'bar}} 61 | 62 | // @ts-expect-error Cannot assign to 'd' because it is a read-only property. 63 | constObj.c.d = 'foo' 64 | 65 | const frozen = Object.freeze({a: 1, b: 'foo', c: {d: 'bar'}}) 66 | 67 | frozen; // Readonly<{a: number; b: string; c: {d: string}}> 68 | 69 | // @ts-expect-error Cannot assign to 'b' because it is a read-only property. 70 | frozen.b = 'foo 2' 71 | 72 | // no error since `Readonly` is not deep 73 | frozen.c.d = 'foo' 74 | ``` 75 | 76 | The key things that happen when const assertions are being used are: 77 | 78 | - no literal types in that expression should be widened (e.g. no going from `"hello"` to `string`) 79 | - object literals get `readonly` properties 80 | - array literals become `readonly` tuples 81 | 82 | ## Assert functions 83 | 84 | Added in [v3.7](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions) 85 | 86 | Assert functions are similar to `type guards` with the only difference that the function throws instead of returning a falsy value. This works on par with nodejs [`assert`](https://nodejs.org/docs/latest/api/assert.html) module. 87 | 88 | Using assert function you can validate an input ie 89 | 90 | ```ts 91 | function plainAssertion(arg: unknown): asserts arg { 92 | if (!arg) { 93 | throw new Error(`arg is expected to be truthy, got "${arg}"`); 94 | } 95 | } 96 | 97 | function foo(input: boolean, item: string | null) { 98 | input; // boolean 99 | plainAssertion(input); 100 | input; // true 101 | 102 | item; // string | null 103 | plainAssertion(item); 104 | item; // string 105 | } 106 | ``` 107 | 108 | Alternatively you can narrow down the type to be more specific. This is when the similarity with `type guards` shows. 109 | 110 | ```ts 111 | function specificAssertion(arg: unknown): asserts arg is string { 112 | if (typeof arg !== 'string') { 113 | throw new Error(`arg is expected to be string, got "${arg}"`) 114 | } 115 | } 116 | 117 | function bar(input: string | null) { 118 | input; // string | null 119 | specificAssertion(input); 120 | input; // string 121 | } 122 | ``` 123 | 124 | ## Key remapping in mapped types 125 | 126 | ie `{[K in keyof T as Foo]: T[K]}` 127 | 128 | Added in [v4.1](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html#key-remapping-in-mapped-types) 129 | 130 | Reminder of mapped type syntax in typescript 131 | 132 | ```ts 133 | type Inlined = { 134 | [key in 'a' | 'b' | 'c']: string 135 | }; 136 | 137 | type Keys = 'a' | 'b' | 'c'; 138 | type Aliased = { 139 | [key in Keys]: string 140 | }; 141 | 142 | /** 143 | * both `Inlined` and `Aliased` have the type 144 | * `{a: string; b: string; c: string}` 145 | */ 146 | ``` 147 | 148 | Even though the example in the typescript docs present this feature to be useful in many cases when working with object keys. There are times when there is no need for it. 149 | 150 | From the typescript docs: 151 | 152 | ```ts 153 | // Remove the 'kind' property 154 | type RemoveKindField = { 155 | [K in keyof T as Exclude]: T[K] 156 | }; 157 | 158 | interface Circle { 159 | kind: "circle"; 160 | radius: number; 161 | } 162 | 163 | type KindlessCircle = RemoveKindField; 164 | // ^ = type KindlessCircle = { 165 | // radius: number; 166 | // } 167 | ``` 168 | 169 | This gets you the same result 170 | 171 | ```ts 172 | type RemoveKindField = { 173 | [K in Exclude]: T[K] 174 | }; 175 | ``` 176 | 177 | Where this feature shines is when you would like to remap the keys using template literal type with having the old key to extract the original value type. 178 | 179 | Example from typescript docs: 180 | 181 | ```ts 182 | type Getters = { 183 | [K in keyof T as `get${Capitalize}`]: () => T[K] 184 | }; 185 | 186 | interface Person { 187 | name: string; 188 | age: number; 189 | location: string; 190 | } 191 | 192 | type LazyPerson = Getters; 193 | // ^ = type LazyPerson = { 194 | // getName: () => string; 195 | // getAge: () => number; 196 | // getLocation: () => string; 197 | // } 198 | ``` 199 | -------------------------------------------------------------------------------- /posts/2025-11-30-diy-easymotion.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: DIY EasyMotion 3 | excerpt: I tried reimplementing an easy motion style jump feature in neovim and learned that you can do it in just 60 lines of lua. 4 | --- 5 | 6 | I tried reimplementing an easy motion style jump feature in neovim and learned that you can do it in just 60 lines of lua. 7 | 8 | ## Rationale 9 | 10 | Over a decade ago I used the [EasyMotion plugin](https://packagecontrol.io/packages/EasyMotion) in Sublime Text. About that time I started toying with vim. The built-in modal editing and navigation features were fascinating so I did not consider looking into easy motion plugins for vim for many years. A decade later I was reminded of EasyMotion and decided to include it for the rare occasions when it is more convenient. 11 | 12 | There are plenty of EasyMotion-like options available for neovim. The OGs [vim-easymotion](https://github.com/easymotion/vim-easymotion) and [vim-sneak](https://github.com/justinmk/vim-sneak) and some of the modern alternatives [leap.nvim](https://github.com/ggandor/leap.nvim), [flash.nvim](https://github.com/folke/flash.nvim), [mini-jump2d](https://github.com/nvim-mini/mini.nvim/blob/main/readmes/mini-jump2d.md). I've tried them all and while all work as advertised. None hit that sweet spot. Maybe my memory is hazy or the workflow changed is different now. Some use entire window than the current buffer split, others place labels over the character I want to jump to which breaks my mental flow. As a result, I decided to give it a try to re-implement it myself to my exact liking. 13 | 14 | Before we jump into implementation, for extra challenge I wanted to implement it per my memory without verifying if that's exactly how the sublime plugin works. My requirements were to jump to any character on the screen by typing the char to jump to, the next char, and the next char get's highlighted a _single_ label. 15 | 16 | For example, if I want to jump to the `I`, I type `I` followed by a space. That puts a label on the first letter of the next word. Now you type the label for the I that you want to jump to. 17 | 18 | ![easy motion demo](../assets/easy-motion.gif) 19 | 20 | The coolest part is that to implement this yourself you will only need to know about a handful of neovim APIs. 21 | 22 | * [`:h getchar()`](https://neovim.io/doc/user/vimfn.html#getchar()) - a way to block editor and get next typed character from user 23 | * [`:h extmarks`](https://neovim.io/doc/user/api.html#extmarks) - a way to pin something virtual and display it along the code without modifying the buffer content 24 | * [`:h vim.schedule()`](https://neovim.io/doc/user/lua.html#vim.schedule()) - schedule a callback to run on the next event loop iteration 25 | 26 | ## Make it work 27 | 28 | Now let's go into the implementation 29 | 30 | ```lua 31 | -- namespace for the extmarks. It will make it easier to clean up later 32 | local EASYMOTION_NS = vim.api.nvim_create_namespace('EASYMOTION_NS') 33 | -- Characters to use as labels. Note how we only use the letters from lower 34 | -- to upper case in ascending order of how easy to type them in qwerty layout 35 | local EM_CHARS = vim.split('fjdkslgha;rueiwotyqpvbcnxmzFJDKSLGHARUEIWOTYQPVBCNXMZ', '') 36 | 37 | local function easy_motion() 38 | -- implementation will go here 39 | end 40 | 41 | vim.keymap.set( 42 | { 'n', 'x' }, -- trigger it in normal and visual modes 43 | 'S', -- trigger search on 44 | easy_motion, 45 | { desc = 'Jump to 2 characters' } 46 | ) 47 | ``` 48 | 49 | From the requirements we already know how we want to implement the `easy_motion` function. 50 | 1. We get 2 characters typed by the user 51 | 2. Label all the matching positions on the screen 52 | 3. Listen for user input to pick the location to jump to 53 | 4. Jump to the location or cancel on any other key 54 | 5. Clean up the labels 55 | 56 | ```lua 57 | -- 1. Get 2 characters typed by the user 58 | 59 | -- since getchar() returns key code, we need to covert it to character string using `nr2char()` 60 | local char1 = vim.fn.nr2char(vim.fn.getchar()) 61 | local char2 = vim.fn.nr2char(vim.fn.getchar()) 62 | ``` 63 | 64 | ```lua 65 | -- 2. Label all the matching positions on the screen 66 | 67 | -- To locate characters on the screen, we need the screen boundaries. 68 | -- Buffer content does not always fit on the screen size 69 | 70 | -- First line displayed on the screen 71 | local line_idx_start = vim.fn.line('w0') 72 | -- Last line displayed on the screen 73 | local line_idx_end = vim.fn.line('w$') 74 | 75 | -- to keep track of labels to use 76 | local char_idx = 1 77 | -- dictionary of extmarks so we can refer back to picked location, from label char to location 78 | ---@type table 79 | local extmarks = {} 80 | -- lines on the screen 81 | -- `line_idx_start - 1` to convert from 1 based to 0 based index 82 | local lines = vim.api.nvim_buf_get_lines(bufnr, line_idx_start - 1, line_idx_end, false) 83 | -- the needle we are looking for 84 | local needle = char1 .. char2 85 | 86 | for line_i, line_text in ipairs(lines) do 87 | local line_idx = line_i + line_idx_start - 1 88 | 89 | -- since a single line can contain multiple matches, let's brute force each position 90 | for i = 1, #line_text do 91 | -- once we find a match, put an extmark there 92 | if line_text:sub(i, i + 1) == needle and char_idx <= #EM_CHARS then 93 | local overlay_char = EM_CHARS[char_idx] 94 | -- line number, `-2` to convert from 1 based to 0 based index 95 | local linenr = line_idx_start + line_i - 2 96 | -- column number, `-1` to convert from 1 based to 0 based index 97 | local col = i - 1 98 | -- set the extmark with virtual text overlay 99 | -- We specify the buffer, namespace, position, and options 100 | -- use `col + 2` to position the label 2 characters after the match 101 | local id = vim.api.nvim_buf_set_extmark(bufnr, EASYMOTION_NS, linenr, col + 2, { 102 | -- text we want to overlay the character 103 | virt_text = { { overlay_char, 'CurSearch' } }, 104 | -- how to position the virtual text, we use `overlay` to cover the existing content 105 | virt_text_pos = 'overlay', 106 | -- use `replace` to ignore the highlighting of the content below and only use the highlight group specified in `virt_text` 107 | hl_mode = 'replace', 108 | }) 109 | -- save the extmark info to jump to it if selected 110 | extmarks[overlay_char] = { line = linenr, col = col, id = id } 111 | -- increment the label index 112 | char_idx = char_idx + 1 113 | end 114 | end 115 | end 116 | ``` 117 | 118 | ```lua 119 | -- 3. Listen for user input to pick the location to jump to 120 | 121 | -- Before block editor to listen for user input, we need to allow neovim to draw the labels. 122 | -- We can user `vim.schedule` to allow neovim to process pending UI updates. 123 | vim.schedule(function() 124 | -- Get the next character typed by the user 125 | local pick_char = vim.fn.nr2char(vim.fn.getchar()) 126 | 127 | if extmarks[pick_char] then 128 | -- 4. Jump to the location 129 | local target = extmarks[pick_char] 130 | vim.api.nvim_win_set_cursor(0, { target.line + 1, target.col }) 131 | end 132 | 133 | -- 5. Clean up the labels 134 | vim.api.nvim_buf_clear_namespace(bufnr, EASYMOTION_NS, 0, -1) 135 | end) 136 | ``` 137 | 138 | ## Make it better 139 | 140 | At this point we have all the basics down. There are still a few more improvements we can make 141 | 142 | First let's handle **concealed lines**, no need to put search and label hidden content 143 | 144 | ```lua 145 | -- Make sure we only label visible lines 146 | if vim.fn.foldclosed(line_idx) == -1 then 147 | -- check line content ... 148 | end 149 | ``` 150 | 151 | I enjoy `smartcase` for search using `/`. Let's make our implementation act similar - enables case sensitivity if needle contains an uppercase character 152 | 153 | ```lua 154 | local needle = char1 .. char2 155 | -- if needle has any uppercase letters, make the search case sensitive 156 | local is_case_sensitive = needle ~= string.lower(needle) 157 | 158 | for line_i, line_text in ipairs(lines) do 159 | if not is_case_sensitive then 160 | -- for case insensitive search, convert line to lower case so we can match on all content 161 | line_text = string.lower(line_text) 162 | end 163 | 164 | -- check line content ... 165 | end 166 | ``` 167 | 168 | Sometimes you want to go back, `` does just that. We need to support jumplist. We need to add a mark in jump list, we can do it in a single line 169 | 170 | ```lua 171 | vim.cmd("normal! m'") 172 | ``` 173 | 174 | Right now we continue looking for matches even after we ran out of labels. Some plugins start using labels with multiple characters. I never have my terminal font size large enough. So let's exit early once we used up all labels. 175 | 176 | ```lua 177 | if char_idx > #EM_CHARS then 178 | break 179 | end 180 | ``` 181 | 182 | ## Final code 183 | 184 | Slightly cleaned up and just 60 lines of lua with all improvements included 185 | 186 | ```lua 187 | local EASYMOTION_NS = vim.api.nvim_create_namespace('EASYMOTION_NS') 188 | local EM_CHARS = vim.split('fjdkslgha;rueiwotyqpvbcnxmzFJDKSLGHARUEIWOTYQPVBCNXMZ', '') 189 | 190 | local function easy_motion() 191 | local char1 = vim.fn.nr2char( vim.fn.getchar() --[[@as number]] ) 192 | local char2 = vim.fn.nr2char( vim.fn.getchar() --[[@as number]] ) 193 | local line_idx_start, line_idx_end = vim.fn.line('w0'), vim.fn.line('w$') 194 | local bufnr = vim.api.nvim_get_current_buf() 195 | vim.api.nvim_buf_clear_namespace(bufnr, EASYMOTION_NS, 0, -1) 196 | 197 | local char_idx = 1 198 | ---@type table 199 | local extmarks = {} 200 | local lines = vim.api.nvim_buf_get_lines(bufnr, line_idx_start - 1, line_idx_end, false) 201 | local needle = char1 .. char2 202 | 203 | local is_case_sensitive = needle ~= string.lower(needle) 204 | 205 | for lines_i, line_text in ipairs(lines) do 206 | if not is_case_sensitive then 207 | line_text = string.lower(line_text) 208 | end 209 | local line_idx = lines_i + line_idx_start - 1 210 | -- skip folded lines 211 | if vim.fn.foldclosed(line_idx) == -1 then 212 | for i = 1, #line_text do 213 | if line_text:sub(i, i + 1) == needle and char_idx <= #EM_CHARS then 214 | local overlay_char = EM_CHARS[char_idx] 215 | local linenr = line_idx_start + lines_i - 2 216 | local col = i - 1 217 | local id = vim.api.nvim_buf_set_extmark(bufnr, EASYMOTION_NS, linenr, col + 2, { 218 | virt_text = { { overlay_char, 'CurSearch' } }, 219 | virt_text_pos = 'overlay', 220 | hl_mode = 'replace', 221 | }) 222 | extmarks[overlay_char] = { line = linenr, col = col, id = id } 223 | char_idx = char_idx + 1 224 | if char_idx > #EM_CHARS then 225 | goto break_outer 226 | end 227 | end 228 | end 229 | end 230 | end 231 | ::break_outer:: 232 | 233 | -- otherwise setting extmarks and waiting for next char is on the same frame 234 | vim.schedule(function() 235 | local next_char = vim.fn.nr2char(vim.fn.getchar() --[[@as number]]) 236 | if extmarks[next_char] then 237 | local pos = extmarks[next_char] 238 | -- to make work 239 | vim.cmd("normal! m'") 240 | vim.api.nvim_win_set_cursor(0, { pos.line + 1, pos.col }) 241 | end 242 | -- clear extmarks 243 | vim.api.nvim_buf_clear_namespace(0, EASYMOTION_NS, 0, -1) 244 | end) 245 | end 246 | 247 | vim.keymap.set({ 'n', 'x' }, 'S', easy_motion, { desc = 'Jump to 2 characters' }) 248 | ``` 249 | 250 | ## Conclusion 251 | 252 | I have two main takeaways from this exercise. Trying to implement an existing plugin functionality usually leaves you with a better understanding of available neovim APIs. If successful, you also end up with a more precise solution for your exact use case. 253 | 254 | Modern neovim is great. Happy hacking 255 | -------------------------------------------------------------------------------- /scripts/build-site.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as cp from 'child_process'; 3 | import * as path from 'path'; 4 | import matter from 'gray-matter'; 5 | import {marked, MarkedOptions, Token} from 'marked'; 6 | import {gfmHeadingId} from 'marked-gfm-heading-id'; 7 | import Prism from 'prismjs'; 8 | import {slug} from 'github-slugger'; 9 | import assert from 'assert'; 10 | import {fileURLToPath} from 'url'; 11 | 12 | const __filename = fileURLToPath(import.meta.url); 13 | const __dirname = path.dirname(__filename) 14 | 15 | const DIST_FOLDER = path.resolve(__dirname, '../dist'); 16 | const DIST_POSTS = path.resolve(__dirname, '../dist/post'); 17 | const DIST_WIKI = path.resolve(__dirname, '../dist/wiki'); 18 | const DIST_ASSETS = path.resolve(__dirname, '../dist/assets'); 19 | 20 | const SRC_POSTS = path.resolve(__dirname, '../posts'); 21 | const SRC_WIKI = path.resolve(__dirname, '../wiki'); 22 | const SRC_IMAGES = path.resolve(__dirname, '../images'); 23 | const SRC_ASSETS = path.resolve(__dirname, '../assets'); 24 | 25 | type PostMeta = { 26 | title?: string; 27 | date?: string; 28 | excerpt?: string; 29 | kind?: 'home' | string; 30 | } 31 | 32 | type ItemEntry = { 33 | href: string; 34 | title: string; 35 | excerpt: string; 36 | date: string; 37 | html: string; 38 | } 39 | 40 | // filepaths of posts that use codebloks with languages 41 | const prismCodeBlockSet = new Set(); 42 | 43 | // replace the heading content with a link to the heading id 44 | const getMarkedOpts = (filepath: string): MarkedOptions => ({ 45 | hooks: { 46 | options: {}, 47 | preprocess: x => x, 48 | postprocess: x => x, 49 | processAllTokens(tokens) { 50 | return tokens.map(t => { 51 | switch (t.type) { 52 | case 'code': 53 | if (t.lang === 'ts') { 54 | t.lang = 'typescript'; 55 | } 56 | if (t.lang && ['javascript', 'typescript', 'lua'].includes(t.lang)) prismCodeBlockSet.add(filepath) 57 | return t; 58 | case 'heading': 59 | if (t.tokens?.length == 1 && t.tokens[0].type === 'text') { 60 | const textToken = t.tokens[0]; 61 | const id = slug(t.text); 62 | const newTokens: Token[] = [{ 63 | type: 'link', 64 | raw: `${t.text}`, 65 | href: `#${id}`, 66 | text: t.text, 67 | tokens: [textToken], 68 | }]; 69 | 70 | return { 71 | ...t, 72 | tokens: newTokens, 73 | } 74 | } 75 | return t; 76 | default: 77 | return t 78 | } 79 | }); 80 | } 81 | } 82 | }); 83 | // add heading id to the heading itself 84 | marked.use(gfmHeadingId({})); 85 | 86 | const renderer = new marked.Renderer(); 87 | 88 | const prismGrammarMap: Prism.LanguageMap = { 89 | ...Prism.languages, 90 | lua: { 91 | comment: [/--\[\[.*\]\]/m, { 92 | pattern: /--.*/, 93 | greedy: true 94 | } ], 95 | string: { 96 | pattern: /(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/, 97 | greedy: true 98 | }, 99 | keyword: /\b(?:and|break|do|else|elseif|end|false|for|function|if|in|local|nil|not|or|repeat|return|then|true|until|while)\b/, 100 | function: /\b\w+(?=\s*\()/, 101 | number: /\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i, 102 | operator: /[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/, 103 | punctuation: /[{}[\];(),.:]/ 104 | } as Prism.Grammar, 105 | } as Prism.LanguageMap; 106 | 107 | renderer.code = ({text, lang}) => { 108 | const grammar = prismGrammarMap[lang ?? ''] || prismGrammarMap.markup; // Fallback to markup if language not found 109 | const highlighted = (lang && ['ts','tsx', 'typescript', 'javascript', 'lua'].includes(lang)) ? Prism.highlight(text, grammar, lang) : text; 110 | return `
${highlighted}
\n`; 111 | }; 112 | 113 | marked.setOptions({ renderer }); 114 | 115 | 116 | (async function main() { 117 | console.log('> creating dist folder'); 118 | if (fs.existsSync(DIST_FOLDER)) { 119 | await fs.promises.mkdir(DIST_FOLDER, { recursive: true }); 120 | } 121 | 122 | console.log('> creating dist folder'); 123 | await fs.promises.mkdir(DIST_POSTS, { recursive: true }); 124 | await fs.promises.mkdir(DIST_WIKI, { recursive: true }); 125 | await fs.promises.mkdir(DIST_ASSETS, { recursive: true }); 126 | 127 | await fs.promises.readdir(SRC_ASSETS).then( 128 | files => Promise.all( 129 | files.map(file => fs.promises.copyFile( 130 | path.resolve(SRC_ASSETS, file), 131 | path.resolve(DIST_ASSETS, file), 132 | )) 133 | ) 134 | ); 135 | 136 | await fs.promises.mkdir(DIST_FOLDER, { recursive: true }); 137 | 138 | // read the posts folder 139 | console.log('> reading posts'); 140 | const posts = await fs.promises.readdir(SRC_POSTS).then(x => x.sort().reverse()); 141 | 142 | console.log('> creating posts'); 143 | const postEntries: ItemEntry[] = []; 144 | for (const post of posts) { 145 | const filepath = path.resolve(SRC_POSTS, post); 146 | const postContent = await fs.promises.readFile(filepath, 'utf-8'); 147 | 148 | const parsed = matter(postContent); 149 | const html = await marked(parsed.content, getMarkedOpts(filepath)); 150 | 151 | assert.ok(parsed.data.title, `Post "${post}" is missing "title" fornt matter`); 152 | assert.ok(parsed.data.excerpt, `Post "${post}" is missing "exerpt" fornt matter`); 153 | let date = undefined; 154 | if (/^\d\d\d\d-\d\d-\d\d-/.test(post)) { 155 | date = post.slice(0, 10); 156 | } 157 | assert.ok(date, `Post "${post}" does not start from YYYY-MM-DD or is missing "date" front matter`); 158 | 159 | await fs.promises.writeFile( 160 | path.resolve(DIST_POSTS, post.replace('.md', '.html')), 161 | surroundWithHtml(html, parsed.data, filepath), 162 | ); 163 | postEntries.push({ 164 | href: `/post/${post.replace('.md', '.html')}`, 165 | title: parsed.data.title, 166 | date: date ?? parsed.data.date, 167 | excerpt: parsed.data.excerpt, 168 | html, 169 | }) 170 | }; 171 | 172 | console.log('> reading wiki'); 173 | const wikis = await fs.promises.readdir(SRC_WIKI).then(x => x.sort()); 174 | console.log('> creating wiki'); 175 | 176 | const wikiEntries: ItemEntry[] = []; 177 | for (const wiki of wikis) { 178 | const filepath = path.resolve(SRC_WIKI, wiki); 179 | const wikiContent = await fs.promises.readFile(filepath, 'utf-8'); 180 | 181 | const parsed = matter(wikiContent); 182 | const html = await marked(parsed.content, getMarkedOpts(filepath)); 183 | 184 | assert.ok(parsed.data.title, `Wiki "${wiki}" is missing "title" front matter`); 185 | // assert.ok(parsed.data.excerpt, `Wiki "${wiki}" is missing "exerpt" front matter`); 186 | 187 | await fs.promises.writeFile( 188 | path.resolve(DIST_WIKI, wiki.replace('.md', '.html')), 189 | surroundWithHtml(html, parsed.data, filepath), 190 | ); 191 | 192 | const date = cp.execSync( 193 | `git log --diff-filter=A --format="%ad" --date=format:"%Y-%m-%d" -- ${SRC_WIKI}/${wiki}` 194 | ).toString().trim(); 195 | 196 | wikiEntries.push({ 197 | href: `/wiki/${wiki.replace('.md', '.html')}`, 198 | title: parsed.data.title, 199 | excerpt: parsed.data.excerpt, 200 | html, 201 | date, 202 | }) 203 | } 204 | 205 | console.log('> creating home page index.html'); 206 | { 207 | const content = [ 208 | '

Posts

', 209 | '
    ', 210 | ...printEntrys(postEntries), 211 | '
', 212 | '

Wiki

', 213 | '
    ', 214 | ...printEntrys(wikiEntries), 215 | '
', 216 | ].join(''); 217 | await fs.promises.writeFile( 218 | path.resolve(DIST_FOLDER, 'index.html'), 219 | surroundWithHtml( 220 | content, 221 | { 222 | title: "Web dev and stuff", 223 | excerpt: "A collection of posts and wiki entries about web development and other stuff.", 224 | kind: 'home', 225 | }, 226 | 'index.html', 227 | ), 228 | ); 229 | } 230 | 231 | console.log('> copying images'); 232 | cp.execSync(`cp -a ${SRC_IMAGES} ${DIST_FOLDER}`); 233 | 234 | console.log('> done'); 235 | })() 236 | 237 | function printEntrys(entries: ItemEntry[]) { 238 | return entries 239 | .sort((a, b) => a.date < b.date ? 1 : -1) 240 | .map(entry => `
  • ${entry.title}${entry.date ? ` - ${entry.date}` : ''}
  • `); 241 | } 242 | 243 | const primsJsAssets = ``; 244 | 245 | function surroundWithHtml(content: string, data: PostMeta, filepath: string) { 246 | return ( 247 | ` 248 | 249 | 250 | 251 | 252 | ${data.title ?? "no-title"} 253 | ${data.excerpt ? `` : ""} 254 | 255 | 304 | ${prismCodeBlockSet.has(filepath) ? primsJsAssets : ''} 305 | 306 | 307 | 308 |
    309 | Web dev and stuff 310 | GitHub 311 | Twitter 312 |
    313 | 314 | <${data.kind === 'home' ? 'main' : 'article'} class="markdown-body"> 315 |

    ${data.title ?? "no-title"}

    316 | ${data.kind === 'home' ? `

    ${data.excerpt ?? 'no exerpt'}

    ` : ''} 317 | ${content} 318 | 319 | 320 | 321 | `); 322 | } 323 | -------------------------------------------------------------------------------- /wiki/flow-to-typescript-notes.md: -------------------------------------------------------------------------------- 1 | --- 2 | tags: [flow, typescript] 3 | title: Flow to typescript migration notes 4 | --- 5 | 6 | Core difference to note when switching from flow type system to typescript. I've been mainly using flow v0.83, if you used newer versions your experience may vary. 7 | 8 | ## Why static types 9 | 10 | - Documentation 11 | - Tests/contracts 12 | - DX 13 | - Type safety\* 14 | 15 | ## Preword 16 | 17 | ⚠️ - marks things to pay close attention to 18 | 19 | ## Types and guarantees 20 | 21 | ![working-with-typed-programs](https://user-images.githubusercontent.com/5817809/112736958-c8e84200-8f67-11eb-872a-216ca432c5c8.png) 22 | 23 | Types are about contract validation within your program. Unlike tests types have a chance to let the developer know what can go wrong before they finish the work. 24 | 25 | DX improvement - how few things people have to keep in their head while working on a project. Types help to reduce this load. 26 | 27 | ## Core difference 28 | 29 | Flow aims at being a sound type system. In other words flow might have false positives when raising an error. 30 | 31 | Typescript aims to be a complete type system. In other words typescript might have false negatives when raising an error. See [non-goals](https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals#non-goals). 32 | 33 | ## Language differences 34 | 35 | - Flow only provides type annotations 36 | - Typescript expands javascript syntax with type annotation and the following: 37 | - Enums 38 | - Non null assertions 39 | - `private` class properties(not `#name` syntax, it has both) 40 | - Decorators 41 | - [Parameter properties](https://www.typescriptlang.org/docs/handbook/2/classes.html#parameter-properties) 42 | - Abstract classes 43 | - ... **TODO** 44 | 45 | ## Structural/nominal vs structural only 46 | 47 | Flow has a mix of structural and [nominal types](https://en.wikipedia.org/wiki/Nominal_type_system)(classes use nominal types). 48 | 49 | ```typescript 50 | // @flow 51 | 52 | class Foo { 53 | method(input: string): number { return 42 } 54 | } 55 | 56 | class Bar { 57 | method(input: string): number { return 42 } 58 | } 59 | 60 | let foo: Foo = new Bar() // ERROR!! 61 | ``` 62 | 63 | Typescript uses purely [structural typing](https://en.wikipedia.org/wiki/Structural_type_system). 64 | 65 | ```typescript 66 | // typescript 67 | 68 | class Foo { 69 | method(input: string): number { return 42 } 70 | } 71 | 72 | class Bar { 73 | method(input: string): number { return 42 } 74 | } 75 | 76 | let foo: Foo = new Bar() // OK 77 | ``` 78 | 79 | 80 | 81 | ## Suppression comments 82 | 83 | Flow marks every unused suppression comment as unused if there's no error. If used `include_warnings=true` unused suppressions are marked as errors. 84 | 85 | Typescript allows to have `// @ts-ignore` anywhere yet doesn't report it as an error/warning when there's no error to suppress ⚠️. 86 | 87 | In [v3.9](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-9.html#-ts-expect-error-comments) a `// @ts-expect-error` was added which is reported as an error where there's no error to suppress. 88 | 89 | Both typescript and flow cannot suppress a specific error by error code or other methods. 90 | 91 | ## Maybe values 92 | 93 | Flow has maybe operator `?type` and typescript doesn't 94 | 95 | ``` 96 | // @flow 97 | 98 | type A = ?string 99 | 100 | // the same as 101 | type B = string | void | null 102 | ``` 103 | 104 | ```typescript 105 | // typescript 106 | 107 | type A = string | undefined | null 108 | ``` 109 | 110 | ## Dangerous types 111 | 112 | In flow `Object` type can be used to describe an object that can have any key and any value. 113 | 114 | ```typescript 115 | // @flow 116 | 117 | const obj: Object = {} 118 | 119 | // the same as {[string | number]: any} 120 | ``` 121 | 122 | In typescript `Object` type is the actual Object constructor, which means almost every value can assigned to it 123 | 124 | ```typescript 125 | // typescript 126 | 127 | const bool: Object = true // ok 128 | const str: Object = 'foo' // ok 129 | const num: Object = 123 // ok 130 | 131 | const nil: Object = null // err 132 | ``` 133 | 134 | If you need the "whatever" object type in typescript you can use the `object` type with lowercase o 135 | 136 | ```typescript 137 | // typescript 138 | 139 | const obj1: object = {} // ok 140 | 141 | const obj2: object = { 142 | a: 'string', 143 | b: true, 144 | c: 123, 145 | d: null, 146 | } // ok 147 | ``` 148 | 149 | ```typescript 150 | // @flow 151 | 152 | const obj1: Object = {} // ok 153 | 154 | const obj2: Object = { 155 | a: 'string', 156 | b: true, 157 | c: 123, 158 | d: null, 159 | } // ok 160 | ``` 161 | 162 | Other dangerous types can be `Function` or `{}`. See [ban-types](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/ban-types.md) eslint rule from typescript-eslint-plugin. 163 | 164 | ## Named arguments / keys 165 | 166 | In Flow one does not have to specify argument or object key names in order to provide a type annotation. 167 | 168 | ```typescript 169 | // @flow 170 | 171 | const func 172 | : string => void 173 | = (arg) => console.log(arg) 174 | 175 | const obj 176 | : {[string]: number} 177 | = {foo: 1} 178 | ``` 179 | 180 | Typescript marks this code with errors forcing users to provide a more explanatory names that will be used in type hints 181 | 182 | ```typescript 183 | // typescript 184 | 185 | const func 186 | : (arg: string) => void 187 | = (arg) => console.log(arg) 188 | 189 | const obj 190 | : {[key: string]: number} 191 | = {foo: 1} 192 | ``` 193 | 194 | Screen Shot 2021-03-28 at 5 00 30 PM 195 | 196 | ## JSDoc support 197 | 198 | Typescript has support for [JSDoc comments](https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html) and will show you them on type hits. 199 | 200 | ```typescript 201 | // typescript 202 | 203 | /** 204 | * Does cool stuff 205 | * 206 | * @example 207 | * 208 | * `foo('hey', 'you') => 'hey, you'` 209 | */ 210 | type Foo = (start: string, end: string) => string 211 | ``` 212 | 213 | Preview: 214 | 215 | ![Screen Shot 2021-03-28 at 10 12 19 PM](https://user-images.githubusercontent.com/5817809/112764820-f2aa7300-9012-11eb-9b34-277e5dc51311.png) 216 | 217 | ✅ It's a good idea to always include comments for utility functions. 218 | 219 | ## Intersection / spread object types 220 | 221 | In flow you can "merge" object types by using intersection or by spreading types. Spreads create an inexact object type therefore if we need an exact one we have to specify it with `{||}` syntax. 222 | 223 | ```typescript 224 | // @flow 225 | 226 | type A = {a: string} 227 | type B = {b: number} 228 | 229 | type AB_1 = A & B // {a: string, b: number} 230 | type AB_2 = {...A, ...B} // {a: string, b: number} 231 | 232 | type AB = {a: boolean, b: boolean} 233 | 234 | type AB_3 = {...A, ...B, ...AB} // {a: boolean, b: boolean} 235 | 236 | type AB_3_strict = {|...A, ...B, ...AB|} // {|a: boolean, b: boolean|} 237 | ``` 238 | 239 | In typescript we cannot spread object types though the experience is pretty similar. 240 | 241 | ```typescript 242 | // typescript 243 | 244 | type A = {a: string} 245 | type B = {b: number} 246 | 247 | type AB_1 = A & B // {a: string, b: number} 248 | 249 | type AB = {a: boolean, b: boolean} 250 | 251 | type AB_3 = A & B & AB // never 😳 252 | 253 | type FML = string & number // never 254 | ``` 255 | 256 | ```typescript 257 | // typescript 258 | 259 | interface A { 260 | a: string 261 | } 262 | 263 | interface B extends A { 264 | // ^ error 265 | a: number 266 | } 267 | // Interface 'B' incorrectly extends interface 'A'. 268 | // Types of property 'a' are incompatible. 269 | // Type 'number' is not assignable to type 'string'. 270 | ``` 271 | 272 | When intersecting object with the same key TS attemps to find the common type between the value types under the common key. 273 | ```typescript 274 | // typescript 275 | 276 | type Strs = 'A' | 'B' | 'C' 277 | type Str = 'A' 278 | 279 | type IntStr = Strs & Str 280 | 281 | const a: IntStr = 'A' // ok 282 | const b: IntStr = 'B' // error 283 | const c: IntStr = 'C' // error 284 | 285 | // =============== 286 | 287 | type A1 = {prop: 'A' | 'B' | 'C'} 288 | type A2 = {prop: 'A'} 289 | 290 | type Intersection = A1 & A2 291 | 292 | const a: Intersection = {prop: 'A'} // ok 293 | const b: Intersection = {prop: 'B'} // error 294 | const c: Intersection = {prop: 'C'} // error 295 | ``` 296 | 297 | To get a similar result as object spreads in flow we can add a utility type like `ShallowMerge` 298 | 299 | ```typescript 300 | // typescript 301 | 302 | type A = {a: string} 303 | type B = {b: number} 304 | 305 | type AB_1 = A & B // {a: string, b: number} 306 | 307 | type AB = {a: boolean, b: boolean} 308 | type ShallowMerge = Omit & B 309 | 310 | type AB_merged = ShallowMerge // {a: boolean, b: boolean} 311 | ``` 312 | 313 | ## Type casting 314 | 315 | ```typescript 316 | // @flow 317 | 318 | const str = 'foobar' 319 | 320 | str // string 321 | 322 | const func = ((str: any): Function) 323 | 324 | func() // ok 325 | ``` 326 | 327 | ```typescript 328 | // typescript 329 | 330 | const str = 'foobar' 331 | 332 | str // string 333 | 334 | const func = (str as any) as Function 335 | 336 | func() // ok 337 | ``` 338 | 339 | This feature should be avoided when possible. 340 | 341 | ## Difference between `void` & `undefined` in typescript 342 | 343 | In flow one uses `void` at all times. In typescript you use `undefined` where it is an expected value to be used and `void` otherwise. For example for a function which result should not be used. 344 | 345 | ```typescript 346 | // @flow 347 | 348 | const func 349 | : string => void 350 | = (arg) => console.log(arg) 351 | 352 | if (func()) { // ok 353 | } 354 | ``` 355 | 356 | ```typescript 357 | // typescript 358 | 359 | const func 360 | : (arg: string) => void 361 | = (arg) => console.log(arg) 362 | 363 | if (func()) { // error: An expression of type 'void' cannot be tested for truthiness.(1345) 364 | } 365 | 366 | const bar: undefined = void 0 367 | 368 | if (bar) { // ok 369 | } 370 | ``` 371 | 372 | ## Typed `this` 373 | 374 | In flow you cannot specify the type for `this` for functions 375 | 376 | In typescript you can specify the type by naming the first argument type `this` 377 | 378 | ```typescript 379 | // typescript in 380 | function HtmlPage(this: {redirect: (url: string) => void}, params: Record) { 381 | if (typeof params.id !== 'string') { 382 | this.redirect('/login') 383 | } 384 | 385 | // logic 386 | } 387 | 388 | // javascript out 389 | function HtmlPage(params) { 390 | if (typeof params.id !== 'string') { 391 | this.redirect('/login') 392 | } 393 | 394 | // logic 395 | } 396 | ``` 397 | 398 | ## `any` vs `mixed` in flow or `any` vs `unknown` in typescript 399 | 400 | `any` is a hack in both type systems ⚠️. It is both a subtype and a supertype of every type. This is why you can use it in any way 401 | 402 | ```typescript 403 | function foo(arg: any) { 404 | arg(null) 405 | 406 | arg(1, 2, 3) 407 | 408 | arg.toFixed(5) 409 | 410 | arg.map(console.log) 411 | 412 | arg.has(42) 413 | 414 | arg.then(someFunc) 415 | } // ok 416 | ``` 417 | 418 | There is a type to represent an unknown value in both type systems, it typescript it is called `unknown` and in flow we have `mixed` 419 | 420 | ```typescript 421 | // typescript 422 | 423 | function foo(arg: unknown) { 424 | if (typeof arg === 'function') { 425 | arg(null) 426 | 427 | arg(1, 2, 3) 428 | } 429 | 430 | if (typeof arg === 'number') { 431 | arg.toFixed(5) 432 | } 433 | 434 | if (Array.isArray(arg)) { 435 | arg.map(console.log) 436 | } 437 | 438 | if (arg instanceof Set) { 439 | arg.has(42) 440 | } 441 | 442 | if (arg instanceof Promise) { 443 | arg.then(someFunc) 444 | } 445 | } // ok 446 | ``` 447 | 448 | ⚠️ Avoid using `any` at all costs 449 | 450 | ## Type narrowing a.k.a. type refinement 451 | 452 | [typescript](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) 453 | 454 | [flow](https://flow.org/en/docs/lang/refinements/) 455 | 456 | Kinds of guards: 457 | - `typeof` guards can narrow the type to string/number/biging/boolean/symbol/undefined/object/function 458 | - Truthiness narrowing via `&&`/`||`/`if`/`!`, misses `0`/`NaN`/`''`/`0n`/`null`/`undefined` 459 | - Equality narrowing `===`/`==`/`!==`/`!=` 460 | - `instanceof` narrowing 461 | 462 | ```typescript 463 | // typescript 464 | function foo(arg: string) { 465 | if (['A', 'B'].includes(arg)) { 466 | arg // string 467 | } 468 | 469 | if (arg === 'A' || arg === 'B') { 470 | arg // 'A' | 'B' 471 | } 472 | } 473 | ``` 474 | 475 | ## Type guards 476 | 477 | Both [typescript](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates) and [flow](https://flow.org/en/docs/types/functions/#toc-predicate-functions) have type guards. 478 | 479 | Example: let's check that the given nullable variable is not in fact `null` 480 | 481 | ```typescript 482 | // @flow 483 | 484 | function isNonNullable(arg: string | null | void): boolean %checks { 485 | return arg != null 486 | } 487 | 488 | function foo(arg: ?string) { 489 | if (isNonNullable(arg)) { 490 | arg // string 491 | } 492 | } 493 | ``` 494 | 495 | ```typescript 496 | // typescript 497 | 498 | function isNonNullable(arg: string | null | undefined): arg is string { 499 | return arg != null 500 | // return typeof arg === 'string' 501 | } 502 | 503 | function foo(arg: string | null | undefined) { 504 | if (isNonNullable(arg)) { 505 | arg // string 506 | } 507 | } 508 | ``` 509 | 510 | ### Handling sloppy cases 511 | 512 | Flow forces you to check the type withing your type predicate 513 | 514 | ```typescript 515 | // @flow 516 | 517 | function isNonNullable(arg: string | null | void): boolean %checks { 518 | return true // <-- sloppy check 519 | } 520 | 521 | function foo(arg: ?string) { 522 | if (isNonNullable(arg)) { 523 | arg // ?string 524 | // ^ note the `?` 525 | } 526 | } 527 | ``` 528 | 529 | ```typescript 530 | // typescript 531 | 532 | function isNonNullable(arg: string | null | undefined): arg is string { 533 | return true 534 | } 535 | 536 | function foo(arg: string | null | undefined) { 537 | if (isNonNullable(arg)) { 538 | arg // string 539 | // ^^^^^^ still works 😳 540 | } 541 | } 542 | ``` 543 | 544 | ⚠️ Pay close attention to type predicates, typescript won't guard you from writing sloppy checks 545 | 546 | ## Type assertion 547 | 548 | ### Non-null assertion operator 549 | 550 | Unlike flow typescript expands javascript syntax. An example can be [non-null assertion operator](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#non-null-assertion-operator). Its usage does not affect the runtime, ie it can be dangerous ⚠️ 551 | 552 | ```typescript 553 | // typescript 554 | 555 | declare function foo(): null | {prop: string} 556 | 557 | foo().prop // error 558 | 559 | foo()!.prop // ok 560 | // ^ this 561 | 562 | // transpiled javascript 563 | foo().prop 564 | ``` 565 | 566 | ### Const assertions 567 | 568 | `const foo = {} as const` 569 | 570 | Added in [v3.4](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions) 571 | 572 | This feature allows you to disable type widening when declaring values in typescript. 573 | 574 | ```typescript 575 | // typescript 576 | 577 | const plainObj = {a: 1, b: 'foo'} 578 | 579 | plainObj // {a: number; b: string} 580 | 581 | const constObj = {a: 1, b: 'foo'} as const 582 | 583 | constObj // {readonly a: 1; readonly b: 'foo'} 584 | ``` 585 | 586 | This is **not** the same as using `Object.freeze` 587 | 588 | ```typescript 589 | // typescript 590 | const constObj = {a: 1, b: 'foo', c: {d: 'bar'}} as const 591 | 592 | constObj // {readonly a: 1, readonly b: 'foo', readonly c: {readonly d: 'bar}} 593 | 594 | // @ts-expect-error Cannot assign to 'd' because it is a read-only property. 595 | constObj.c.d = 'foo' 596 | 597 | // ------------------------ 598 | 599 | const frozen = Object.freeze({a: 1, b: 'foo', c: {d: 'bar'}}) 600 | 601 | frozen // Readonly<{a: number; b: string; c: {d: string}}> 602 | 603 | // @ts-expect-error Cannot assign to 'b' because it is a read-only property. 604 | frozen.b = 'foo 2' 605 | 606 | // no error since `Readonly` is not deep 607 | frozen.c.d = 'foo' 608 | ``` 609 | 610 | The key things that happen when const assertions are being used are: 611 | 612 | - no literal types in that expression should be widened (e.g. no going from `"hello"` to `string`) 613 | - object literals get `readonly` properties 614 | - array literals become `readonly` tuples 615 | 616 | ### Assert functions 617 | 618 | Added in [v3.7](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions) 619 | 620 | Assert functions are similar to `type guards` with the only difference that the function throws instead of returning a falsy value. This works on par with nodejs [`assert`](https://nodejs.org/docs/latest/api/assert.html) module. 621 | 622 | Using assert function you can validate an input ie 623 | 624 | ```ts 625 | // typescript 626 | 627 | function plainAssertion(arg: unknown): asserts arg { 628 | if (!arg) { 629 | throw new Error(`arg is expected to be truthy, got "${arg}"`) 630 | } 631 | } 632 | 633 | function foo(input: boolean, item: string | null) { 634 | input // boolean 635 | plainAssertion(input) 636 | input // true 637 | 638 | item // string | null 639 | plainAssertion(item) 640 | item // string 641 | } 642 | ``` 643 | 644 | Alternatively you can narrow down the type to be more specific. This is when the similarity with `type guards` shows. 645 | 646 | ```ts 647 | // typescript 648 | 649 | type Item = { 650 | type: 'item'; 651 | } 652 | 653 | function assertItem(arg: unknown): asserts arg is Item { 654 | if (isObject(arg) && 'type' in arg && arg.type === 'item') { 655 | return arg 656 | } 657 | 658 | throw new Error(`arg is expected to be an Item, got "${arg}"`) 659 | } 660 | 661 | function getItemById(state: State, id: string): Item | undefined { 662 | const item = state.collections.items[id] 663 | 664 | item // undefined | Item 665 | 666 | return item 667 | } 668 | 669 | function getItemByIdSafe(state: State, id: string): Item { 670 | const item = state.collections.items[id] 671 | 672 | item // undefined | Item 673 | 674 | assertItem(item) 675 | 676 | item // Item 677 | 678 | return item 679 | } 680 | ``` 681 | 682 | The same as plain type guards you don't have to validate the entire object scheme to guard other values. In other words the below code is OK for typescript standards ⚠️ 683 | 684 | ```typescript 685 | function assertWhatever(arg: unknown): asserts arg is Item { 686 | return undefined 687 | } 688 | 689 | function foo(arg: unknown) { 690 | arg // unknown 691 | 692 | assertWhatever(arg) 693 | 694 | arg // Item 695 | } 696 | ``` 697 | 698 | 699 | 700 | 701 | ## Type narrowing invalidation 702 | 703 | Flow has so called [refinement invalidations](https://flow.org/en/docs/lang/refinements/#toc-refinement-invalidations) 704 | 705 | ```typescript 706 | // @flow 707 | function func(value: { prop?: string }) { 708 | if (value.prop) { 709 | value.prop // string 710 | otherMethod() 711 | value.prop // string | void 712 | // $ExpectError 713 | value.prop.charAt(0) 714 | } 715 | } 716 | ``` 717 | 718 | Once we checked for `value.prop` value, the refined type is string. However if we call something within the current scope. Flow invalidated the refinement since it is possible that the object `value` was mutated within `otherMethod`. To avoid the invalidation one can extract the primitive value into its own variable ie 719 | 720 | ```typescript 721 | // @flow 722 | function func(value: { prop?: string }) { 723 | if (value.prop) { 724 | const {prop} = value 725 | prop // string 726 | otherMethod() 727 | prop // string 728 | value.prop // void | string 729 | prop.charAt(0) 730 | } 731 | } 732 | ``` 733 | 734 | Typescript is missing this feature on purpose since its [goals](https://github.com/microsoft/TypeScript/wiki/TypeScript-Design-Goals) aim at being a complete type system rather than sound. 735 | 736 | ## Strict vs loose objects 737 | 738 | Flow has syntax to specify whether the objects has a specified set of fields and nothing more aka strict or non extensible or is loose aka can have other non specified fields 739 | 740 | ```typescript 741 | // @flow 742 | type L = {a: number} 743 | const loose_1: L = {a: 1} // ok 744 | const loose_2: L = {a: 1, b: 'str', c: true} // ok 745 | 746 | type S = {|a: number|} 747 | const strict_1: L = {a: 1} // ok 748 | const strict_2: L = {a: 1, b: 'str', c: true} // error 749 | ``` 750 | 751 | In typescript objects are "strict" by default 752 | 753 | ```typescript 754 | // typescript 755 | type O = {a: number} 756 | const obj_1: O = {a: 1} // ok 757 | const obj_2: O = {a: 1, b: 'str', c: true} // error 758 | ``` 759 | 760 | However this does not always guarantee that typescript wont raise errors about unwanted fields. 761 | 762 | ```typescript 763 | // typescript 764 | 765 | type A = {a: number} 766 | 767 | declare function takesA(arg: A): void 768 | 769 | takesA({a: 1}) // ok 770 | takesA({a: 1, b: 'foo'}) // error 771 | 772 | // ----------------- 773 | 774 | const x = {a: 1, b: 'foo'} 775 | takesA(x) // ok 😳 776 | ``` 777 | 778 | According to typescript this is a valid code. [playground](https://www.typescriptlang.org/play?#code/C4TwDgpgBAggQlAvFA3gQwFxQEYHtcA2EaAdgDQ5Z6HEkC+AUKJFAMJKoDGV+RpjzaAFUAcgEkA8iI7woAHzYMGnXCQDOwKLmwArLKMnTkKbFmAAnAK4QK3KBet0gA) 779 | 780 | ```typescript 781 | type AB = {a: boolean, b: boolean} 782 | type C = {c: boolean} 783 | type UNION = AB | C 784 | 785 | const obj: UNION = {b: true, c: true} // ok 😳 786 | ``` 787 | 788 | ## Tuple Bugs 789 | 790 | Typescript [issue](https://github.com/microsoft/TypeScript/issues/6594#issuecomment-174315415) for context 791 | 792 | ```typescript 793 | // typescript 794 | 795 | const a: number[][] = [[1,2], [3,4]] 796 | const b: number[] = [1,2] 797 | const c: number[][] = a.concat(b) // no error 798 | 799 | c // typescript type `number[][]` 800 | c // runtime value `[[1,2], [3,4], 1, 2]` 801 | ``` 802 | 803 | [ts playground](https://www.typescriptlang.org/play?#code/MYewdgzgLgBAhgLhmArgWwEYFMBOBtAXUJgF4Y88BGAGgCYDryBmagFgIIChRJYMlUmXMTJU6XHtBjAB6bPiIFS8AHQ9gcKAAoMASk7cYAeiMwoATwAOWCMBwBLS7AvWYAA0HzChN4ZMwcFDAoezQsGAA3OAAbFHC3Chp6RjwWdkYaGHo3IA) 804 | [flow raises an error as expected](https://flow.org/try/#0MYewdgzgLgBAhgLhmArgWwEYFMBOBtAXUJgF4Y88BGAGgCYDryBmagFgIIChRJYMlUmXMTJU6XHtBjAB6bPiIFS8AHQ9gcKAAoMASk7cYAeiMwoATwAOWCMBwBLS7AvWYAA0HzChN4ZMwcFDAoezQsGAA3OAAbFHC3Chp6RjwWdkYaGHo3IA) 805 | 806 | ## Enums 807 | 808 | see [play](https://www.typescriptlang.org/play?#code/KYOwrgtgBACgNgQwJYigbwFBW1A9iYAGixwBcB3XYnKUgCwCdhgMBfDDUSKAOQQmAATdCWz5gUALxQA5OJnUylKbIq4Fo2o2YqZ9JsBlsOAY3wBnUlC7QAwhauYa43fMXY1utRpr6d0vW1DYwxSAE8ABwkAUXAIAGlgMPMVAGsk3AAzWkjgLNhEFABuKAB6UqgAInFKqAAfKrVahsq-YEqOTLAQE1IkfChM3FwACgjCkAAuAuQQQigQfiFpvgFBeZMzEEtp+23SAEoRGiRssYmpSQD5I7RWMorgBgZcBmn6CXColJl4WZkoAgQMIZJYGCgAOYAugIABuEhAuDw8IYiAiADoOCczuNZpdpH8UOjxLdNDRcSgHjMieJNOxNKcoCNFmt8bwloJiQRSTQaCyhFTVkIuSwaPTsUzNg42XtLCKebzsFL9lTZaQRXSQkNRnICApVJR9YEDDIjuVrM9Xu9crJqgRKgCkClEVYEOZzEgIYsAEZwT5IiIIBhLUhPPDZL4SX4TIwYbUjQkgEXzIWctTzNXotoHIA) 809 | 810 | ```typescript 811 | enum Plain { 812 | one, 813 | two, 814 | three 815 | } 816 | 817 | enum Named { 818 | one = 'one', 819 | two = 'two', 820 | three = 'three' 821 | } 822 | 823 | const enum Const { 824 | one = 'one', 825 | two = 'two', 826 | three = 'three' 827 | } 828 | 829 | type EnumKeys = keyof typeof Plain; // "one" | "two" | "three" 830 | 831 | function foo(plain: Plain, named: Named, cconst: Const) { 832 | if (plain === 'one') {} // error: the types 'Plain' and 'string' have no overlap. 833 | 834 | if (plain === Plain.one) { 835 | plain // Plain.one 836 | } 837 | 838 | if (named === Named.one) { 839 | named // Named.one 840 | } 841 | 842 | if (cconst === Const.one) { 843 | cconst // Const.one 844 | } 845 | } 846 | 847 | foo('one', 'two', 'three') // error: type '"one"' is not assignable to parameter of type 'Plain' 848 | 849 | foo(Plain.one, Named.two, Const.three) 850 | ``` 851 | 852 | ## Opaque types 853 | 854 | Flow has support for `opaque` type aliases. They are the same as regular type aliases but do not allow access to their underlying type outside of the file in which they are defined. 855 | 856 | ```typescript 857 | // @flow 858 | 859 | // a.js 860 | 861 | opaque type UserId = string 862 | 863 | type User = {id: UserId, name: string} 864 | 865 | declare function getUserById(id: UserId): User | void 866 | 867 | // b.js (has to be a different file) 868 | 869 | getUserById('1234') // error 870 | 871 | const someId: UserId = '4321' 872 | 873 | getUserById(someId) // ok 874 | ``` 875 | 876 | Typescript does not have such feature since it is not nominal yet you can get somewhat similar result 877 | 878 | ```typescript 879 | // typescript 880 | 881 | type Brand = K & { __brand: T } 882 | 883 | type UserId = Brand 884 | type User = {id: UserId, name: string} 885 | 886 | declare function getUserById(id: UserId): User | void 887 | 888 | getUserById('1234') // error 889 | 890 | const someId: UserId = '4321' 891 | // ^^^^^^ Type 'string' is not assignable to type '{ __brand: "userId"; }'. 892 | 893 | const castedId = '4321' as UserId // have to cast explicitly 894 | 895 | getUserById(castedId) // ok 896 | ``` 897 | 898 | This has an issue since these "branded" types cannot be used to index collections. 899 | 900 | ```typescript 901 | // typescript 902 | 903 | type UserCollection = Record 904 | 905 | const userCollection: UserCollection = {} 906 | 907 | let a = userCollection[castedId] 908 | // ^^^^^^^^^^^^^^^^^^^^^^^^ Element implicitly has an 'any' type because expression of 909 | // type 'UserId' can't be used to index type 'UserCollection' 910 | ``` 911 | 912 | ## Mapped types 913 | 914 | For the typical `$ObjMap` & `$ObjMapi` enjoyers, typescript cannot call functions at a type level therefore they have a syntax for mapping over a union 915 | 916 | ```typescript 917 | // typescript 918 | type Union = 'A' | 'B' | 'C' 919 | 920 | type Obj = { 921 | [K in Union]: K; 922 | } 923 | 924 | // typeof Obj -> {A: 'A'; B: 'B'; C: 'C'} 925 | ``` 926 | 927 | ## Built-in utils 928 | 929 | - Partial 930 | - Required 931 | - Readonly 932 | - Record 933 | - Pick 934 | - Omit 935 | - Exclude 936 | - Extract 937 | - NonNullable 938 | - Parameters 939 | - ConstructorParameters 940 | - ReturnType 941 | - InstanceType 942 | - ThisParameterType 943 | - OmitThisParameter 944 | - ThisType 945 | 946 | ```typescript 947 | // typescript 948 | 949 | type Record = { 950 | [P in K]: T; 951 | } 952 | 953 | const userCollection: Record = { 954 | '1': { 955 | id: '1', 956 | name: 'John Doe', 957 | } 958 | } 959 | 960 | // ====================== 961 | 962 | type Props = { 963 | active: boolean; 964 | className: string; 965 | } 966 | 967 | declare function MyComponent(props: Props): ReactNode 968 | 969 | type GetComponentProps ReactNode> = Parameters[0] 970 | 971 | type CompProps = GetComponentProps // Props 972 | ``` 973 | 974 | ## Generics syntax 975 | 976 | ### generic types 977 | 978 | ```typescript 979 | // @flow 980 | 981 | type ToTuple = [T] 982 | 983 | type ToStringTuple = [T] 984 | 985 | type ToDefaultToStringTuple = [T] 986 | 987 | type ToTogetherTuple = [T] 988 | 989 | type A = ToStringTuple<'A'> 990 | 991 | type B = ToDefaultToStringTuple<> 992 | 993 | type C = ToTogetherTuple 994 | 995 | const a: A = ['A'] 996 | 997 | const b: B = ['B'] 998 | 999 | const c: C = ['F'] 1000 | ``` 1001 | 1002 | ```typescript 1003 | // typescript 1004 | 1005 | type ToTuple = [T] 1006 | 1007 | type ToStringTuple = [T] 1008 | 1009 | type ToDefaultToStringTuple = [T] 1010 | 1011 | type ToTogetherTuple = [T] 1012 | 1013 | type A = ToStringTuple<'A'> 1014 | 1015 | type _B = ToDefaultToStringTuple<> 1016 | // ^^ error: cannot be empty 1017 | 1018 | type B = ToDefaultToStringTuple 1019 | 1020 | type C = ToTogetherTuple 1021 | 1022 | const a: A = ['A'] 1023 | 1024 | const b: B = ['B'] 1025 | 1026 | const c: C = ['F'] 1027 | ``` 1028 | 1029 | ### generic functions 1030 | 1031 | ```typescript 1032 | // @flow 1033 | 1034 | declare function foo(arg: T): {foo: T} 1035 | 1036 | declare function bar(arg: T): {bar: T} 1037 | 1038 | declare function baz(arg: T): {baz: T} 1039 | ``` 1040 | 1041 | ```typescript 1042 | // typescript 1043 | 1044 | declare function foo(arg: T): {foo: T} 1045 | 1046 | declare function bar(arg: T): {bar: T} 1047 | 1048 | declare function baz(arg: T): {baz: T} 1049 | ``` 1050 | 1051 | --- 1052 | 1053 | ```typescript 1054 | // @flow 1055 | 1056 | declare function easy(arg: T): T 1057 | 1058 | const aaa = easy({a: '', b: 42}) // ok 1059 | 1060 | aaa // {|a: string, b: number|} 1061 | 1062 | declare function strict(arg: T): T 1063 | 1064 | const bbb = strict({a: '', b: 42}) // error 1065 | 1066 | const ccc = strict({a: ''}) // ok 1067 | 1068 | ccc // {|a: string|} 1069 | ``` 1070 | 1071 | ```typescript 1072 | // typescript 1073 | 1074 | declare function func(arg: T): T 1075 | 1076 | const qlwerk = func({a: '', b: 42}) // ok 1077 | 1078 | qlwerk // {a: string, b: number} 1079 | ``` 1080 | 1081 | ## Generic type variance 1082 | 1083 | ```typescript 1084 | // @flow 1085 | 1086 | type A = {|a: string|} 1087 | 1088 | type AB = {|a: string, b: string|} 1089 | 1090 | type ABC = {|a: string, b: string, c: string|} 1091 | 1092 | declare function takesAB(arg: T): void 1093 | 1094 | declare var a__: A 1095 | declare var ab_: AB 1096 | declare var abc: ABC 1097 | 1098 | takesAB(a__) // error 1099 | 1100 | takesAB(ab_) // ok 1101 | 1102 | takesAB(abc) // error 1103 | ``` 1104 | 1105 | ⚠️ Typescript generics are covariant and there's nothing one can do about it 1106 | 1107 | ```typescript 1108 | // typescript 1109 | 1110 | type A = {a: string} 1111 | 1112 | type AB = {a: string, b: string} 1113 | 1114 | type ABC = {a: string, b: string, c: string} 1115 | 1116 | declare function takesAB(arg: T): void 1117 | 1118 | declare var a__: A 1119 | declare var ab_: AB 1120 | declare var abc: ABC 1121 | 1122 | takesAB(a__) // error 1123 | 1124 | takesAB(ab_) // ok 1125 | 1126 | takesAB(abc) // ok 1127 | ``` 1128 | 1129 | While in flow generics are invariant by default, but one [can specify](https://flow.org/en/docs/types/generics/#toc-variance-sigils) if they want it to behave covariant or contravariant. 1130 | 1131 | ## Type variance 1132 | 1133 | **Flow**: 1134 | - see [flow docs](https://flow.org/en/docs/lang/variance/) 1135 | - allows you to make types covariant or contravariant [docs](https://flow.org/en/docs/types/generics/#toc-variance-sigils) 1136 | 1137 | **Typescript** 1138 | 1139 | ```typescript 1140 | // typescript 1141 | 1142 | class Noun {} 1143 | class City extends Noun {} 1144 | class SanFrancisco extends City {} 1145 | 1146 | declare function method(value: City): void 1147 | 1148 | method(new Noun()) // ok 1149 | method(new City()) // ok 1150 | method(new SanFrancisco()) // ok 1151 | 1152 | // stucture check, {} === {} 1153 | method({}) // proof 1154 | 1155 | method([]) // 😳 1156 | 1157 | method('foo') // still an object 1158 | 1159 | method(null) // err 1160 | ``` 1161 | 1162 | ```typescript 1163 | // typescript 1164 | 1165 | class Noun { 1166 | count() { } 1167 | } 1168 | class City extends Noun {} 1169 | class SanFrancisco extends City {} 1170 | 1171 | method(new Noun()) // ok 1172 | 1173 | method(new City()) // ok 1174 | 1175 | method(new SanFrancisco()) // ok 1176 | 1177 | method({}) // error 1178 | 1179 | method({ count() {} }) // ok 1180 | method(Object.assign([], { count() {} })) // ok 1181 | ``` 1182 | 1183 | covariant like check 1184 | 1185 | ```typescript 1186 | // typescript 1187 | 1188 | class Noun { 1189 | constructor(public name: string) {} 1190 | } 1191 | class City extends Noun { 1192 | constructor(public name: string, public geo: number) { 1193 | super(name) 1194 | } 1195 | } 1196 | class SanFrancisco extends City { 1197 | constructor(public name: string, public geo: number, public whatever: string) { 1198 | super(name, geo) 1199 | } 1200 | } 1201 | 1202 | declare function method(value: City): void 1203 | 1204 | method(new Noun('moscow')) // error 1205 | method(new City('moscow', 42)) // ok 1206 | method(new SanFrancisco('moscow', 42, '')) // ok 1207 | ``` 1208 | 1209 | ### Conditional types 1210 | 1211 | ```typescript 1212 | // typescript 1213 | 1214 | type IsString = T extends string ? true : false 1215 | 1216 | type A = IsString // true 1217 | 1218 | type B = IsString<{}> // false 1219 | ``` 1220 | 1221 | ### Infer 1222 | 1223 | ```typescript 1224 | // typescript 1225 | 1226 | type ElementType = A extends Array ? U : never 1227 | 1228 | type A = ElementType // never 1229 | 1230 | type B = ElementType<['A', 'B']> // 'A' | 'B' 1231 | ``` 1232 | 1233 | ```typescript 1234 | // typescript 1235 | 1236 | declare function add(a: string, b: string): string 1237 | declare function add(a: number, b: number): number 1238 | declare function add(a: string | number, b: string | number): string | number 1239 | 1240 | type SillyResult = ReturnType // string | number 1241 | 1242 | type SmartReturnType> = F extends (...args: A) => infer R ? R : never 1243 | 1244 | type NotThatSilly = SmartReturnType // string | number 1245 | 1246 | type AtLeastWeHaveThis = SmartReturnType // never 1247 | ``` 1248 | 1249 | ```typescript 1250 | // @flow 1251 | 1252 | declare function add(a: string, b: string): string 1253 | declare function add(a: number, b: number): number 1254 | declare function add(a: string | number, b: string | number): string | number 1255 | 1256 | type Returns = $Call // string 1257 | ``` 1258 | --------------------------------------------------------------------------------