├── .babelrc ├── .eslintrc ├── .github ├── crush-pics │ └── config.json └── workflows │ ├── auto-merge.yml │ ├── codeql-analysis.yml │ ├── node.js.yml │ ├── optimize-images.yml │ └── ts-nightly-tests.yml ├── .gitignore ├── .mergepal.yml ├── .postcssrc ├── .prettierrc ├── .spelling ├── .vscode └── settings.json ├── API_EXAMPLES.http ├── DockerFile ├── LICENSE ├── README.md ├── assets └── img │ ├── angry-cat.jpg │ ├── avengers.jpg │ ├── boss.jpg │ ├── cat.jpg │ ├── clippy.png │ ├── colonel-meow.jpg │ ├── desk_flip.jpg │ ├── dilbert.jpg │ ├── drstrange.jpg │ ├── ironman.jpg │ ├── jquery.png │ ├── js.png │ ├── linkedin.png │ ├── lisa.jpeg │ ├── maru.jpg │ ├── microsoft.png │ ├── mike.jpeg │ ├── node.png │ ├── office97.png │ ├── thor.jpg │ └── ts.png ├── css └── app.pcss ├── db.json ├── index.html ├── notes ├── 00-intro.md ├── 01-project-tour.md ├── 02-recent-ts-features.md ├── 03-app-vs-library-concerns.md ├── 04-mikes-ts-setup.md ├── 05-what-have-I-done.md ├── 06-converting-to-ts.md ├── 07-dealing-with-pure-type-info.md ├── 08-types-at-runtime.md ├── 09-tests-for-types.md ├── 10-declaration-files.md ├── 11-api-extractor.md ├── README.md └── img │ ├── eslint-error.png │ ├── project_screenshot.png │ └── ts-3-essentials │ ├── slide-018.png │ ├── slide-019.png │ ├── slide-020.png │ └── slide-021.png ├── package.json ├── sandbox.config.json ├── server ├── api-server.js └── index.js ├── src ├── data │ ├── channels.js │ ├── messages.js │ └── teams.js ├── index.js ├── ui │ ├── App.jsx │ └── components │ │ ├── Channel.jsx │ │ ├── Channel │ │ ├── Footer.jsx │ │ ├── Header.jsx │ │ └── Message.jsx │ │ ├── Loading.jsx │ │ ├── SelectedChannel.jsx │ │ ├── SelectedTeam.jsx │ │ ├── Team.jsx │ │ ├── TeamSelector.jsx │ │ ├── TeamSelector │ │ └── TeamLink.jsx │ │ ├── TeamSidebar.jsx │ │ └── TeamSidebar │ │ └── ChannelLink.jsx └── utils │ ├── api.js │ ├── date.js │ ├── deferred.js │ ├── error.js │ ├── http-error.js │ └── networking.js ├── tailwind.config.js ├── tests └── components │ ├── Channel.test.jsx │ ├── ChannelFooter.test.jsx │ ├── ChannelHeader.test.jsx │ ├── ChannelMessage.test.jsx │ ├── TeamSelector.test.jsx │ ├── TeamSidebar.test.jsx │ └── __snapshots__ │ ├── Channel.test.jsx.snap │ ├── ChannelFooter.test.jsx.snap │ ├── ChannelHeader.test.jsx.snap │ ├── ChannelMessage.test.jsx.snap │ ├── TeamSelector.test.jsx.snap │ └── TeamSidebar.test.jsx.snap ├── tsconfig.eslint.json ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { "targets": { "node": "10" } }], 4 | "@babel/preset-react", 5 | "@babel/preset-typescript" 6 | ], 7 | "plugins": ["@babel/plugin-proposal-class-properties"] 8 | } 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2021": true 4 | }, 5 | "settings": { 6 | "react": { 7 | "version": "detect" 8 | } 9 | }, 10 | "extends": [ 11 | "eslint:recommended", 12 | "prettier", 13 | "plugin:promise/recommended", 14 | "plugin:sonarjs/recommended" 15 | ], 16 | "parser": "@typescript-eslint/parser", 17 | "parserOptions": { 18 | "ecmaFeatures": { 19 | "jsx": true 20 | }, 21 | "ecmaVersion": 12, 22 | "project": "tsconfig.eslint.json" 23 | }, 24 | "plugins": [ 25 | "react", 26 | "@typescript-eslint", 27 | "promise", 28 | "sonarjs", 29 | "prettier" 30 | ], 31 | "rules": { 32 | "prefer-const": "error" 33 | }, 34 | "overrides": [ 35 | /** 36 | * CLIENT SIDE CODE 37 | */ 38 | { 39 | "files": ["src/**/*.{ts,js,jsx,tsx}"], 40 | 41 | "env": { 42 | "browser": true, 43 | "es2021": true 44 | }, 45 | "rules": { 46 | "react/prop-types": "off", 47 | "react/no-children-prop": "off" 48 | }, 49 | "extends": [ 50 | "eslint:recommended", 51 | "plugin:react/recommended", 52 | "prettier/react" 53 | ] 54 | }, 55 | /** 56 | * SERVER SIDE CODE 57 | */ 58 | { 59 | "extends": ["plugin:node/recommended"], 60 | "files": [ 61 | "config/**/*.js", 62 | "babel.config.js", 63 | "tailwind.config.js", 64 | "postcss.config.js", 65 | "server/**/*.js" 66 | ], 67 | "env": { "commonjs": true, "node": true } 68 | }, 69 | /** 70 | * TYPESCRIPT CODE 71 | */ 72 | { 73 | "files": ["{src,tests}/**/*.{ts,tsx}"], 74 | "extends": [ 75 | "prettier/@typescript-eslint", 76 | "plugin:@typescript-eslint/recommended", 77 | "plugin:@typescript-eslint/recommended-requiring-type-checking" 78 | ], 79 | "rules": { 80 | "no-unused-vars": "off", 81 | "@typescript-eslint/no-unsafe-call": "off", 82 | "@typescript-eslint/restrict-template-expressions": "off", 83 | "@typescript-eslint/no-unsafe-member-access": "off", 84 | "@typescript-eslint/no-unsafe-assignment": "off", 85 | "@typescript-eslint/no-unsafe-return": "off", 86 | "@typescript-eslint/no-explicit-any": "off" 87 | } 88 | }, 89 | /** 90 | * TESTS 91 | */ 92 | { 93 | "files": ["tests/**/*.{js,jsx,ts,tsx}"], 94 | "extends": [], 95 | "env": { "node": true, "jest": true } 96 | } 97 | ] 98 | } 99 | -------------------------------------------------------------------------------- /.github/crush-pics/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "compression_mode": "balanced", 3 | "compression_level": 85, 4 | "strip_tags": false 5 | } 6 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Auto Merge 2 | 3 | on: 4 | push: {} # update PR when base branch is updated 5 | status: {} # try to merge when other checks are completed 6 | pull_request_review: # try to merge after review 7 | types: 8 | - submitted 9 | - edited 10 | - dismissed 11 | pull_request: # try to merge if labels have changed (white/black list) 12 | types: 13 | - labeled 14 | - unlabeled 15 | 16 | jobs: 17 | # thats's all. single step is needed - if PR is mergeable according to 18 | # branch protection rules it will be merged automatically 19 | mergepal: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v1 23 | - uses: maxkomarychev/merge-pal-action@v0.5.1 24 | with: 25 | token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [master] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [master] 14 | schedule: 15 | - cron: '0 21 * * 0' 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | # Override automatic language detection by changing the below list 26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 27 | language: ['javascript'] 28 | # Learn more... 29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v2 34 | with: 35 | # We must fetch at least the immediate parents so that if this is 36 | # a pull request then we can checkout the head. 37 | fetch-depth: 2 38 | 39 | # If this run was triggered by a pull request event, then checkout 40 | # the head of the pull request instead of the merge commit. 41 | - run: git checkout HEAD^2 42 | if: ${{ github.event_name == 'pull_request' }} 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | pull_request: 10 | branches: [master] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [10.x, 12.x, 14.x] 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v2 23 | - name: Cache Setup 24 | uses: actions/cache@v2 25 | with: 26 | path: | 27 | ~/cache 28 | ~/.dts/ 29 | !~/cache/exclude 30 | **/node_modules 31 | key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} 32 | 33 | - name: Use (Volta) Node.js ${{ matrix.node-version }} 34 | uses: volta-cli/action@v1 35 | with: 36 | node-version: ${{ matrix.node-version }} 37 | - name: Install dependencies 38 | run: yarn 39 | - name: Build 40 | run: yarn build 41 | # - name: Build Docs 42 | # run: yarn api-report && yarn api-docs 43 | - name: Run tests 44 | run: yarn test 45 | -------------------------------------------------------------------------------- /.github/workflows/optimize-images.yml: -------------------------------------------------------------------------------- 1 | name: Crush images 2 | on: 3 | pull_request: 4 | branches: 5 | - master 6 | paths: 7 | - '**.jpg' 8 | - '**.jpeg' 9 | - '**.png' 10 | - '**.gif' 11 | jobs: 12 | crush: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | - name: Crush images 18 | uses: crush-pics/crush-pics-github-action@master 19 | with: 20 | repo-token: ${{ secrets.GITHUB_TOKEN }} 21 | api-key: ${{ secrets.CRUSH_API_KEY }} 22 | -------------------------------------------------------------------------------- /.github/workflows/ts-nightly-tests.yml: -------------------------------------------------------------------------------- 1 | 2 | name: TypeScript@Next tests 3 | 4 | on: 5 | schedule: 6 | # * is a special character in YAML so you have to quote this string 7 | - cron: '20 19 * * *' 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [14.x] 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v2 21 | - name: Cache Setup 22 | uses: actions/cache@v2 23 | with: 24 | path: | 25 | ~/cache 26 | ~/.dts/ 27 | !~/cache/exclude 28 | **/node_modules 29 | key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} 30 | 31 | - name: Use (Volta) Node.js ${{ matrix.node-version }} 32 | uses: volta-cli/action@v1 33 | with: 34 | node-version: ${{ matrix.node-version }} 35 | - name: Install dependencies 36 | run: yarn 37 | - name: Install latest TypeScript nightly 38 | run: yarn add -D typescript@next 39 | - name: Build 40 | run: yarn build 41 | - name: Run tests 42 | run: yarn test 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | out 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | 111 | # yarn v2 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .yarn/install-state.gz 116 | .pnp.* 117 | 118 | .dist-types 119 | tsdoc-metadata.json 120 | temp -------------------------------------------------------------------------------- /.mergepal.yml: -------------------------------------------------------------------------------- 1 | whitelist: 2 | - merge-when-green 3 | blacklist: 4 | - wip 5 | - do-not-merge 6 | method: squash -------------------------------------------------------------------------------- /.postcssrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "tailwindcss": {}, 4 | "autoprefixer": {} 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "arrowParens": "always", 5 | "trailingComma": "all", 6 | "quoteProps": "consistent", 7 | "printWidth": 70 8 | } 9 | -------------------------------------------------------------------------------- /.spelling: -------------------------------------------------------------------------------- 1 | # markdown-spellcheck spelling configuration file 2 | # Format - lines beginning # are comments 3 | # global dictionary is at the start, file overrides afterwards 4 | # one word per line, to define a file override use ' - filename' 5 | # where filename is relative to this configuration file 6 | linter 7 | ts 8 | nullish 9 | falsy 10 | WeakMap 11 | variadic 12 | vscode 13 | JSDoc 14 | superset 15 | js 16 | UX 17 | runtime 18 | namespace 19 | JSON 20 | codebases 21 | i.e. 22 | refactorings 23 | es2020 24 | es2017 25 | APIs 26 | enums 27 | SemVer 28 | `.gitignore` 29 | volta 30 | tsconfig 31 | CommonJS 32 | es2018 33 | eslint 34 | config 35 | params 36 | npm 37 | webpack 38 | truthy 39 | ok 40 | fall-throughs 41 | codebase 42 | JSdoc 43 | CJS 44 | ESM 45 | interop 46 | node_modules 47 | Node.js 48 | codemod 49 | dtslint 50 | DefinitelyTyped 51 | stringified 52 | tslint 53 | subfolder 54 | tsd 55 | README 56 | initializers 57 | enum 58 | reassignable 59 | lodash 60 | React.js 61 | versioning 62 | tsx 63 | tsconfig.json 64 | rollup 65 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.enable": true, 3 | "files.associations": { 4 | "api-extractor.json": "jsonc" 5 | }, 6 | "vsicons.associations.files": [ 7 | { 8 | "icon": "tsconfig", 9 | "extensions": ["tsconfig.apidocs.json"], 10 | "filename": true, 11 | "format": "svg" 12 | } 13 | ], 14 | "json.schemas": [ 15 | { 16 | "fileMatch": ["tsconfig.apidocs.json"], 17 | 18 | "url": "https://json.schemastore.org/tsconfig" 19 | } 20 | ], 21 | "typescript.tsdk": "node_modules/typescript/lib" 22 | } 23 | -------------------------------------------------------------------------------- /API_EXAMPLES.http: -------------------------------------------------------------------------------- 1 | 2 | ### Get all users 3 | GET http://localhost:1234/api/users HTTP/1.1 4 | 5 | ### Get one user 6 | GET http://localhost:1234/api/users/1 HTTP/1.1 7 | 8 | ### Get all teams 9 | GET http://localhost:1234/api/teams HTTP/1.1 10 | 11 | ### Get a team (includes channels) 12 | GET http://localhost:1234/api/teams/li HTTP/1.1 13 | 14 | ### Get team channel messages 15 | GET http://localhost:1234/api/teams/li/channels/general/messages HTTP/1.1 16 | 17 | ### Create a new message in a team channel 18 | POST http://localhost:1234/api/messages HTTP/1.1 19 | Content-Type: application/json 20 | 21 | { 22 | "teamId": "li", 23 | "channelId": "general", 24 | "userId": 1, 25 | "body": "Hi everyone!" 26 | } 27 | 28 | ### Delete a message 29 | DELETE http://localhost:1234/api/messages/19 HTTP/1.1 30 | Content-Type: application/json 31 | -------------------------------------------------------------------------------- /DockerFile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/DockerFile -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 LinkedIn All rights reserved. 2 | 3 | Redistribution and use in 4 | source and binary forms, with or without modification, are permitted provided 5 | that the following conditions are met: 6 | 7 | 1. Redistributions of source code must 8 | retain the above copyright notice, this list of conditions and the following 9 | disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | THIS 16 | SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 17 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS 22 | OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 23 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 24 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN 25 | IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Professional TypeScript 2 | 3 | ![Node.js CI](https://github.com/mike-north/professional-ts/workflows/Node.js%20CI/badge.svg) 4 | ![ts-nightly compat](https://github.com/mike-north/professional-ts/workflows/TypeScript@Next%20tests/badge.svg) 5 | 6 | ## What's this course about? 7 | 8 | This course is intended to help those already somewhat familiar with TypeScript as a programming language, to the point where they're more ready to use it in a real situation with real stakes. Mike shares some of the experience he's had as LinkedIn's TypeScript infrastructure lead, so you don't have to learn things "the hard way" like he did. 9 | 10 | ## Project setup 11 | 12 | First, you should ensure you have [your ssh keys working with GitHub](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent). You can verify this by running 13 | 14 | ```sh 15 | ssh git@github.com 16 | ``` 17 | 18 | and getting a response like 19 | 20 | ```sh 21 | Hi mike-north! You've successfully authenticated, but GitHub does not provide shell access. 22 | Connection to github.com closed. 23 | ``` 24 | 25 | ### Tools you MUST have installed 26 | 27 | Next, make sure you have installed [volta](http://volta.sh/) which ensures you have the right version of node and yarn for this project 28 | 29 | We also strongly recommend the use of [Visual Studio Code](https://code.visualstudio.com/) as an authoring tool. If you use something else, you're on your own. 30 | 31 | ### Clone 32 | 33 | Next, checkout a working copy of this project 34 | 35 | ```sh 36 | git clone git@github.com:mike-north/professional-ts 37 | ``` 38 | 39 | enter the directory you just created 40 | 41 | ```sh 42 | cd professional-ts 43 | ``` 44 | 45 | ### Install dependencies 46 | 47 | [`yarn`](https://yarnpkg.com/) is the recommended package manager to use with this project. Please use it instead of npm. 48 | 49 | Install dependencies with yarn by running 50 | 51 | ```sh 52 | yarn 53 | ``` 54 | 55 | ### Starting the project 56 | 57 | Start up the project in development mode by running 58 | 59 | ```sh 60 | yarn dev 61 | ``` 62 | 63 | Changing any files in the `src` folder will result in an incremental rebuild, and a refresh of the screen. 64 | 65 | By default, the app is served on http://localhost:3000. 66 | 67 | # Legal 68 | 69 | © 2020 LinkedIn, All Rights Reserved 70 | 71 | ## Licensing 72 | 73 | The code in this project is licensed as [BSD-2-Clause](https://opensource.org/licenses/BSD-2-Clause) license, and the written content in the ./notes folder is licensed under [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/) 74 | -------------------------------------------------------------------------------- /assets/img/angry-cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/angry-cat.jpg -------------------------------------------------------------------------------- /assets/img/avengers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/avengers.jpg -------------------------------------------------------------------------------- /assets/img/boss.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/boss.jpg -------------------------------------------------------------------------------- /assets/img/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/cat.jpg -------------------------------------------------------------------------------- /assets/img/clippy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/clippy.png -------------------------------------------------------------------------------- /assets/img/colonel-meow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/colonel-meow.jpg -------------------------------------------------------------------------------- /assets/img/desk_flip.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/desk_flip.jpg -------------------------------------------------------------------------------- /assets/img/dilbert.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/dilbert.jpg -------------------------------------------------------------------------------- /assets/img/drstrange.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/drstrange.jpg -------------------------------------------------------------------------------- /assets/img/ironman.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/ironman.jpg -------------------------------------------------------------------------------- /assets/img/jquery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/jquery.png -------------------------------------------------------------------------------- /assets/img/js.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/js.png -------------------------------------------------------------------------------- /assets/img/linkedin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/linkedin.png -------------------------------------------------------------------------------- /assets/img/lisa.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/lisa.jpeg -------------------------------------------------------------------------------- /assets/img/maru.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/maru.jpg -------------------------------------------------------------------------------- /assets/img/microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/microsoft.png -------------------------------------------------------------------------------- /assets/img/mike.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/mike.jpeg -------------------------------------------------------------------------------- /assets/img/node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/node.png -------------------------------------------------------------------------------- /assets/img/office97.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/office97.png -------------------------------------------------------------------------------- /assets/img/thor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/thor.jpg -------------------------------------------------------------------------------- /assets/img/ts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/assets/img/ts.png -------------------------------------------------------------------------------- /css/app.pcss: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .hover-target .show-on-hover { 6 | opacity: 0; 7 | filter: alpha(opacity=0); 8 | } 9 | .hover-target:hover .show-on-hover, 10 | .hover-target .show-on-hover:focus, 11 | .hover-target .show-on-hover:active { 12 | opacity: 1; 13 | filter: alpha(opacity=1); 14 | } 15 | 16 | .sr-only { 17 | clip-path: inset(50%); 18 | clip: rect(1px, 1px, 1px, 1px); 19 | height: 1px; 20 | margin: -1px; 21 | overflow: hidden; 22 | padding: 0; 23 | position: absolute; 24 | width: 1px; 25 | } 26 | -------------------------------------------------------------------------------- /db.json: -------------------------------------------------------------------------------- 1 | { 2 | "teams": [ 3 | { 4 | "id": "linkedin", 5 | "name": "LinkedIn", 6 | "order": 2, 7 | "iconUrl": "/assets/img/linkedin.png" 8 | }, 9 | { 10 | "id": "ms", 11 | "name": "Microsoft", 12 | "order": 3, 13 | "iconUrl": "/assets/img/microsoft.png" 14 | }, 15 | { 16 | "id": "avengers", 17 | "name": "Avengers", 18 | "order": 4, 19 | "iconUrl": "/assets/img/avengers.jpg" 20 | }, 21 | { 22 | "id": "angrycat", 23 | "name": "Angry Cat", 24 | "order": 5, 25 | "iconUrl": "/assets/img/angry-cat.jpg" 26 | }, 27 | { 28 | "id": "javascript", 29 | "name": "Javascript", 30 | "order": 6, 31 | "iconUrl": "/assets/img/js.png" 32 | } 33 | ], 34 | "users": [ 35 | { 36 | "id": 0, 37 | "name": "Dilbert", 38 | "username": "dilbert", 39 | "iconUrl": "/assets/img/dilbert.jpg" 40 | }, 41 | { 42 | "id": 1, 43 | "name": "Mike North", 44 | "username": "mike", 45 | "iconUrl": "/assets/img/mike.jpeg" 46 | }, 47 | { 48 | "id": 2, 49 | "name": "Lisa Huang-North", 50 | "username": "lisa", 51 | "iconUrl": "/assets/img/lisa.jpeg" 52 | }, 53 | { 54 | "id": 3, 55 | "name": "Clippy", 56 | "username": "clippy", 57 | "iconUrl": "/assets/img/clippy.png" 58 | }, 59 | { 60 | "id": 4, 61 | "name": "office97", 62 | "username": "office", 63 | "iconUrl": "/assets/img/office97.png" 64 | }, 65 | { 66 | "id": 5, 67 | "name": "Tony Stark", 68 | "username": "ironman", 69 | "iconUrl": "/assets/img/ironman.jpg" 70 | }, 71 | { 72 | "id": 6, 73 | "name": "Thor", 74 | "username": "thor", 75 | "iconUrl": "/assets/img/thor.jpg" 76 | }, 77 | { 78 | "id": 7, 79 | "name": "Dr Stephen Strange", 80 | "username": "strange", 81 | "iconUrl": "/assets/img/drstrange.jpg" 82 | }, 83 | { 84 | "id": 8, 85 | "name": "Lil Bub", 86 | "username": "bub", 87 | "iconUrl": "/assets/img/cat.jpg" 88 | }, 89 | { 90 | "id": 9, 91 | "name": "Colonel Meow", 92 | "username": "meow", 93 | "iconUrl": "/assets/img/colonel-meow.jpg" 94 | }, 95 | { 96 | "id": 10, 97 | "name": "Maru", 98 | "username": "maru", 99 | "iconUrl": "/assets/img/maru.jpg" 100 | }, 101 | { 102 | "id": 11, 103 | "name": "NodeJS", 104 | "username": "nodejs", 105 | "iconUrl": "/assets/img/node.png" 106 | }, 107 | { 108 | "id": 12, 109 | "name": "Engineer Anon", 110 | "username": "anonymous", 111 | "iconUrl": "/assets/img/desk_flip.jpg" 112 | }, 113 | { 114 | "id": 13, 115 | "name": "Typescript", 116 | "username": "typescript", 117 | "iconUrl": "/assets/img/ts.png" 118 | }, 119 | { 120 | "id": 14, 121 | "name": "jQuery", 122 | "username": "jquery", 123 | "iconUrl": "/assets/img/jquery.png" 124 | }, 125 | { 126 | "id": 15, 127 | "name": "boss man", 128 | "username": "boss", 129 | "iconUrl": "/assets/img/boss.jpg" 130 | } 131 | ], 132 | "channels": [ 133 | { 134 | "id": "recruiting", 135 | "name": "recruiting", 136 | "description": "The Next Generation Of Recruiting. Find top talents today!", 137 | "teamId": "linkedin" 138 | }, 139 | { 140 | "id": "general", 141 | "name": "general", 142 | "description": "LinkedIn general chat", 143 | "teamId": "linkedin" 144 | }, 145 | { 146 | "id": "jobs", 147 | "name": "Job hunting", 148 | "description": "Discover companies that fit you.", 149 | "teamId": "linkedin" 150 | }, 151 | { 152 | "id": "tbt", 153 | "name": "throw back thursday", 154 | "description": "Remember the good old days? yay, us too.", 155 | "teamId": "ms" 156 | }, 157 | { 158 | "id": "endgame", 159 | "name": "top secret", 160 | "description": "for your eyes only", 161 | "teamId": "avengers" 162 | }, 163 | { 164 | "id": "dominate", 165 | "name": "catnip", 166 | "description": "exchange tips and best practicse on world domination", 167 | "teamId": "angrycat", 168 | "isDisabled": true 169 | }, 170 | { 171 | "id": "funny", 172 | "name": "funny", 173 | "description": "think you got what it takes? Share your best memes / jokes here!", 174 | "teamId": "javascript" 175 | } 176 | ], 177 | "messages": [ 178 | { 179 | "teamId": "linkedin", 180 | "channelId": "compliments", 181 | "userId": 2, 182 | "createdAt": "2019-04-21T17:48:33.421Z", 183 | "body": "Your penmanship is excellent!", 184 | "id": 1 185 | }, 186 | { 187 | "teamId": "linkedin", 188 | "channelId": "compliments", 189 | "userId": 1, 190 | "createdAt": "2019-04-21T17:54:38.556Z", 191 | "body": "I admire your punctuality", 192 | "id": 2 193 | }, 194 | { 195 | "teamId": "linkedin", 196 | "channelId": "general", 197 | "userId": 2, 198 | "createdAt": "2019-04-21T17:55:08.713Z", 199 | "body": "Hello shlack!", 200 | "id": 3 201 | }, 202 | { 203 | "teamId": "linkedin", 204 | "channelId": "general", 205 | "userId": 1, 206 | "createdAt": "2019-04-21T18:36:30.995Z", 207 | "body": "awda", 208 | "id": 11 209 | }, 210 | { 211 | "teamId": "linkedin", 212 | "channelId": "general", 213 | "userId": 1, 214 | "createdAt": "2019-04-21T18:40:34.648Z", 215 | "body": "wad", 216 | "id": 12 217 | }, 218 | { 219 | "teamId": "linkedin", 220 | "channelId": "general", 221 | "userId": 1, 222 | "body": "wdw", 223 | "createdAt": 1555872178322, 224 | "id": 13 225 | }, 226 | { 227 | "teamId": "linkedin", 228 | "channelId": "general", 229 | "userId": 1, 230 | "body": "awqwdqwqwdq", 231 | "enrichedBody": ["awqwdqwqwdq"], 232 | "createdAt": 1555872270175, 233 | "id": 14 234 | }, 235 | { 236 | "teamId": "linkedin", 237 | "channelId": "general", 238 | "userId": 1, 239 | "body": "qdq", 240 | "createdAt": 1555872790592, 241 | "id": 15 242 | }, 243 | { 244 | "teamId": "linkedin", 245 | "channelId": "general", 246 | "userId": 1, 247 | "body": "w", 248 | "createdAt": 1555872792437, 249 | "id": 16 250 | }, 251 | { 252 | "teamId": "linkedin", 253 | "channelId": "general", 254 | "userId": 2, 255 | "body": "Would you like to join my professional network?", 256 | "createdAt": 1555874498634, 257 | "id": 17 258 | }, 259 | { 260 | "teamId": "linkedin", 261 | "channelId": "general", 262 | "userId": 1, 263 | "body": "Hello developer, I looked at your profile and am impressed by your 14 years of COBOL experience. Are you happy in your current role?", 264 | "createdAt": 1555874584752, 265 | "id": 18 266 | }, 267 | { 268 | "channelId": "bofny", 269 | "teamId": "avengers", 270 | "body": "Hey dudes", 271 | "userId": "3", 272 | "createdAt": 1556676377508, 273 | "id": 19 274 | }, 275 | { 276 | "channelId": "funny", 277 | "teamId": "javascript", 278 | "body": "\"How do you comfort a JavaScript bug?\" ", 279 | "userId": "14", 280 | "createdAt": 1556679721022, 281 | "id": 20 282 | }, 283 | { 284 | "channelId": "funny", 285 | "teamId": "javascript", 286 | "body": "I dunno.. you de-bug it?", 287 | "userId": "12", 288 | "createdAt": 1556679740793, 289 | "id": 21 290 | }, 291 | { 292 | "channelId": "funny", 293 | "teamId": "javascript", 294 | "body": "No man, You console it", 295 | "userId": "14", 296 | "createdAt": 1556679745885, 297 | "id": 22 298 | }, 299 | { 300 | "channelId": "funny", 301 | "teamId": "javascript", 302 | "body": " Why was the JavaScript developer sad?", 303 | "userId": "14", 304 | "createdAt": 1556679754017, 305 | "id": 23 306 | }, 307 | { 308 | "channelId": "funny", 309 | "teamId": "javascript", 310 | "body": "Because there are too many JS frameworks!", 311 | "userId": "11", 312 | "createdAt": 1556679782382, 313 | "id": 24 314 | }, 315 | { 316 | "channelId": "funny", 317 | "teamId": "javascript", 318 | "body": "Wrong! It's because he didn’t Node how to Express himself", 319 | "userId": "14", 320 | "createdAt": 1556679797050, 321 | "id": 25 322 | }, 323 | { 324 | "channelId": "funny", 325 | "teamId": "javascript", 326 | "body": "ha-ha", 327 | "userId": "11", 328 | "createdAt": 1556679800867, 329 | "id": 26 330 | }, 331 | { 332 | "channelId": "funny", 333 | "teamId": "javascript", 334 | "body": "Ok here's one: Why do C# and Java developers keep breaking their keyboards?", 335 | "userId": "12", 336 | "createdAt": 1556679820803, 337 | "id": 27 338 | }, 339 | { 340 | "channelId": "funny", 341 | "teamId": "javascript", 342 | "body": "Mmm... because one of them tried to write a Hello World application in Java EE?", 343 | "userId": "14", 344 | "createdAt": 1556679939014, 345 | "id": 28 346 | }, 347 | { 348 | "channelId": "funny", 349 | "teamId": "javascript", 350 | "body": "Nah, it's because they both use strongly typed language", 351 | "userId": "12", 352 | "createdAt": 1556680157584, 353 | "id": 29 354 | }, 355 | { 356 | "channelId": "funny", 357 | "teamId": "javascript", 358 | "body": "Seriously.", 359 | "userId": "13", 360 | "createdAt": 1556680172998, 361 | "id": 30 362 | }, 363 | { 364 | "channelId": "endgame", 365 | "teamId": "avengers", 366 | "body": "Hey... has someone seen my hammer? I can't find it and I need a can opener...", 367 | "userId": "6", 368 | "createdAt": 1556680603106, 369 | "id": 31 370 | }, 371 | { 372 | "channelId": "endgame", 373 | "teamId": "avengers", 374 | "body": "No, if you remember the exact date and time you last used it, I can open a time portal for you", 375 | "userId": "7", 376 | "createdAt": 1556680649251, 377 | "id": 32 378 | }, 379 | { 380 | "channelId": "endgame", 381 | "teamId": "avengers", 382 | "body": "I'm pretty sure I saw whats-his-name take it. You might want to work out more or the hammer might choose *him* instead", 383 | "userId": "5", 384 | "createdAt": 1556680753815, 385 | "id": 33 386 | }, 387 | { 388 | "channelId": "endgame", 389 | "teamId": "avengers", 390 | "body": "NO! I SHALL BRING THUNDER DOWN ON HIM!", 391 | "userId": "6", 392 | "createdAt": 1556680812491, 393 | "id": 34 394 | }, 395 | { 396 | "channelId": "endgame", 397 | "teamId": "avengers", 398 | "body": "With which hand? Are you giving up the beer or Fortnite?", 399 | "userId": "5", 400 | "createdAt": 1556680973259, 401 | "id": 35 402 | }, 403 | { 404 | "channelId": "dominate", 405 | "teamId": "angrycat", 406 | "body": "Meowhahaha! I crossed the 800K followers mark today! One step closer to world domination 😈", 407 | "userId": "8", 408 | "createdAt": 1556681392393, 409 | "id": 36 410 | }, 411 | { 412 | "channelId": "dominate", 413 | "teamId": "angrycat", 414 | "body": "What did you cats get up to today?", 415 | "userId": "8", 416 | "createdAt": 1556681406684, 417 | "id": 37 418 | }, 419 | { 420 | "channelId": "dominate", 421 | "teamId": "angrycat", 422 | "body": "Oh and Penguin approached me for another book deal today. Apparently my last book has been hailed \"a literary classic\"...", 423 | "userId": "8", 424 | "createdAt": 1556681504008, 425 | "id": 38 426 | }, 427 | { 428 | "channelId": "dominate", 429 | "teamId": "angrycat", 430 | "body": "Can you believe these human?", 431 | "userId": "8", 432 | "createdAt": 1556681514248, 433 | "id": 39 434 | }, 435 | { 436 | "channelId": "dominate", 437 | "teamId": "angrycat", 438 | "body": "Pfff, 800K on what network? I have 400K fans on Facebook and they hail me as their \"fearsome dictator\", a \"prodigious Scotch drinker\" and \"the angriest cat in the world\"", 439 | "userId": "9", 440 | "createdAt": 1556681660966, 441 | "id": 40 442 | }, 443 | { 444 | "channelId": "dominate", 445 | "teamId": "angrycat", 446 | "body": "AND I hold a Guinness world record 🏆, beat that Bub 🐈", 447 | "userId": "9", 448 | "createdAt": 1556681701911, 449 | "id": 41 450 | }, 451 | { 452 | "channelId": "dominate", 453 | "teamId": "angrycat", 454 | "body": "*stretches* not that anyone's counting, but my videos have been been viewed over 325 million times on Youtube. ", 455 | "userId": "10", 456 | "createdAt": 1556681886829, 457 | "id": 42 458 | }, 459 | { 460 | "channelId": "dominate", 461 | "teamId": "angrycat", 462 | "body": "Holding a Guinness World Record doesn't get us closer to world domination, meow. We need to think outside the box", 463 | "userId": "10", 464 | "createdAt": 1556681916083, 465 | "id": 43 466 | }, 467 | { 468 | "channelId": "dominate", 469 | "teamId": "angrycat", 470 | "body": "But I looooooove cupboard boxes! ", 471 | "userId": "8", 472 | "createdAt": 1556681928716, 473 | "id": 44 474 | }, 475 | { 476 | "channelId": "tbt", 477 | "teamId": "ms", 478 | "body": "Sigh, I miss having a job...", 479 | "userId": "3", 480 | "createdAt": 1556682112281, 481 | "id": 45 482 | }, 483 | { 484 | "channelId": "tbt", 485 | "teamId": "ms", 486 | "body": "It was so nice to \"talk\" and help people around the world with their spelling mistakes!", 487 | "userId": "3", 488 | "createdAt": 1556682143644, 489 | "id": 46 490 | }, 491 | { 492 | "channelId": "tbt", 493 | "teamId": "ms", 494 | "body": "Don't you miss it?", 495 | "userId": "3", 496 | "createdAt": 1556682148707, 497 | "id": 47 498 | }, 499 | { 500 | "channelId": "tbt", 501 | "teamId": "ms", 502 | "body": "Tell me about it, they were so happy when they found the pinball game in Word 97", 503 | "userId": "4", 504 | "createdAt": 1556682251128, 505 | "id": 48 506 | }, 507 | { 508 | "channelId": "tbt", 509 | "teamId": "ms", 510 | "body": "OHH and that flight simulator game in Excel! People always tried to shoot me for some reason", 511 | "userId": "3", 512 | "createdAt": 1556682314145, 513 | "id": 49 514 | }, 515 | { 516 | "channelId": "tbt", 517 | "teamId": "ms", 518 | "body": "At least people remembers you and try to bring you back. I don't think anyone even use floppy disks installation any more...", 519 | "userId": "4", 520 | "createdAt": 1556682392864, 521 | "id": 50 522 | }, 523 | { 524 | "channelId": "tbt", 525 | "teamId": "ms", 526 | "body": "I don't miss Windows 95 though. Dang he was slow!", 527 | "userId": "4", 528 | "createdAt": 1556682501011, 529 | "id": 51 530 | }, 531 | { 532 | "channelId": "recruiting", 533 | "teamId": "linkedin", 534 | "body": "Hey guys, looking for a ninja rockstar software engineer. Must have 15 years of Rust experience and willing to take 80% compensation in equity. We have free lunches and table tennis in the office.", 535 | "userId": "15", 536 | "createdAt": 1556682816711, 537 | "id": 52 538 | }, 539 | { 540 | "channelId": "recruiting", 541 | "teamId": "linkedin", 542 | "body": "Oh and the position is in Antarctica. Low income tax rate, free snow cones and the skiing is great!", 543 | "userId": "15", 544 | "createdAt": 1556682870302, 545 | "id": 53 546 | }, 547 | { 548 | "channelId": "jobs", 549 | "teamId": "linkedin", 550 | "body": "LF new position. Our tech lead just decided to go with a COBOL/CoffeeScript stack and I'm getting out of here!!", 551 | "userId": "2", 552 | "createdAt": 1556682931336, 553 | "id": 54 554 | }, 555 | { 556 | "channelId": "recruiting", 557 | "teamId": "linkedin", 558 | "body": "#ask Does anyone know the difference between Java and Javascript? ", 559 | "userId": "0", 560 | "createdAt": 1556682976168, 561 | "id": 55 562 | }, 563 | { 564 | "channelId": "recruiting", 565 | "teamId": "linkedin", 566 | "body": "Ah nvm, probably doesn't matter. I'll just spray and pray", 567 | "userId": "0", 568 | "createdAt": 1556682987555, 569 | "id": 56 570 | }, 571 | { 572 | "channelId": "jobs", 573 | "teamId": "linkedin", 574 | "body": "Somebody just gave me a LinkedIn endorsement for \"Copying & Pasting from Stackoverflow\" and \"Private API usage.\" Are they trying to tell me something?", 575 | "userId": "1", 576 | "createdAt": 1556683077121, 577 | "id": 57 578 | } 579 | ] 580 | } 581 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /notes/00-intro.md: -------------------------------------------------------------------------------- 1 | # Welcome to Professional TypeScript 2 | 3 | ### What is TypeScript? How does Mike think about it? 4 | 5 | TypeScript aims to be a typed semantic superset of JavaScript. It is not a 6 | syntactic superset, which means that NOT ALL valid JavaScript is valid TypeScript. 7 | 8 | In fact even a piece of simple JS code like 9 | 10 | ```js 11 | let x = 4; 12 | x = 'hello'; 13 | ``` 14 | 15 | Will make the TypeScript compiler unhappy, and that's the point! 16 | 17 | In a modern tool chain, the TypeScript compiler isn't even usually the 18 | thing emitting runnable JavaScript any more (often that's Babel's job), 19 | so it can be thought of as a "very fancy linter", with some extra 20 | in-editor UX treats. 21 | 22 | ### Who is this course for? 23 | 24 | This course is intended for developers who are already familiar with TypeScript 25 | as a programming language, and are interested in learning more about how 26 | to use it at scale, in libraries, and as a core part of large software projects. 27 | 28 | ### What kind of stuff will we learn? 29 | 30 | We'll cover a lot of ground in this course, but much of it will come back to a 31 | few central themes: 32 | 33 | #### Productivity through stability and automation 34 | 35 | - The whole point of TS is to allow you do more, and do it better. We'll learn how to make sure that it delivers on this promise 36 | - The bigger your team is, and the more critical your app is, the more costly disruptions can be. We'll learn how to avoid them 37 | 38 | #### Developer ergonomics, and a modern authoring environment 39 | 40 | - Part of the TS value proposition is the idea of a _fantastic development environment_. 41 | We'll learn how to make sure this is set up nicely so that it works well 42 | for even _very large_ apps. 43 | - We'll learn about how to use valuable (but oft neglected) features like API 44 | deprecations, tracking a public API surface, and marking parts of your 45 | code as "alpha" or "beta" maturity 46 | 47 | #### Release Confidence & Type Safety 48 | 49 | - TypeScript places a greater importance on productivity than 100% type safety. 50 | Compare this to [flow](https://flow.org), which has made a different choice 51 | > In type systems, soundness is the ability for a type checker to catch every single error that might happen at runtime. This comes at the cost of sometimes catching errors that will not actually happen at runtime. 52 | > 53 | > On the flip-side, completeness is the ability for a type checker to only ever catch errors that would happen at runtime. This comes at the cost of sometimes missing errors that will happen at runtime. 54 | > 55 | > In an ideal world, every type checker would be both sound and complete so that it catches every error that will happen at runtime. 56 | > 57 | > Flow tries to be as sound and complete as possible. But because JavaScript was not designed around a type system, Flow sometimes has to make a trade-off. When this happens Flow tends to favor soundness over completeness, ensuring that code doesn’t have any bugs. ([source](https://flow.org/en/docs/lang/types-and-expressions/#toc-soundness-and-completeness)) 58 | 59 | #### Take extra care where runtime and type-checking meet 60 | 61 | - We'll see first hand how bugs in these areas significantly weaken TypeScript's value as a tool 62 | - By the end of this course, you'll understand how to give these areas extra attention 63 | 64 | --- 65 | 66 |

67 | Next: Project Tour ▶ 68 |

69 | -------------------------------------------------------------------------------- /notes/01-project-tour.md: -------------------------------------------------------------------------------- 1 |

2 | ◀ Back: Intro 3 |

4 | 5 | --- 6 | 7 | # Project tour and getting started 8 | 9 | In this workshop we'll be working in the context of a simplified Slack app 10 | 11 | ![project screenshot](./img/project_screenshot.png) 12 | 13 | As we begin the course, it's written entirely in JavaScript, and is comprised of a few mostly stateless React components. 14 | 15 | The web client for this app lives in [`src/`](../src/), and is roughly organized as 16 | 17 | ```bash 18 | src/ # web client 19 | data/ # data layer 20 | ui/ # react components 21 | utils/ # low-level utilities 22 | index.js # entry point 23 | ``` 24 | 25 | There's a API and database in this project as well, but we won't be changing them during the workshop. 26 | 27 | The database comes from your [`db.json`](../db.json) file, and the API code is located in the [`server/`](../server/) folder. 28 | 29 | **One thing you _absolutely will_ want to look at is the API documentation, which can be found in your [`API_EXAMPLES.http`](API_EXAMPLES.http) file**. 30 | 31 | --- 32 | 33 |

34 | Next: Recent TS Language Features ▶ 35 |

36 | -------------------------------------------------------------------------------- /notes/02-recent-ts-features.md: -------------------------------------------------------------------------------- 1 |

2 | ◀ Back: Project Tour 3 |

4 | 5 | --- 6 | 7 | # TS 3.7 -> 4.1: Quick Summary 8 | 9 | ## Modern JS Language Features 10 | 11 | ### Optional Chaining (3.7) 12 | 13 | Stops running expression if a null or undefined is encountered 14 | 15 | - 🚧 **Warning:** adds complexity. Beware of multiple optional links in a single expression 16 | - 🚧 **Warning:** not appropriate to just “power through” nullish values 17 | - 🚧 **Warning:** only short-circuits the expression in question, not any other compound expressions joined by operators 18 | 19 | ### Nullish Coalescing (3.7) 20 | 21 | Useful for fallback defaults, where a user-supplied 0 or ‘’ would have been treated improperly 22 | 23 | - 🚧 **Warning:** not the same as ||, due to falsy values that aren’t nullish 24 | 25 | ### ECMAScript Private Fields (3.8) 26 | 27 | ```js 28 | class Foo { 29 | #bar; 30 | } 31 | ``` 32 | 33 | - “hard private” (undetectable from the outside) 34 | - Cannot be overridden by subclasses 35 | - Use WeakMap under the hood, so might perform differently compared to public class fields (not a problem unless you’re on a really hot path) 36 | 37 | ### Namespace exports (3.8) 38 | 39 | ```ts 40 | export * as utils from './utils.mjs'; 41 | ``` 42 | 43 | ### Inference of class field types (4.0) 44 | 45 | Types for class fields that are assigned in constructor are inferred, and no longer need an explicit type declaration 46 | 47 | ## Tuple Types 48 | 49 | ### Variadic tuple types (4.0) 50 | 51 | ```ts 52 | type StrStrNumNumBool = [...Strings, ...Numbers, boolean]; 53 | ``` 54 | 55 | ### Labeled tuple types (4.0) 56 | 57 | ```ts 58 | function foo(x: [first: string, second: number]) {} 59 | ``` 60 | 61 | ## More powerful type aliases and interfaces 62 | 63 | ### Recursive type aliases (3.7) 64 | 65 | JSON can now be typed in a single type alias! 66 | 67 | ### Recursive conditional types (4.1) 68 | 69 | ```ts 70 | type ElementType = T extends ReadonlyArray 71 | ? ElementType 72 | : T; 73 | ``` 74 | 75 | ### Template type literals (4.1) 76 | 77 | ```ts 78 | type Corner = `${'top'|'bottom'}-${'left'|'right'}`; 79 | ``` 80 | 81 | Allows capitalize, uncapitalize, uppercase, lowercase 82 | 83 | ```ts 84 | type Corner = `${capitalize 'top'|'bottom'}-${'left'|'right'}`; 85 | ``` 86 | 87 | ## Editor Experience 88 | 89 | ### `/** @deprecated \*/` (4.0) 90 | 91 | Strikes out symbols in vscode 92 | Support for "assert that this is not deprecated" in tests (we'll see this later) 93 | 94 | ### `/** @see \*/` (4.1) 95 | 96 | - Reference other documentation in JSDoc comments 97 | - You can "jump to definition" just like it's code 98 | 99 | ## Error and Assertion Handling 100 | 101 | ### `// @ts-expect-error` (3.9) 102 | 103 | - A huge win for negative test cases 104 | - I prefer it nearly always over `// @ts-ignore` 105 | 106 | ### Unknown on catch clause (4.0) 107 | 108 | - A big improvement over `any` error types 109 | - Forces you to deal with `instanceof Error` properly 110 | 111 | ### Assertion functions (3.7) 112 | 113 | Type guards, but based on return/throw instead of returning true/false 114 | 115 | ```ts 116 | function assertIsArray(val: any): asserts val is any[] { 117 | if (!Array.isArray(val)) throw new Error(`${val} is not an array`); 118 | } 119 | ``` 120 | 121 | This makes things like tests _much_ easier. The code you want to write for your tests 122 | and the code you need to write to make type-checking happy are now the same thing. 123 | 124 | ## Typed JS Support 125 | 126 | ### Declaration files can be generated from `.js` (3.7) 127 | 128 | This is a big deal for projects that may not be viable for converting to TS -- they can still offer first-class TS support entirely based on their JSDoc comments 129 | 130 | ## Modules 131 | 132 | ### Type-only imports (3.8) 133 | 134 | ```ts 135 | import type { SomeThing } from './some-module.js'; 136 | ``` 137 | 138 | Big win for lazy loading, in situations where you _only_ need to refer to 139 | a package for type information 140 | 141 | --- 142 | 143 |

144 | Next: App vs. Library Concerns ▶ 145 |

146 | -------------------------------------------------------------------------------- /notes/03-app-vs-library-concerns.md: -------------------------------------------------------------------------------- 1 |

2 | Next: Recent TS Language Features ▶ 3 |

4 | 5 | --- 6 | 7 | # TypeScript: App vs. Library Concerns 8 | 9 | ## Myths (almost always) 10 | 11 | - ❌ "No more runtime errors" 12 | - ❌ "My code will run measurably faster" 13 | 14 | ## Most TS Codebases 15 | 16 | - **Improved developer experience**, including in-editor docs for your dependencies 17 | - **Reduced need to "drill into” files** to understand how adjacent code works 18 | - **Micro “rigor” that adds up to macro benefits** 19 | I can type things well-enough and let the system help me when I cross the threshold beyond which I “remember” what’s going on, and what types a particular value could be 20 | - Developers can encode more of their intent 21 | - More formalized and stable contracts “between things” (i.e., one component to another) 22 | - **Coloring inside the lines** 23 | > - Stay on the public API surface of your dependencies 24 | > - Catch incomplete refactorings 25 | > - Some subset of runtime errors moved to compile time 26 | > - Using an ES2020 feature while needing to support ES2017 27 | > - Momentarily forgetting about browser or node APIs 28 | 29 | ## (mostly) App-specific concerns 30 | 31 | - More richness when working with data 32 | - Better encapsulation tools, to facilitate maintaining lazy loading boundaries 33 | - Improved “major version upgrade” story for typed libraries 34 | 35 | ## (mostly) Library-specific concerns 36 | 37 | - Create and maintain a deliberate public API surface 38 | ...while still being able to create a private API surface to use between modules or components 39 | - Keep your users on track (i.e., enums allow you to signal allowed value sets better than `number`) 40 | - SemVer (deprecations, breakage) 41 | - API Docs 42 | 43 | --- 44 | 45 |

46 | Next: Mike's Professional-Grade TS Setup ▶ 47 |

48 | -------------------------------------------------------------------------------- /notes/04-mikes-ts-setup.md: -------------------------------------------------------------------------------- 1 |

2 | ◀ Back: App vs. Library Concerns 3 |

4 | 5 | --- 6 | 7 | # Mike's "bare bones" TS setup 8 | 9 | In this part of the workshop, we'll create a new small library from nothing, so you can see how Mike's "lots of value out of few tools" approach keeps things nice and simple. 10 | 11 | ## Getting Started 12 | 13 | First, create a new directory and enter it 14 | 15 | ```sh 16 | mkdir my-lib 17 | cd my-lib 18 | ``` 19 | 20 | Then, create a `.gitignore` file 21 | 22 | ```sh 23 | npx gitignore node 24 | ``` 25 | 26 | and a package.json file 27 | 28 | ```sh 29 | yarn init --yes 30 | ``` 31 | 32 | Make a few direct modifications to your `package.json` file as follows 33 | 34 | ```diff 35 | --- a/package.json 36 | +++ b/package.json 37 | @@ -1,6 +1,13 @@ 38 | { 39 | "name": "my-lib", 40 | "version": "1.0.0", 41 | - "main": "index.js", 42 | + "main": "dist/index.js", 43 | + "types": "dist/index.d.ts", 44 | + "scripts": { 45 | + "build": "tsc", 46 | + "dev": "yarn build --watch --preserveWatchOutput", 47 | + "lint": "eslint src --ext js,ts", 48 | + "test": "jest" 49 | + }, 50 | "license": "MIT" 51 | } 52 | ``` 53 | 54 | This ensures that TS and non-TS consumers alike can use this library, and that we can run the following commands 55 | 56 | ```sh 57 | yarn build # build the project 58 | yarn dev # build, and rebuild when source is changed 59 | yarn lint # run the linter 60 | yarn test # run tests 61 | ``` 62 | 63 | Pin the node and yarn versions to their current stable releases using volta 64 | 65 | ```sh 66 | volta pin node yarn 67 | ``` 68 | 69 | this will add `node` and `yarn` versions to your `package.json` automatically. 70 | 71 | Next, initialize the git repository 72 | 73 | ```sh 74 | git init 75 | ``` 76 | 77 | ## TypeScript Compiler 78 | 79 | install typescript as a development dependency. We'll only need this at build 80 | time, because not all consumers of this library may be using TypeScript 81 | themselves. 82 | 83 | ```sh 84 | yarn add -D typescript 85 | ``` 86 | 87 | ## Setting up your tsconfig 88 | 89 | Create a default `tsconfig.json` 90 | 91 | ```sh 92 | yarn tsc --init 93 | ``` 94 | 95 | and ensure the following values are set: 96 | 97 | ```diff 98 | "compilerOptions": { 99 | + "outDir": "dist", 100 | + "rootDirs": ["src"], 101 | }, 102 | + "include": ["src"] 103 | ``` 104 | 105 | We want to make sure that the `src/` folder is where our source code lives, that it's treated as a root directory, and that the compiled output is in the `dist/` folder. 106 | 107 | Next, make sure that the TS compiler creates Node-friendly CommonJS modules, and that we target the ES2018 language level (Node 10, allowing for features like `async` and `await`). 108 | 109 | ```diff 110 | "compilerOptions": { 111 | + "module": "commonjs", 112 | + "target": "ES2018", 113 | } 114 | ``` 115 | 116 | Let's make sure two potentially problematic features are _disabled_. We'll talk later about why these are not great for a library. 117 | 118 | ```diff 119 | "compilerOptions": { 120 | + "esModuleInterop": false, 121 | + "skipLibCheck": false 122 | } 123 | ``` 124 | 125 | Make sure that the compiler outputs ambient type information in addition to the JavaScript 126 | 127 | ```diff 128 | "compilerOptions": { 129 | + "declaration": true, 130 | } 131 | ``` 132 | 133 | And finally, let's make sure that we set up an "extra strict" type-checking configuration 134 | 135 | ```diff 136 | "compilerOptions": { 137 | /** 138 | * "strict": true, 139 | * ------------------- 140 | * - noImplicitAny 141 | * - strictNullChecks 142 | * - strictFunctionTypes 143 | * - strictBindCallApply 144 | * - strictPropertyInitialization 145 | * - noImplicitThis 146 | * - alwaysStrict 147 | */ 148 | + "strict": true, 149 | + "noUnusedLocals": true, 150 | + "noImplicitReturns": true, 151 | + "stripInternal": true, 152 | + "types": [], 153 | + "forceConsistentCasingInFileNames": true, 154 | } 155 | ``` 156 | 157 | We'll go in to more detail later about what some of these options mean, and why I suggest setting them this way. 158 | 159 | Finally, please create a folder for your source code, and create an empty `index.ts` file within it 160 | 161 | ```sh 162 | mkdir src 163 | touch src/index.ts 164 | ``` 165 | 166 | Open `src/index.ts` and set its contents to the following 167 | 168 | ```ts 169 | /** 170 | * @packageDocumentation A small library for common math functions 171 | */ 172 | 173 | /** 174 | * Calculate the average of three numbers 175 | * 176 | * @param a - first number 177 | * @param b - second number 178 | * @param c - third number 179 | * 180 | * @public 181 | */ 182 | export function avg(a: number, b: number, c: number): number { 183 | return sum3(a, b, c) / 3; 184 | } 185 | 186 | /** 187 | * Calculate the sum of three numbers 188 | * 189 | * @param a - first number 190 | * @param b - second number 191 | * @param c - third number 192 | * 193 | * @beta 194 | */ 195 | export function sum3(a: number, b: number, c: number): number { 196 | return sum2(a, sum2(b, c)); 197 | } 198 | 199 | /** 200 | * Calculate the sum of two numbers 201 | * 202 | * @param a - first number 203 | * @param b - second number 204 | * 205 | * @internal 206 | */ 207 | export function sum2(a: number, b: number): number { 208 | const sum = a + b; 209 | return sum; 210 | } 211 | ``` 212 | 213 | This is obviously convoluted, but it'll serve our purposes for looking at some interesting behavior later. 214 | 215 | Let's make sure that things are working so far by trying to build this project. 216 | 217 | ```sh 218 | rm -rf dist # clear away any old compiled output 219 | yarn build # build the project 220 | ls dist # list the contents of the dist/ folder 221 | ``` 222 | 223 | You should see something like 224 | 225 | ```sh 226 | index.d.ts index.js 227 | ``` 228 | 229 | ## Linting 230 | 231 | Install eslint as a development dependency 232 | 233 | ```sh 234 | yarn add -D eslint 235 | ``` 236 | 237 | and go through the process of creating a starting point ESLint config file 238 | 239 | ```sh 240 | yarn eslint --init 241 | ``` 242 | 243 | When asked, please answer as follows for the choices presented to you: 244 | 245 |
246 |
How would you like to use ESLint?
247 |
To check syntax and find problems
248 |
What type of modules does your project use
249 |
None of these
250 |
Which framework does your project use?
251 |
None of these
252 |
Does your project use TypeScript?
253 |
Yes
254 |
Where does your code run?
255 |
Node
256 |
What format do you want your config file to be in?
257 |
JSON
258 |
Would you like to install them now with npm?
259 |
Yes
260 |
261 | 262 | Because we're using `yarn`, let's delete that `npm` file `package-lock.json` and run `yarn` to update `yarn.lock`. 263 | 264 | ```sh 265 | rm package-lock.json 266 | yarn 267 | ``` 268 | 269 | Let's also enable a set of rules that take advantage of type-checking information 270 | 271 | ```diff 272 | --- a/.eslintrc.json 273 | +++ b/.eslintrc.json 274 | @@ -5,7 +5,8 @@ 275 | }, 276 | "extends": [ 277 | "eslint:recommended", 278 | - "plugin:@typescript-eslint/recommended" 279 | + "plugin:@typescript-eslint/recommended", 280 | + "plugin:@typescript-eslint/recommended-requiring-type-checking" 281 | ], 282 | "parser": "@typescript-eslint/parser", 283 | ``` 284 | 285 | There's one rule we want to enable, and that's a preference for `const` over `let`. While we're here, 286 | we can disable ESLint's rules for unused local variables and params, because the TS 287 | compiler is responsible for telling us about those 288 | 289 | ```diff 290 | --- a/.eslintrc.json 291 | +++ b/.eslintrc.json 292 | @@ -14,5 +14,6 @@ 293 | }, 294 | "plugins": ["@typescript-eslint"], 295 | "rules": { 296 | + "prefer-const": "error", 297 | + "@typescript-eslint/no-unused-vars": "off", 298 | + "@typescript-eslint/no-unused-params": "off" 299 | } 300 | } 301 | ``` 302 | 303 | ESLint needs a single tsconfig file that includes our entire project (including tests), so we'll need to make a small dedicated one 304 | 305 | ##### `/tsconfig.eslint.json` 306 | 307 | ```json 308 | { 309 | "extends": "./tsconfig.json", 310 | "compilerOptions": { 311 | "types": ["jest"] 312 | }, 313 | "include": ["src", "tests"] 314 | } 315 | ``` 316 | 317 | Going back to our `/.eslintrc.json`, we need to tell ESLint about this new TS config -- rules that require type-checking need to know about where it is 318 | 319 | ```diff 320 | --- a/.eslintrc.json 321 | +++ b/.eslintrc.json 322 | @@ -4,14 +4,17 @@ 323 | "parserOptions": { 324 | - "ecmaVersion": 12 325 | + "ecmaVersion": 12, 326 | + "project": "tsconfig.eslint.json" 327 | }, 328 | 329 | } 330 | ``` 331 | 332 | While we're in here, let's set up some different rules for our 333 | test files compared to our source files 334 | 335 | ```diff 336 | --- a/.eslintrc.json 337 | +++ b/.eslintrc.json 338 | @@ -15,5 +15,11 @@ 339 | "plugins": ["@typescript-eslint"], 340 | "rules": { 341 | "prefer-const": "error" 342 | - } 343 | + }, 344 | + "overrides": [ 345 | + { 346 | + "files": "tests/**/*.ts", 347 | + "env": { "node": true, "jest": true } 348 | + } 349 | + ] 350 | } 351 | ``` 352 | 353 | Let's make sure this works by making a change to our `src/index.ts` that breaks our `prefer-const` rule 354 | 355 | ```diff 356 | --- a/src/index.ts 357 | +++ b/src/index.ts 358 | @@ -33,5 +33,6 @@ export function sum3(a: number, b: number, c: number): number { 359 | * @internal 360 | */ 361 | export function sum2(a: number, b: number): number { 362 | - return a + b; 363 | + let sum = a + b; 364 | + return sum; 365 | } 366 | ``` 367 | 368 | running 369 | 370 | ```sh 371 | yarn lint 372 | ``` 373 | 374 | should tell us that this is a problem. If properly configured, you may also see feedback right in your code editor as well 375 | 376 | ![eslint error](img/eslint-error.png) 377 | 378 | Undo the problematic code change, run `yarn lint` again and you should see no errors 379 | 380 | ## Testing 381 | 382 | Next, let's install our test runner, and associated type information, along with some required babel plugins 383 | 384 | ```sh 385 | yarn add -D jest @types/jest @babel/preset-env @babel/preset-typescript 386 | ``` 387 | 388 | and make a folder for our tests 389 | 390 | ```sh 391 | mkdir tests 392 | ``` 393 | 394 | Create a file to contain the tests for our `src/index.ts` module 395 | 396 | ```sh 397 | touch tests/index.test.ts 398 | ``` 399 | 400 | ###### `tests/index.test.ts` 401 | 402 | ```ts 403 | import { avg, sum3 } from '..'; 404 | 405 | describe('avg should calculate an average properly', () => { 406 | test('three positive numbers', () => { 407 | expect(avg(3, 4, 5)).toBe(4); 408 | }); 409 | test('three negative numbers', () => { 410 | expect(avg(3, -4, -5)).toBe(-2); 411 | }); 412 | }); 413 | 414 | describe('sum3 should calculate a sum properly', () => { 415 | test('three positive numbers', () => { 416 | expect(sum3(3, 4, 5)).toBe(12); 417 | }); 418 | test('three negative numbers', () => { 419 | expect(sum3(3, -4, -5)).toBe(-6); 420 | }); 421 | }); 422 | ``` 423 | 424 | We'll need to make a one-line change in our existing `/tsconfig.json` file 425 | 426 | ```diff 427 | --- a/tsconfig.json 428 | +++ b/tsconfig.json 429 | @@ -1,4 +1,5 @@ 430 | { 431 | "compilerOptions": { 432 | + "composite": true, 433 | ``` 434 | 435 | and to create a small `tests/tsconfig.json` just for our tests 436 | 437 | ###### `tests/tsconfig.json` 438 | 439 | ```json 440 | { 441 | "extends": "../tsconfig.json", 442 | "references": [{ "name": "my-lib", "path": ".." }], 443 | "compilerOptions": { 444 | "types": ["jest"], 445 | "rootDir": ".." 446 | }, 447 | "include": ["."] 448 | } 449 | ``` 450 | 451 | and a small little babel config at the root of our project, so that Jest can understand TypeScript 452 | 453 | ###### `.babelrc` 454 | 455 | ```json 456 | { 457 | "presets": [ 458 | ["@babel/preset-env", { "targets": { "node": "10" } }], 459 | "@babel/preset-typescript" 460 | ] 461 | } 462 | ``` 463 | 464 | ### Take it for a spin 465 | 466 | At this point, we should make sure that everything works as intended before proceeding further. 467 | 468 | Run 469 | 470 | ```sh 471 | yarn test 472 | ``` 473 | 474 | to run the tests with jest. You should see some output like 475 | 476 | ```sh 477 | PASS tests/index.test.ts 478 | avg should calculate an average properly 479 | ✓ three positive numbers (2 ms) 480 | ✓ three negative numbers 481 | sum3 should calculate a sum properly 482 | ✓ three positive numbers 483 | ✓ three negative numbers 484 | 485 | Test Suites: 1 passed, 1 total 486 | Tests: 4 passed, 4 total 487 | Snapshots: 0 total 488 | Time: 1.125 s 489 | Ran all test suites. 490 | ✨ Done in 1.74s. 491 | ``` 492 | 493 | ## API Surface Report & Docs 494 | 495 | We're going to use Microsoft's [api-extractor](https://api-extractor.com/) as our 496 | documentation tool -- but it's really much more than that as we'll see later 497 | 498 | First, let's install it 499 | 500 | ```sh 501 | yarn add -D @microsoft/api-extractor @microsoft/api-documenter 502 | ``` 503 | 504 | and let's ask `api-extractor` to create a default config for us 505 | 506 | ```sh 507 | yarn api-extractor init 508 | ``` 509 | 510 | This should result in a new file `/api-extractor.json` being created. Open it 511 | up and make the following changes 512 | 513 | ```diff 514 | --- a/api-extractor.json 515 | +++ b/api-extractor.json 516 | @@ -45,7 +45,7 @@ 517 | * 518 | * SUPPORTED TOKENS: , , 519 | */ 520 | - "mainEntryPointFilePath": "/lib/index.d.ts", 521 | + "mainEntryPointFilePath": "/dist/index.d.ts",^M 522 | 523 | /** 524 | * A list of NPM package names whose exports should be treated as part of this package. 525 | @@ -181,7 +181,7 @@ 526 | /** 527 | * (REQUIRED) Whether to generate the .d.ts rollup file. 528 | */ 529 | - "enabled": true 530 | + "enabled": true,^M 531 | 532 | /** 533 | * Specifies the output path for a .d.ts rollup file to be generated without any trimming. 534 | @@ -195,7 +195,7 @@ 535 | * SUPPORTED TOKENS: , , 536 | * DEFAULT VALUE: "/dist/.d.ts" 537 | */ 538 | - // "untrimmedFilePath": "/dist/.d.ts", 539 | + "untrimmedFilePath": "/dist/-private.d.ts", 540 | 541 | /** 542 | * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "beta" release. 543 | @@ -207,7 +207,7 @@ 544 | * SUPPORTED TOKENS: , , 545 | * DEFAULT VALUE: "" 546 | */ 547 | - // "betaTrimmedFilePath": "/dist/-beta.d.ts", 548 | + "betaTrimmedFilePath": "/dist/-beta.d.ts", 549 | 550 | /** 551 | * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "public" release. 552 | @@ -221,7 +221,7 @@ 553 | * SUPPORTED TOKENS: , , 554 | * DEFAULT VALUE: "" 555 | */ 556 | - // "publicTrimmedFilePath": "/dist/-public.d.ts", 557 | + "publicTrimmedFilePath": "/dist/-public.d.ts" 558 | 559 | /** 560 | * When a declaration is trimmed, by default it will be replaced by a code comment such as 561 | ``` 562 | 563 | Make an empty `/etc` folder 564 | 565 | ```sh 566 | mkdir etc 567 | ``` 568 | 569 | and then run api-extractor for the first time 570 | 571 | ```sh 572 | yarn api-extractor run --local 573 | ``` 574 | 575 | This should result in a new file being created: `/etc/my-lib.api.md`. This is 576 | your api-report. There's also a `/temp` folder that will have been created. You 577 | should add this to your `.gitignore`. 578 | 579 | ```diff 580 | --- a/.gitignore 581 | +++ b/.gitignore 582 | @@ -114,3 +114,5 @@ dist 583 | .yarn/build-state.yml 584 | .yarn/install-state.gz 585 | .pnp.* 586 | + 587 | +temp 588 | ``` 589 | 590 | you may also notice that some new `.d.ts` files are in your `/dist` folder. 591 | Take a look at the contents. Do you see anything interesting? 592 | 593 | ## API Docs 594 | 595 | We can use `api-documenter` to create markdown API docs by running 596 | 597 | ```sh 598 | yarn api-documenter markdown -i temp -o docs 599 | ``` 600 | 601 | This should result in the creation of a `/docs` folder containing the markdown pages. 602 | 603 | Finally, we should make a couple of new npm scripts to help us easily 604 | generate new docs by running `api-extractor` and `api-documenter` sequentially 605 | 606 | ```diff 607 | --- a/package.json 608 | +++ b/package.json 609 | @@ -7,7 +7,10 @@ 610 | "build": "tsc", 611 | "watch": "yarn build --watch --preserveWatchOutput", 612 | "lint": "eslint src tests --ext ts,js", 613 | - "test": "jest" 614 | + "test": "jest", 615 | + "api-report": "api-extractor run", 616 | + "api-docs": "api-documenter markdown -i temp -o docs", 617 | + "build-with-docs": "yarn build && yarn api-report && yarn api-docs" 618 | }, 619 | "license": "MIT", 620 | "volta": { 621 | ``` 622 | 623 | Make a commit so you have a clean workspace. 624 | 625 | ```sh 626 | git commit -am "setup api-extractor and api-documenter" 627 | ``` 628 | 629 | ## Making a change that affects our API 630 | 631 | Let's "enhance" our library by supporting a fourth number in `sum3()` 632 | 633 | ```diff 634 | --- a/src/index.ts 635 | +++ b/src/index.ts 636 | @@ -21,11 +21,12 @@ export function avg(a: number, b: number, c: number): number { 637 | * @param a - first number 638 | * @param b - second number 639 | * @param c - third number 640 | + * @param d - fourth number 641 | * 642 | * @beta 643 | */ 644 | -export function sum3(a: number, b: number, c: number): number { 645 | - return sum2(a, sum2(b, c)); 646 | +export function sum3(a: number, b: number, c: number, d = 0): number { 647 | + return sum2(a, b) + sum2(c, d); 648 | } 649 | ``` 650 | 651 | Now run 652 | 653 | ```sh 654 | yarn build-with-docs 655 | ``` 656 | 657 | You should see something like 658 | 659 | ``` 660 | Warning: You have changed the public API signature for this project. Please copy the file "temp/my-lib.api.md" to "etc/my-lib.api.md", or perform a local build (which does this automatically). See the Git repo documentation for more info. 661 | ``` 662 | 663 | This is `api-extractor` telling you that something that users can observe 664 | through the public API surface of this library has changed. We can follow its instructions 665 | to indicate that this was an _intentional change_ (and probably a minor release instead of a patch) 666 | 667 | ```sh 668 | cp temp/my-lib.api.md etc 669 | ``` 670 | 671 | and build the docs again 672 | 673 | ```sh 674 | yarn build-with-docs 675 | ``` 676 | 677 | You should now see an updated api-report. It's now very easy to see 678 | the ramifications of changes to our API surface on a per-code-change basis! 679 | 680 | ```diff 681 | diff --git a/etc/my-lib.api.md b/etc/my-lib.api.md 682 | index fc8ea25..82c4ac4 100644 683 | --- a/etc/my-lib.api.md 684 | +++ b/etc/my-lib.api.md 685 | @@ -8,7 +8,7 @@ 686 | export function avg(a: number, b: number, c: number): number; 687 | 688 | // @beta 689 | -export function sum3(a: number, b: number, c: number): number; 690 | +export function sum3(a: number, b: number, c: number, d?: number): number; 691 | ``` 692 | 693 | Our documentation has also been updated automatically 694 | 695 | ```diff 696 | --- a/docs/my-lib.md 697 | +++ b/docs/my-lib.md 698 | @@ -11,5 +11,5 @@ A small library for common math functions 699 | | Function | Description | 700 | | --- | --- | 701 | | [avg(a, b, c)](./my-lib.avg.md) | Calculate the average of three numbers | 702 | -| [sum3(a, b, c)](./my-lib.sum3.md) | (BETA) Calculate the sum of three numbers | 703 | +| [sum3(a, b, c, d)](./my-lib.sum3.md) | (BETA) Calculate the sum of three numbers | 704 | 705 | diff --git a/docs/my-lib.sum3.md b/docs/my-lib.sum3.md 706 | index 8ab69a1..4ca8888 100644 707 | --- a/docs/my-lib.sum3.md 708 | +++ b/docs/my-lib.sum3.md 709 | @@ -12,7 +12,7 @@ Calculate the sum of three numbers 710 | Signature: 711 | 712 | ` ``typescript 713 | -export declare function sum3(a: number, b: number, c: number): number; 714 | +export declare function sum3(a: number, b: number, c: number, d?: number): number; 715 | ` `` 716 | 717 | ## Parameters 718 | 719 | @@ -22,6 +22,7 @@ export declare function sum3(a: number, b: number, c: number): number; 720 | | a | number | first number | 721 | | b | number | second number | 722 | | c | number | third number | 723 | +| d | number | fourth number | 724 | 725 | Returns: 726 | 727 | ``` 728 | 729 | Congrats! we now have 730 | 731 | - Compiling to JS 732 | - Linting 733 | - Tests 734 | - Docs 735 | - API surface change detection 736 | 737 | without having to reach for more complicated tools like webpack! 738 | 739 | --- 740 | 741 |

742 | Next: ...What have I done? ▶ 743 |

744 | -------------------------------------------------------------------------------- /notes/05-what-have-I-done.md: -------------------------------------------------------------------------------- 1 |

2 | ◀ Back: Mike's Professional-Grade TS Setup 3 |

4 | 5 | --- 6 | 7 | # What have I done? 8 | 9 | Let's look closely at what we just did, and make sure we understand all of the parts that make up the whole 10 | 11 | ## In my `tsconfig.json` what exactly is "strict"? 12 | 13 | The source of truth is [here](https://github.com/microsoft/TypeScript/blob/dc8952d308c9de815e95bdb96727a9cbaedc9adb/src/compiler/commandLineParser.ts#L594), and it's important to know that this is a moving target 14 | 15 | ### `noImplicitAny` 16 | 17 | - “Default to explicit” instead of “default to loose” 18 | - This is not in any way restrictive, it only requires that we be explicit about `any` 19 | 20 | ### `noImplicitThis` 21 | 22 | - There are certain places where `this` is important and non-inferrable 23 | > Example: addEventListener 24 | > 25 | > ```ts 26 | > my_element.addEventListener('click', function (e) { 27 | > // logs the className of my_element 28 | > console.log(this.className); 29 | > // logs `true` 30 | > console.log(e.currentTarget === this); 31 | > }); 32 | > ``` 33 | 34 | ### `alwaysStrict` 35 | 36 | - JS “use strict” 37 | - necessary for modern JS language features 38 | 39 | ### `strictBindCallApply` 40 | 41 | - Bind, call, apply used to return very loosely-typed functions. 42 | - No good reasons I'm aware of to disable this 43 | 44 | ### `strictNullChecks` 45 | 46 | - Without this enabled, primitive types allow `null` values 47 | - Leaving this disabled makes truthy/falsy type guards much less useful 48 | - This is asking for runtime errors that could otherwise be caught at build time 49 | 50 | ### `strictFunctionTypes` 51 | 52 | - Some common-sense loopholes around matching function arguments during type-checking function values 53 | - [example](https://www.typescriptlang.org/play?#code/IYIwzgLgTsDGEAJYBthjAgggOwJYFthkEBvAKAUoQAcBXEZXWBUSGeBKAU2ABMB7bMgCeCZFwDmYAFwJstfCC5QA3GQC+ZFGgwBhYIi4APCF2y8MOAkVIUq4qQgC8CACxqqNWlCgAKAJSksIJg-OIAdMj8Er4A5HQ+sf6amtroCAAi0QjGpuaWeITE5J4OGC7udpQgwFAA1gFBIWFckdFxtQBmSSlkZLxc2txiXIidAIyyvkayVkWBTgB8CABu-Li8agNDXCNjAExTM5nRC8trG1uDqMPiYwDMR7L6EGer65tkE84InfsqlAA9ICEABRHz8KAIADuuAgAAsEABaJFsJgQABitGw8FwggAKsJqFwwF99j8JgCEMCEAB5Opkin3Kk08FQSFAA) 54 | > ```ts 55 | > abstract class Animal { 56 | > public abstract readonly legs: number; 57 | > } 58 | > class Cat extends Animal { 59 | > legs = 4; 60 | > purr() { 61 | > console.log('purr'); 62 | > } 63 | > } 64 | > class Dog extends Animal { 65 | > legs = 4; 66 | > bark() { 67 | > console.log('arf'); 68 | > } 69 | > } 70 | > 71 | > declare let f1: (x: Animal) => void; 72 | > declare let f2: (x: Dog) => void; 73 | > declare let f3: (x: Cat) => void; 74 | > 75 | > // Error with --strictFunctionTypes 76 | > f1 = f2; 77 | > f2 = f1; // Always ok 78 | > f2 = f3; // Always error 79 | > ``` 80 | 81 | ### `strictPropertyInitialization` 82 | 83 | - Holds you to your promises around class fields really being “always there” vs. “sometimes undefined” 84 | 85 | ## Even more strict 86 | 87 | ### `noUnusedLocals` 88 | 89 | - Busts you on unused local variables 90 | - Better to have TS detect this rather than a linter 91 | 92 | ### `noUnusedParameters` 93 | 94 | - Function arguments you don’t use need to be prefixed with \_ 95 | - I love this during the “exploration” phase of development b/c it highlights opportunities to simplify API surface 96 | 97 | ### `noImplicitReturns` 98 | 99 | - If any code paths return something explicitly, all code paths must return something explicitly 100 | 101 | ### `noFallthroughCasesInSwitch` 102 | 103 | - I’m ok with this one as being _disabled_, as I find case fall-throughs to be useful, important and easy (enough) to notice while reading code 104 | 105 | ### `types` 106 | 107 | - Instead of pulling in all @types/\*, specify exactly what should be available 108 | - **NOTE:** this is nuanced, and only affects global scope (i.e., window, process) and auto-import. 109 | - _Why I care:_ I don’t want types used exclusively in things like tests to be quite so readily available for accidental use in “app code” 110 | 111 | ### `stripInternal` (most important for libraries) 112 | 113 | - Sometimes you need type information to only be available within a codebase. 114 | - `@internal` JSdoc tag surgically strips out type information for respective symbols 115 | 116 | ## Don't go viral 117 | 118 | There are some compiler options that I _really dislike_ when used in libraries, because they have a high probability of "infecting" any consumer and depriving them from making choices about their own codebase 119 | 120 | ### `allowSyntheticDefaultImports` 121 | 122 | Allows you to import CommonJS modules as if they’re ES modules with a default export 123 | 124 | ### `esModuleInterop` 125 | 126 | Adds some runtime support for CJS/ESM interop, and enables allowSyntheticDefaultImports 127 | 128 | ### `skipDefaultLibCheck` 129 | 130 | This effectively ignores potential breaking changes that stem from your node_modules types mixing with your own types. Particularly if you’re building a library, you need to know that if you “hide” this problem they’ll still “feel” it (and probably need to “skip” too) 131 | 132 | ### But sometimes we need these, right? 133 | 134 | I have never found a good reason to enable these options in well-structured TS code. 135 | 136 | `allowSyntheticDefaultImports` and `esModuleInterop` aim to allow patterns like 137 | 138 | ```ts 139 | import fs from 'fs'; 140 | ``` 141 | 142 | in situations where `fs` doesn't actually expose an ES module `export default`. It exports a _namespace_ of filesystem-related functions. Thankfully, even with these flags _both_ disabled, we can still use a namespace import: 143 | 144 | ```ts 145 | import * as fs from 'fs'; 146 | ``` 147 | 148 | Now there are rare situations where some CommonJS code exports a single non-namespace thing in a way like 149 | 150 | ```ts 151 | // calculator.ts 152 | module.exports = function add(a, b) { 153 | return a + b; 154 | }; 155 | ``` 156 | 157 | add is definitely not a namespace, and 158 | 159 | ```ts 160 | import * as add from './calculator'; 161 | ``` 162 | 163 | WILL NOT WORK. There's a TS-specific pattern that _will work_ though -- it's a little weird, but it doesn't require turning any compiler options on 164 | 165 | ```ts 166 | import add = require('./calculator'); 167 | ``` 168 | 169 | ### Is Mike asking me to take on tech debt? 170 | 171 | You may be thinking "these don't look like ES modules", and "won't the TS team standardize on ES modules later?" 172 | 173 | The answer is: _yes_, but you should think about this the same way that you think about a "legacy" version of Node.js that you need to support 174 | 175 | - You shouldn't break consumers yet 176 | - Apps should be the first to adopt new things, followed by libraries that are more conservative 177 | 178 | TS modules predate ES modules, but there’s tons of code out there that already uses TS module stuff, and this is one of the most easy to codemod kinds of “tech debt” to incur. 179 | 180 | --- 181 | 182 |

183 | Next: Converting to TypeScript ▶ 184 |

185 | ``` 186 | -------------------------------------------------------------------------------- /notes/06-converting-to-ts.md: -------------------------------------------------------------------------------- 1 |

2 | ◀ Back: ...What have I done? 3 |

4 | 5 | --- 6 | 7 | # Converting to TypeScript 8 | 9 | In my [TS Fundamentals Course (v2)](https://drive.google.com/file/d/170oHzpLNeprUa-TMmOAnSU4caEFDSb3e/view) we discuss a procedure for progressively converting a codebase from JS to TS. 10 | 11 |

12 | 13 | ![](img/ts-3-essentials/slide-018.png) 14 | 15 |

16 |

17 | 18 | ![](img/ts-3-essentials/slide-019.png) 19 | 20 |

21 |

22 | 23 | ![](img/ts-3-essentials/slide-020.png) 24 | 25 |

26 |

27 | 28 | ![](img/ts-3-essentials/slide-021.png) 29 | 30 |

31 | 32 | Putting aside that the last image above is a bit redundant (with `"strict"` enabling `"strictNullChecks"`, `"strictFunctionTypes"` and `""strictNullChecks"` already), there's a lot of work to unpack here 33 | 34 | Realistically, this should be done in separate steps for a large codebase 35 | 36 | For example: 37 | 38 | - 3.1) strict mode 39 | 40 | ```diff 41 | --- a/tsconfig.json 42 | +++ b/tsconfig.json 43 | "compilerOptions": { 44 | + "strict": true, 45 | } 46 | ``` 47 | 48 | - 3.2) more strict mode 49 | 50 | ```diff 51 | --- a/tsconfig.json 52 | +++ b/tsconfig.json 53 | "compilerOptions": { 54 | + "noUnusedLocals": true, 55 | + "noImplicitReturns": true, 56 | + "stripInternal": true, 57 | + "types": [], 58 | + "forceConsistentCasingInFileNames": true 59 | } 60 | ``` 61 | 62 | - 3.3) TS-specific linting 63 | 64 | ```diff 65 | --- a/.eslintrc 66 | +++ b/.eslintrc 67 | + "parser": "@typescript-eslint/parser", 68 | "extends": [ 69 | + "prettier/@typescript-eslint", 70 | + "plugin:@typescript-eslint/recommended", 71 | + "plugin:@typescript-eslint/recommended-requiring-type-checking" 72 | ], 73 | ``` 74 | 75 | - 3.4) Even more strict mode 76 | 77 | ```diff 78 | --- a/.eslintrc 79 | +++ b/.eslintrc 80 | "rules": { 81 | + "@typescript-eslint/no-unsafe-assignment": "off", 82 | + "@typescript-eslint/no-unsafe-return": "off", 83 | + "@typescript-eslint/no-explicit-any": "off" 84 | } 85 | ``` 86 | 87 | There are steps beyond this conversion that are important in order to mitigate some other risks. We'll get into those later 88 | 89 | # CHALLENGE: Follow steps 1 and 2 to convert the workshop chat app from JS to ts 90 | 91 | ## Some Quick Tricks 92 | 93 | ```sh 94 | # rename all JSX files in src/ to TSX 95 | find src -name '*.jsx' -exec bash -c 'git mv "$0" "${0%.jsx}.tsx"' "{}" \; 96 | # rename all JS files in src/ to TS 97 | find src -name '*.js' -exec bash -c 'git mv "$0" "${0%.js}.ts"' "{}" \; 98 | # rename all JSX files in src/ to TSX 99 | find tests -name '*.jsx' -exec bash -c 'git mv "$0" "${0%.jsx}.tsx"' "{}" \; 100 | # rename all JSX files in src/ to TSX 101 | find tests -name '*.jsx.snap' -exec bash -c 'git mv "$0" "${0%.jsx.snap}.tsx.snap"' "{}" \; 102 | # rename all JS files in tests/ to TS 103 | find tests -name '*.js' -exec bash -c 'git mv "$0" "${0%.js}.ts"' "{}" \; 104 | ``` 105 | 106 | and don't forget to make this small change to [`/index.html`](/index.html) 107 | 108 | ```diff 109 | --- a/index.html 110 | +++ b/index.html 111 | @@ -8,6 +8,6 @@ 112 | 113 | 114 |
115 | - 116 | + 117 | 118 | 119 | ``` 120 | 121 | and this change to tsconfig for step 1 122 | 123 | ```diff 124 | --- a/tsconfig.json 125 | +++ b/tsconfig.json 126 | @@ -3,9 +3,10 @@ 127 | "target": "ES2018", 128 | "allowJs": true, 129 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 130 | - "strict": true /* Enable all strict type-checking options. */, 131 | + // "strict": true /* Enable all strict type-checking options. */, 132 | "forceConsistentCasingInFileNames": true, 133 | "noEmit": true, 134 | + "noImplicitAny": false, 135 | "outDir": "dist", 136 | "declaration": true, 137 | "jsx": "react", 138 | ``` 139 | 140 | ## Other tips 141 | 142 | - Most react components can be typed as `React:FunctionComponent` 143 | 144 | --- 145 | 146 |

147 | Next: App vs. Library Concerns ▶ 148 |

149 | -------------------------------------------------------------------------------- /notes/07-dealing-with-pure-type-info.md: -------------------------------------------------------------------------------- 1 |

2 | ◀ Back: Converting to TypeScript 3 |

4 | 5 | --- 6 | 7 | # Dealing with Pure Type Information 8 | 9 | The next step we'll take in this project involves finding the right "home" for 10 | type information of various kinds. Two we'll focus on in particular are 11 | 12 | - Types that relate to _our own code_, and are part of our public API surface 13 | - Fixes and customizations of _types for dependencies_, that are _not_ part of our public API surface 14 | 15 | ## Some relevant parts of tsconfig 16 | 17 | - `typeRoots` allows us to tell TypeScript about top-level folders which may contain type information 18 | - `paths` allows us to instruct the compiler to look for type information for specific modules in specific locations 19 | - `types` allows us to specify which types can affect the global scope, and which appear in vscode auto-imports 20 | 21 | ## Local type overrides 22 | 23 | Create a `types/` folder 24 | 25 | ```sh 26 | mkdir types 27 | ``` 28 | 29 | and in your top-level tsconfig add a `paths` and a `baseUrl` property 30 | 31 | ```diff 32 | --- a/tsconfig.json 33 | +++ b/tsconfig.json 34 | @@ -10,7 +10,10 @@ 35 | "outDir": "dist", 36 | "declaration": true, 37 | "jsx": "react", 38 | - "moduleResolution": "Node" 39 | + "moduleResolution": "Node", 40 | + "baseUrl": ".", 41 | + "paths": { 42 | + "*": ["types/*"] 43 | + } 44 | }, 45 | "include": ["src"] 46 | ``` 47 | 48 | In this folder we can place our type overrides for dependencies. We'll talk more about 49 | this in our discussion of ambient type information later. 50 | 51 | ## Published type information for your app 52 | 53 | Types can pass through module boundaries just like values, so we can create 54 | one or more modules for interfaces that are needed by many concerns in our 55 | chat app. 56 | 57 | Let's start by forbidding explicit anys in our app. To do so, make the following 58 | change in your app 59 | 60 | ```diff 61 | --- a/.eslintrc 62 | +++ b/.eslintrc 63 | @@ -83,7 +83,7 @@ 64 | "@typescript-eslint/no-unsafe-member-access": "off", 65 | "@typescript-eslint/no-unsafe-assignment": "off", 66 | "@typescript-eslint/no-unsafe-return": "off", 67 | - "@typescript-eslint/no-explicit-any": "off" 68 | + "@typescript-eslint/no-explicit-any": "error" 69 | } 70 | }, 71 | ``` 72 | 73 | A lot of the `any`s we'll encounter in the app can be replaced with either 74 | and `unknown` or a more appropriate concrete type. 75 | 76 | Let's make sure we first create types for important data models in our app. 77 | Make a new file `src/types.ts` containing the following 78 | 79 | ```ts 80 | /** 81 | * A user participating in a chat 82 | */ 83 | export interface IUser { 84 | id: number; 85 | username: string; 86 | name: string; 87 | iconUrl: string; 88 | } 89 | 90 | /** 91 | * A chat message 92 | */ 93 | export interface IMessage { 94 | id: number; 95 | teamId: string; 96 | channelId: string; 97 | userId: string; 98 | createdAt: string; 99 | user: IUser; 100 | body: string; 101 | } 102 | 103 | /** 104 | * A team, containing one or more chat channels 105 | */ 106 | export interface ITeam { 107 | iconUrl: string; 108 | name: string; 109 | id: string; 110 | channels: IChannel[]; 111 | } 112 | 113 | /** 114 | * A chat channel, containing many chat messages 115 | */ 116 | export interface IChannel { 117 | teamId: string; 118 | name: string; 119 | description: string; 120 | id: string; 121 | messages: IMessage[]; 122 | } 123 | ``` 124 | 125 | # Challenge: get rid of all of the new lint errors 126 | 127 | Keep replacing `any`s until the ESLint errors and warnings go away, **with one exception** 128 | 129 | ```ts 130 | /** 131 | * PLEASE LEAVE THIS WARNING ABOUT AN UNSPECIFIED RETURN TYPE ALONE 132 | */ 133 | 134 | // src/utils/networking.ts 135 | export async function apiCall(path: string, init?: RequestInit) {} 136 | ``` 137 | 138 | --- 139 | 140 |

141 | Next: Types at Runtime ▶ 142 |

143 | ``` 144 | -------------------------------------------------------------------------------- /notes/08-types-at-runtime.md: -------------------------------------------------------------------------------- 1 |

2 | ◀ Back: Dealing with Pure Type Information 3 |

4 | 5 | --- 6 | 7 | # Types at Runtime 8 | 9 | TypeScript is a build-time-only tool, and this is part of why it performs well. 10 | The cost of a runtime type system is not trivial, and in browsers (particularly 11 | on limited-performance devices like cheap android phones) there's not a whole 12 | lot of performance to spare. 13 | 14 | What we can do is take advantage of places where compile-time type-checking and 15 | runtime behavior align, to ensure that our static tools (like TS) provide 16 | information that's as _complete_ as possible. 17 | 18 | User-defined type guards are one of the most important tools available to 19 | accomplish this. 20 | 21 | We're going to start exactly where we left off in the previous exercise 22 | 23 | ```ts 24 | --- a/src/utils/networking.ts 25 | +++ b/src/utils/networking.ts 26 | @@ -28,7 +28,7 @@ async function getJSON(input: RequestInfo, init?: RequestInit) { 27 | * @param path 28 | * @param init 29 | */ 30 | -export async function apiCall(path: string, init?: RequestInit) { 31 | +export async function apiCall(path: string, init?: RequestInit): Promise { 32 | let response; 33 | let json; 34 | try { 35 | ``` 36 | 37 | and use type guards to ensure that we have _some_ validation of incoming 38 | data at runtime, without doing something foolish like attempting to 39 | run the ts compiler in each user's browser. All of the files that use this `apiCall` function will now be unhappy, and it's your job to make some user-defined type guards to make things work again. 40 | 41 | create a new file [`src/type-guards.ts`](../src/type-guards.ts) and build 42 | the following functions such that they provide a meaningful runtime check that can be used as a guard for compile-time type-checking. 43 | 44 | ```ts 45 | /* eslint-disable @typescript-eslint/no-explicit-any */ 46 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ 47 | import { IChannel, IMessage, ITeam } from './types'; 48 | 49 | export function isTypedArray( 50 | arr: unknown, 51 | check: (x: any) => x is T, 52 | ): arr is T[] {} 53 | 54 | export function isTeam(arg: any): arg is ITeam {} 55 | 56 | export function isChannel(arg: any): arg is IChannel {} 57 | 58 | export function isMessage(arg: any): arg is IMessage {} 59 | ``` 60 | 61 | and then apply these guards for all files in the [`/src/data/`](../src/data) folder 62 | 63 | --- 64 | 65 |

66 | Next:Tests For Types ▶ 67 |

68 | ``` 69 | -------------------------------------------------------------------------------- /notes/09-tests-for-types.md: -------------------------------------------------------------------------------- 1 |

2 | ◀ Back: Types at Runtime 3 |

4 | 5 | --- 6 | 7 | # Tests for types 8 | 9 | ## Why should we care? 10 | 11 | We need to do this for a couple of reasons: 12 | 13 | - Type information is code too. Breaking your users’ types is a breaking change 14 | - You can’t easily write useful negative test cases for type-equivalence in your value tests 15 | - TS introduces breaking changes with each middle-digit release, and you’ll usually detect these w/ type information (includes your “value tests” too for some things) 16 | - Maintaining compatibility w/ multiple TS versions 17 | - This normally wouldn’t be a big concern, but TS includes breaking changes with each “middle digit” release 18 | - It’s very easy (and very bad for users) to accidentally change your range of supported compiler versions 19 | 20 | ## Solutions to this problem 21 | 22 | TL;DR: TypeScript professionals need to know BOTH. You won’t get away from dtslint yet because it’s still used for everything in DefinitelyTyped, and it’s still the easiest way to guard against compatibility regressions. 23 | 24 | ### [`dtslint`](https://github.com/microsoft/dtslint) 25 | 26 | This is what’s used by DefinitelyTyped, and it effectively has 3 features 27 | 28 | ``` 29 | // $ExpectType 30 | // $ExpectError 31 | ``` 32 | 33 | dtslint tests a range of TS compiler versions. This may sound like it’s less than we need, but it’s enough to get a 34 | lot of release confidence. Because it’s a tool built on top of a linter, it can handle negative test cases 35 | without early termination 36 | 37 | #### Concerns 38 | 39 | - **SETUP** - It’s very particular about how it’s set up, and you end up doing things “just because the tool wants you to do it that way” (i.e., the index.d.ts) 40 | - **STRINGIFIED TYPES** - The \$ExpectType assertion is based on stringified types, which can change across compiler versions (i.e., `string|number` vs `number | string`) 41 | - **BUILT ON TOP OF TSLINT** - The linter it’s built on top of is tslint, which is now deprecated in favor of ESLint. We’ll talk about the difference later, but it’s not great to be so entangled with something that’s on its way out 42 | 43 | #### Benefits 44 | 45 | - Automatically downloads compiler versions and caches them 46 | - Tests against nightly TS builds 47 | - Microsoft uses this for DefinitelyTyped (read into this for stability) 48 | - Being built on top of a linter = you get lint-ish feedback in your editor 49 | 50 | ### [`tsd`](https://github.com/SamVerschueren/tsd) 51 | 52 | This is “the new kid on the block”, but it’s my current favorite for basic typescript testing 53 | 54 | #### Concerns 55 | 56 | At the time of writing, it does not allow testing against multiple compiler versions (Issue) 57 | 58 | #### Benefits 59 | 60 | - **Better assertion mechanism** 61 | Test assertions are based on type equivalence rather than stringified types 62 | - **Easier setup** 63 | Much less convoluted setup compared to dtslint 64 | - **Better range of assertions** 65 | Much wider range of assertions, which allow your tests to become much more readable and much less convoluted 66 | - **Support for deprecations** 67 | Catch that accidental (un-)deprecation. This is very important for responsible management of a stable API surface 68 | 69 | # Challenge: tests for types 70 | 71 | 1. `tsd` and `dtslint` are both installed. Set up a subfolder in `tests/` for each of them 72 | 2. wire both commands up to `yarn test` 73 | 74 | ## What to test? 75 | 76 | Make sure your new `isTypedArray` generic user-defined type guard works 77 | 78 | For your `dtslint` tests, use the following config files 79 | 80 | #### `tsconfig.json` 81 | 82 | ```json 83 | { 84 | "extends": "../../tsconfig.json", 85 | "compilerOptions": { 86 | "lib": ["ES2018", "DOM"], 87 | "strict": true, 88 | "baseUrl": "../.." 89 | }, 90 | "include": [".", "../../src"] 91 | } 92 | ``` 93 | 94 | #### `tslint.json` 95 | 96 | ```json 97 | { 98 | "extends": "dtslint/dtslint.json", 99 | "rules": { 100 | "no-relative-import-in-test": false, 101 | "semicolon": true 102 | } 103 | } 104 | ``` 105 | 106 | and run the command as follows 107 | 108 | ```sh 109 | yarn dtslint tests/types-dtslint 110 | ``` 111 | 112 | For tsd, make just make sure you follow the README carefully 113 | 114 | --- 115 | 116 |

117 | Next: Tests for Types ▶ 118 |

119 | ``` 120 | -------------------------------------------------------------------------------- /notes/10-declaration-files.md: -------------------------------------------------------------------------------- 1 |

2 | ◀ Back: Tests for Types 3 |

4 | 5 | --- 6 | 7 | # Declaration Files & DefinitelyTyped 8 | 9 | ## What is a declaration file? 10 | 11 | If TypeScript is “JS with added type information”, these files are purely the type information made to layer on top of an existing JS file. 12 | Why declaration files exist 13 | 14 | - TS is a language that compiles to JavaScript -- This is a critical driver of adoption 15 | - ...but it only provides value if type information comes along with dependencies 16 | - You rarely, if ever end up consuming .ts code directly -- it’ll nearly always be a declaration file and the matching JS source 17 | 18 | ## When I might find myself working in a declaration file 19 | 20 | - Overriding something broken in a DefinitelyTyped package 21 | - Typing runtime features that may not be totally standard (i.e., chrome/safari-specific APIs, a piece of code that might run in a Node.js sandbox) 22 | - Augmenting types with something more convenient (i.e., Object.keys returns a special string[]) 23 | 24 | ## How is it different from a regular .ts file? 25 | 26 | - You’re limited in terms of what you can put here 27 | - No values, although it can contain things that typically would have a value aspect to them 28 | - initializers can only be string/numeric/enum literals 29 | - No statements, only expressions 30 | - Nothing reassignable (i.e., `let`) 31 | - Source `.d.ts` files completely disappear as part of the compile process 32 | - You used to only be able to create ones from TypeScript source, but as of TS 3.7 you can now create them from `.js` as well 33 | - These usually are just a starting point and need some significant manual touch up 34 | - They’re code. You need tests (we’ll talk about this later) 35 | 36 | - **Manually created .d.ts:** they can become misaligned with the JS code they aim to describe 37 | - TypeScript’s type-checking is done exclusively using type information 38 | - This can create a situation where everything looks great 39 | 40 | ## DefinitelyTyped 41 | 42 | **A giant place for shared ambient type packages** 43 | Over 6k libraries are typed here 44 | 45 | **Publishes packages of the form `@types/*`** 46 | i.e., @types/react, @types/lodash 47 | 48 | **Community-run** 49 | People may list themselves as “owners” of certain types by adding their names to types//index.d.ts 50 | 51 | When new PRs come in that touch files in the folder for these types, you get mentioned by a bot and asked to perform a code review within a few (7) days. 52 | 53 | - If an owner “requests changes” in their review, you proceed with normal code review norms 54 | - As soon as any owner gives an approving review, the code is eligible for automated merge/publish 55 | - If nobody reviews the PR, the code gets merged anyway 56 | 57 | PRs from owners themselves are merged almost immediately 58 | 59 | ### Definitely Challenges 60 | 61 | There’s no mechanism (other than asking submitters to not do it) for maintainers to keep “ownership” in the same sense as “some authors can commit to React.js and some cannot” 62 | 63 | Maintainers have very little control over version numbers. They can only have a stream of types associated with each major version of the library being typed -- if they fix a bug in `@types/foo 3.4.5` and have already released `3.5.0`, there’s no way to release a `3.4.6`. 64 | 65 | ### Versioning Challenges 66 | 67 | On the issue of versioning, there’s an additional challenge. 68 | 69 | SemVer instructs us to use x.y.z version numbers as follows 70 | 71 | - **Major** - Incremented “when you make incompatible API changes” 72 | - **Minor** - Incremented “when you add functionality in a backwards compatible manner” 73 | - **Patch** - Incremented “when you make backwards compatible bug fixes” 74 | 75 | This way, particularly for a consumer of a lib, we know how much risk we’re exposing ourselves to when bumping from version A to B. 76 | 77 | Things get way more complicated when dealing with types 78 | 79 | - **Lib Major, Minor, Patch** - Which version of the library is being typed 80 | - **TypeMajor** - Incremented for breaking changes to types (i.e., dropping a compiler version 81 | - **TypeMinor** - Incremented for non-breaking functionality improvements to types 82 | - **TypePatch** - Incremented for “backwards compatible bug fixes” to types 83 | 84 | So we have 6 pieces of information here. 85 | 86 | We can reduce this to 5 if we’re willing to consider “backwards compatible bug fixes” to be “a similar thing” in users’ eyes, regardless of whether they happen in “values” or “types”. 87 | 88 | We can make other tough compromises (i.e., “we always track latest minor”) and get this to 4, but this will already either limit the kinds of changes that can be made 89 | 90 | ## Type Acquisition 91 | 92 | `--traceResolution` to log out extra information 93 | 94 | **Prioritization based on file type** 95 | .ts > .tsx > .js+.d.ts 96 | 97 | **Prioritization based on type of location** 98 | 99 | - tsconfig.json paths: {} 100 | - “Type roots” 101 | - Explicit module declarations 102 | - @types/\* 103 | 104 | **Prioritization based on location** 105 | 106 | Same as Node’s require.resolve algorithm, as long as moduleResolution: “Node” 107 | 108 | --- 109 | 110 |

111 | Next: API Extractor ▶ 112 |

113 | ``` 114 | -------------------------------------------------------------------------------- /notes/11-api-extractor.md: -------------------------------------------------------------------------------- 1 |

2 | ◀ Back: Declaration Files 3 |

4 | 5 | --- 6 | 7 | # API Extractor 8 | 9 | [API extractor](https://api-extractor.com/) is a tool that's part of Microsoft's tech stack, and 10 | in my opinion it's one of the most under-used and under-appreciated things in the TypeScript ecosystem. 11 | 12 | ## What problems does it solve? 13 | 14 | - Generating API documentation 15 | - Tells you if there are any changes in your API surface associated with a code change 16 | - Allows you to provide multiple _variants_ of your public API surface, at various levels of release maturity (i.e., "beta") 17 | 18 | It's also monorepo-friendly, which I'll go into in more detail in the **JavaScript and TypeScript Monorepos** course. 19 | 20 | ## Setup 21 | 22 | ```sh 23 | # Install dependencies 24 | yarn add -D @microsoft/api-extractor @microsoft/api-documenter 25 | 26 | # Generate an initial config file: api-extractor.json 27 | yarn api-extractor init 28 | ``` 29 | 30 | ## Configuration: The basics 31 | 32 | API extractor consumes _your tree of declaration files_ as an input. Let's make 33 | a tsconfig specifically for this. 34 | 35 | #### `tsconfig.apidocs.json` 36 | 37 | ```json 38 | { 39 | "extends": "./tsconfig.json", 40 | "compilerOptions": { 41 | "outDir": ".dist-types", 42 | "declaration": true, 43 | "noEmit": false, 44 | "emitDeclarationOnly": true, 45 | "jsx": "react" 46 | }, 47 | "include": ["src"] 48 | } 49 | ``` 50 | 51 | and add this `.dist-types` folder to our `.gitignore` 52 | 53 | ```diff 54 | --- a/.gitignore 55 | +++ b/.gitignore 56 | @@ -115,5 +115,5 @@ dist 57 | .yarn/install-state.gz 58 | .pnp.* 59 | 60 | +# Type information for api-extractor 61 | +.dist-types 62 | +# API report JSON generated by api-extractor 63 | +temp 64 | ``` 65 | 66 | Let's try to tell typescript to build using this config file and see what happens 67 | 68 | ```sh 69 | # build 70 | tsc -P tsconfig.apidocs.json 71 | 72 | # list contents of output folder 73 | ls -p .dist-types 74 | ``` 75 | 76 | You should see something like 77 | 78 | ``` 79 | data/ type-guards.d.ts ui/ 80 | index.d.ts types.d.ts utils/ 81 | ``` 82 | 83 | Note the absence of `.js` files. This is _just the type information_ 84 | 85 | Next, we need to tell `api-extractor` about this. The whole point of this library is to consume a public API surface of some sort. We kind of have an app, and this is more traditionally done with a library, but there's still a lot of value here! We can create a dedicated entry point just for api-extractor 86 | 87 | Create a new file `src/public-api-surface.ts` with the following content 88 | 89 | #### [`src/public-api-surface.ts`](../src/public-api-surface.ts) 90 | 91 | ```ts 92 | export { 93 | isChannel, 94 | isMessage, 95 | isTeam, 96 | isTypedArray, 97 | } from './type-guards'; 98 | export { IChannel, IMessage, ITeam, IUser } from './types'; 99 | 100 | export { apiCall } from './utils/networking'; 101 | export { formatTimestamp } from './utils/date'; 102 | 103 | export { 104 | HTTPErrorKind, 105 | default as HTTPError, 106 | } from './utils/http-error'; 107 | 108 | export { 109 | default as Deferred, 110 | RejectHandler, 111 | ResolveHandler, 112 | } from './utils/deferred'; 113 | ``` 114 | 115 | and build your declaration files again 116 | 117 | ```sh 118 | # build 119 | tsc -P tsconfig.apidocs.json 120 | 121 | # list contents of output folder 122 | ls -p .dist-types 123 | ``` 124 | 125 | You should see something like 126 | 127 | ``` 128 | data/ type-guards.d.ts utils/ 129 | index.d.ts types.d.ts 130 | public-api-surface.d.ts ui/ 131 | ``` 132 | 133 | Note the new `public-api-surface.d.ts`. This is the "main entry point" of our types, as far as api-extractor is concerned. 134 | 135 | Open [`api-extractor.json`](../api-extractor.json) and look for `mainEntryPointFilePath`. Update it to point to your new declaration file 136 | 137 | ```diff 138 | --- a/api-extractor.json 139 | +++ b/api-extractor.json 140 | @@ -45,7 +45,7 @@ 141 | * 142 | * SUPPORTED TOKENS: , , 143 | */ 144 | - "mainEntryPointFilePath": "/lib/index.d.ts", 145 | + "mainEntryPointFilePath": "/.dist-types/public-api-surface.d.ts", 146 | 147 | /** 148 | ``` 149 | 150 | and let's "extract the API surface" 151 | 152 | ```sh 153 | yarn api-extractor run --local 154 | ``` 155 | 156 | You should see a few things happen 157 | 158 | - you now have a `/etc` folder containing a markdown API report 159 | - you have a `/temp` folder with a `.json` and `.md` version of your API report 160 | - you'll see a bunch of warning messages in your console 161 | 162 | You'll want to commit that `/etc` folder -- it's the "human-approved" API surface that 163 | future changes will be compared to. Do not commit the `/temp` folder to git (it should be ignored, if you followed `.gitignore`-related instructions above). 164 | 165 | Add a few npm-scripts to your [`package.json`](../package.json) 166 | 167 | ```diff 168 | --- a/package.json 169 | +++ b/package.json 170 | @@ -6,7 +6,9 @@ 171 | "test": "yarn test-jest && yarn test-tsd && yarn test-dtslint", 172 | "test-jest": "jest tests/components", 173 | "test-tsd": "tsd tests/types-tsd", 174 | - "test-dtslint": "dtslint tests/types-dtslint" 175 | + "test-dtslint": "dtslint tests/types-dtslint", 176 | + "api-report": "tsc -b tsconfig.apidocs.json && yarn api-extractor run", 177 | + "api-docs": "api-documenter markdown -i temp -o docs" 178 | }, 179 | "devDependencies": { 180 | ``` 181 | 182 | Now you should be able to run 183 | 184 | ```sh 185 | # Update api report with any changes 186 | yarn update-api-report 187 | 188 | # Analyze the current state of the code, and 189 | # return 0 if api report is up to date 190 | yarn api-report 191 | 192 | # Extract the API information and generate docs 193 | yarn api-report && yarn api-docs 194 | ``` 195 | 196 | Next, let's fix the errors in your console, it shouldn't take long 197 | 198 | - `tsdoc-param-tag-missing-hyphen` wants `@param` JSDoc tags to look like 199 | 200 | ``` 201 | @param foo - description of foo 202 | ``` 203 | 204 | - `ae-missing-release-tag` wants everything that is exported to be marked as one of [four special JSDoc tags](https://api-extractor.com/pages/tsdoc/doc_comment_syntax/#release-tags) - `@public`, `@beta`, `@alpha` and `@internal` (in descending order of release maturity) 205 | 206 | ## Configuring the rollup 207 | 208 | Make the following changes to your [`api-extractor.json`](../api-extractor.json) file 209 | 210 | ```diff 211 | --- a/api-extractor.json 212 | +++ b/api-extractor.json 213 | @@ -181,7 +181,7 @@ 214 | /** 215 | * (REQUIRED) Whether to generate the .d.ts rollup file. 216 | */ 217 | - "enabled": true 218 | + "enabled": true, 219 | 220 | /** 221 | * Specifies the output path for a .d.ts rollup file to be generated without any trimming. 222 | @@ -195,7 +195,7 @@ 223 | * SUPPORTED TOKENS: , , 224 | * DEFAULT VALUE: "/dist/.d.ts" 225 | */ 226 | - // "untrimmedFilePath": "/dist/.d.ts", 227 | + "untrimmedFilePath": "/dist/-private.d.ts", 228 | 229 | /** 230 | * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "beta" release. 231 | @@ -207,7 +207,7 @@ 232 | * SUPPORTED TOKENS: , , 233 | * DEFAULT VALUE: "" 234 | */ 235 | - // "betaTrimmedFilePath": "/dist/-beta.d.ts", 236 | + "betaTrimmedFilePath": "/dist/-beta.d.ts", 237 | 238 | /** 239 | * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "public" release. 240 | @@ -221,7 +221,7 @@ 241 | * SUPPORTED TOKENS: , , 242 | * DEFAULT VALUE: "" 243 | */ 244 | - // "publicTrimmedFilePath": "/dist/-public.d.ts", 245 | + "publicTrimmedFilePath": "/dist/.d.ts" 246 | 247 | /** 248 | * When a declaration is trimmed, by default it will be replaced by a code comment such as 249 | ``` 250 | 251 | and re-run 252 | 253 | ```sh 254 | yarn update-api-report && yarn api-docs 255 | ``` 256 | 257 | You should see some new declaration files in your `/dist` folder now 258 | 259 | - `professional-ts.d.ts` for your public API 260 | - `professional-ts-beta.d.ts` for your "beta" public API 261 | - `professional-ts-private.d.ts` for your private API 262 | 263 | You could edit your [`package.json`](../package.json) to point to the public API 264 | `.d.ts` rollup 265 | 266 | ```diff 267 | --- a/package.json 268 | +++ b/package.json 269 | @@ -63,6 +64,7 @@ 270 | "parcel-bundler": "^1.12.4", 271 | "json-server": "^0.16.2" 272 | }, 273 | + "types": "dist/professional-ts.d.ts", 274 | "name": "professional-ts", 275 | "version": "0.0.0", 276 | "description": "Mike's \"professional TypeScript\" course", 277 | ``` 278 | 279 | If a consumer wanted to use your "beta" API, they'd just have to make a small 280 | modification to their `tsconfig.json` 281 | 282 | ```diff 283 | "compilerOptions": { 284 | + "paths": { 285 | + "professional-ts": ["node_modules/professional-ts/dist/professional-ts-beta.d.ts"] 286 | + } 287 | } 288 | ``` 289 | 290 | ## Even more capability 291 | 292 | There are all sorts of other capabilities of api-extractor. For example 293 | 294 | - See what happens if your [`src/public-api-surface.ts`](../src/public-api-surface.ts) no longer exports `ITeam` -- you'd be told that you can't link to it in your documentation 295 | - See what happens if you forget to export an interface that's part of a public API signature 296 | 297 | You have a lot of control over what's surfaced in the api-report, vs. in the console, 298 | and what's regarded as a warning vs. error. 299 | 300 | All in all, this provides unparalleled visibility into how proposed code changes 301 | would have ramifications on the public API surface of a library. 302 | 303 | --- 304 | 305 |

306 | Next: Tests for Types ▶ 307 |

308 | -------------------------------------------------------------------------------- /notes/README.md: -------------------------------------------------------------------------------- 1 | # Professional TypeScript: Course Notes 2 | 3 | Please begin reading at [`00-intro.md`](./00-intro.md) 4 | -------------------------------------------------------------------------------- /notes/img/eslint-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/notes/img/eslint-error.png -------------------------------------------------------------------------------- /notes/img/project_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/notes/img/project_screenshot.png -------------------------------------------------------------------------------- /notes/img/ts-3-essentials/slide-018.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/notes/img/ts-3-essentials/slide-018.png -------------------------------------------------------------------------------- /notes/img/ts-3-essentials/slide-019.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/notes/img/ts-3-essentials/slide-019.png -------------------------------------------------------------------------------- /notes/img/ts-3-essentials/slide-020.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/notes/img/ts-3-essentials/slide-020.png -------------------------------------------------------------------------------- /notes/img/ts-3-essentials/slide-021.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-north/professional-ts/ff7ee720ea0503b6ef35999651166fc1e3c41863/notes/img/ts-3-essentials/slide-021.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "parcel build index.html", 4 | "dev": "node ./server/index.js", 5 | "lint": "eslint src server tests --ext .js,.ts,.jsx,.tsx", 6 | "test": "yarn test-jest", 7 | "test-jest": "jest tests/components", 8 | "test-tsd": "tsd tests/types-tsd", 9 | "test-dtslint": "dtslint tests/types-dtslint" 10 | }, 11 | "devDependencies": { 12 | "@babel/plugin-proposal-class-properties": "^7.10.4", 13 | "@babel/preset-env": "^7.11.5", 14 | "@babel/preset-react": "^7.10.4", 15 | "@babel/preset-typescript": "^7.10.4", 16 | "@fullhuman/postcss-purgecss": "^3.0.0", 17 | "@microsoft/api-documenter": "^7.9.16", 18 | "@microsoft/api-extractor": "^7.10.4", 19 | "@types/express": "^4.17.8", 20 | "@types/jest": "^26.0.14", 21 | "@types/react": "^17.0.3", 22 | "@types/react-dom": "^17.0.2", 23 | "@types/react-router": "^5.1.8", 24 | "@types/react-router-dom": "^5.1.5", 25 | "@types/react-test-renderer": "^17.0.1", 26 | "@typescript-eslint/eslint-plugin": "^4.4.0", 27 | "@typescript-eslint/parser": "^4.3.0", 28 | "concurrently": "^6.0.0", 29 | "date-fns": "^2.16.1", 30 | "dtslint": "^4.0.4", 31 | "eslint": "^7.10.0", 32 | "eslint-config-prettier": "^8.1.0", 33 | "eslint-plugin-node": "^11.1.0", 34 | "eslint-plugin-prettier": "^3.1.4", 35 | "eslint-plugin-promise": "^4.2.1", 36 | "eslint-plugin-react": "^7.21.2", 37 | "eslint-plugin-sonarjs": "^0.6.0", 38 | "jest": "^26.4.2", 39 | "nodemon": "^2.0.4", 40 | "postcss": "^8.1.1", 41 | "postcss-import": "^12.0.0", 42 | "postcss-nesting": "^7.0.1", 43 | "postcss-preset-env": "^6.0.0", 44 | "postcss-purgecss": "^2.0.0", 45 | "prettier": "^2.2.1", 46 | "react": "^17.0.1", 47 | "react-dom": "^17.0.1", 48 | "react-router-dom": "^5.2.0", 49 | "react-test-renderer": "^17.0.1", 50 | "sass": "^1.26.11", 51 | "tailwindcss": "^1.8.10", 52 | "tsd": "^0.14.0", 53 | "typescript": "^4.2.3" 54 | }, 55 | "volta": { 56 | "node": "12.18.4", 57 | "yarn": "1.22.10" 58 | }, 59 | "dependencies": { 60 | "express": "^4.17.1", 61 | "json-server": "^0.16.2", 62 | "parcel-bundler": "^1.12.4" 63 | }, 64 | "name": "professional-ts", 65 | "version": "1.1.0", 66 | "description": "Mike's \"professional TypeScript\" course", 67 | "repository": "https://github.com/mike-north/professional-ts", 68 | "author": "Mike North ", 69 | "license": "BSD-2-Clause", 70 | "private": true, 71 | "jest": { 72 | "verbose": true, 73 | "collectCoverageFrom": [ 74 | "src/**/*.{js,jsx}", 75 | "!**/node_modules/**", 76 | "!**/vendor/**", 77 | "!server/**" 78 | ] 79 | }, 80 | "resolutions": { 81 | "node-forge": "^0.10.0" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "node" 3 | } 4 | -------------------------------------------------------------------------------- /server/api-server.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const jsonServer = require('json-server'); 3 | const router = jsonServer.router('db.json'); 4 | const middlewares = jsonServer.defaults(); 5 | 6 | /** 7 | * @param {import('express').Request} req 8 | * @param {import('express').Response} res 9 | * @param {import('express').NextFunction} next 10 | */ 11 | function SINGULAR_MIDDLEWARE(req, res, next) { 12 | const _send = res.send; 13 | res.send = function (body) { 14 | if (req.url.indexOf('singular') >= 0) { 15 | try { 16 | const json = JSON.parse(body); 17 | if (Array.isArray(json)) { 18 | if (json.length === 1) { 19 | return _send.call(this, JSON.stringify(json[0])); 20 | } else if (json.length === 0) { 21 | return _send.call(this, '{}', 404); 22 | } 23 | } 24 | } catch (e) { 25 | throw new Error('Problem unwrapping array'); 26 | } 27 | } 28 | return _send.call(this, body); 29 | }; 30 | next(); 31 | } 32 | 33 | /** 34 | * @param {import('express').Request} req 35 | * @param {import('express').Response} _res 36 | * @param {import('express').NextFunction} next 37 | */ 38 | function addDateToPost(req, _res, next) { 39 | if (req.method === 'POST') { 40 | req.body.createdAt = Date.now(); 41 | } 42 | // Continue to JSON Server router 43 | next(); 44 | } 45 | 46 | function setupAPI(server) { 47 | server.use('/api', SINGULAR_MIDDLEWARE, ...middlewares); 48 | server.use(jsonServer.bodyParser); 49 | 50 | server.use(addDateToPost); 51 | server.use( 52 | jsonServer.rewriter({ 53 | '/api/teams': '/api/teams?_embed=channels', 54 | '/api/teams/:id': '/api/teams/:id?_embed=channels', 55 | '/api/teams/:id/channels': '/api/channels?teamId=:id', 56 | '/api/teams/:id/channels/:channelId': 57 | '/api/channels?id=:channelId&teamId=:id&singular=1', 58 | '/api/teams/:id/channels/:channelId/messages': 59 | '/api/messages?_expand=user&teamId=:id&channelId=:channelId', 60 | }), 61 | ); 62 | 63 | server.use('/api', router); 64 | return server; 65 | } 66 | 67 | module.exports = { setupAPI }; 68 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const Bundler = require('parcel-bundler'); 3 | const e = require('express'); 4 | const jsonServer = require('json-server'); 5 | const server = jsonServer.create(); 6 | const { setupAPI } = require('./api-server'); 7 | const { join } = require('path'); 8 | 9 | const PORT = process.env['PORT'] || 3000; 10 | 11 | const app = e(); 12 | 13 | setupAPI(server); 14 | 15 | const file = join(__dirname, '..', 'index.html'); // Pass an absolute path to the entrypoint here 16 | const options = {}; // See options section of api docs, for the possibilities 17 | 18 | // Initialize a new bundler using a file and options 19 | const bundler = new Bundler(file, options); 20 | 21 | app.use('/assets', e.static(join(__dirname, '..', 'assets'))); 22 | app.use(server); 23 | app.use(bundler.middleware()); 24 | 25 | // Listen on port PORT 26 | app.listen(PORT, () => { 27 | console.log(`Serving on http://localhost:${PORT}`) 28 | }); 29 | -------------------------------------------------------------------------------- /src/data/channels.js: -------------------------------------------------------------------------------- 1 | import { apiCall } from '../utils/networking'; 2 | 3 | const cachedChannelRecords = {}; 4 | 5 | export async function getChannelById(id) { 6 | let cached = cachedChannelRecords[id]; 7 | if (typeof cached !== 'undefined') return await cached; 8 | cached = cachedChannelRecords[id] = apiCall(`Channels/${id}`); 9 | 10 | return await cached; 11 | } 12 | -------------------------------------------------------------------------------- /src/data/messages.js: -------------------------------------------------------------------------------- 1 | import { apiCall } from '../utils/networking'; 2 | 3 | const cachedMessageRecordArrays = {}; 4 | 5 | export async function getChannelMessages(teamId, channelId) { 6 | let cached = cachedMessageRecordArrays[channelId]; 7 | if (typeof cached === 'undefined') 8 | cached = cachedMessageRecordArrays[channelId] = apiCall( 9 | `teams/${teamId}/channels/${channelId}/messages`, 10 | ); 11 | return await cached; 12 | } 13 | -------------------------------------------------------------------------------- /src/data/teams.js: -------------------------------------------------------------------------------- 1 | import { apiCall } from '../utils/networking'; 2 | 3 | let cachedAllTeamsList; 4 | export async function getAllTeams() { 5 | if (typeof cachedAllTeamsList === 'undefined') 6 | cachedAllTeamsList = apiCall('teams'); 7 | 8 | return await cachedAllTeamsList; 9 | } 10 | 11 | const cachedTeamRecords = {}; 12 | 13 | export async function getTeamById(id) { 14 | let cached = cachedTeamRecords[id]; 15 | if (typeof cached === 'undefined') 16 | cached = cachedTeamRecords[id] = apiCall(`teams/${id}`); 17 | return await cached; 18 | } 19 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import 'regenerator-runtime/runtime'; 2 | import * as React from 'react'; 3 | import { render } from 'react-dom'; 4 | import App from './ui/App'; 5 | 6 | function initializeReactApp() { 7 | const appContainer = document.getElementById('appContainer'); 8 | if (!appContainer) throw new Error('No #appContainer found in DOM'); 9 | render(React.createElement(App), appContainer); 10 | } 11 | 12 | initializeReactApp(); 13 | -------------------------------------------------------------------------------- /src/ui/App.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | BrowserRouter as Router, 4 | Route, 5 | Switch, 6 | } from 'react-router-dom'; 7 | import { getAllTeams } from '../data/teams'; 8 | import { useAsyncDataEffect } from '../utils/api'; 9 | import Loading from './components/Loading'; 10 | import SelectedTeam from './components/SelectedTeam'; 11 | import TeamSelector from './components/TeamSelector'; 12 | 13 | const { useState } = React; 14 | 15 | const App = () => { 16 | const [teams, setTeams] = useState(); 17 | 18 | useAsyncDataEffect(() => getAllTeams(), { 19 | setter: setTeams, 20 | stateName: 'teams', 21 | }); 22 | if (!teams) return ; 23 | return ( 24 | 25 |
26 | 27 | 28 | 29 |
30 |

Please select a team

31 |
32 |
33 | 34 |
35 |

Please select a team

36 |
37 |
38 | ( 41 | 42 | )} 43 | /> 44 |
45 |
46 |
47 | ); 48 | }; 49 | export default App; 50 | -------------------------------------------------------------------------------- /src/ui/components/Channel.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { getChannelMessages } from '../../data/messages'; 3 | import { useAsyncDataEffect } from '../../utils/api'; 4 | import ChannelFooter from './Channel/Footer'; 5 | import ChannelHeader from './Channel/Header'; 6 | import ChannelMessage from './Channel/Message'; 7 | import Loading from './Loading'; 8 | 9 | 10 | const Channel = ({ 11 | channel, 12 | }) => { 13 | 14 | const [messages, setMessages] = React.useState(); 15 | useAsyncDataEffect( 16 | () => getChannelMessages(channel.teamId, channel.id), 17 | { 18 | setter: setMessages, 19 | stateName: 'messages', 20 | otherStatesToMonitor: [channel], 21 | }, 22 | ); 23 | if (!messages) return ; 24 | if (messages.length === 0) return ; 25 | console.log( 26 | `%c CHANNEL render: ${channel.name}`, 27 | 'background-color: purple; color: white', 28 | ); 29 | return ( 30 |
31 | 35 |
39 | {messages.map((m) => ( 40 | 46 | ))} 47 |
48 | 49 | 50 |
51 | ); 52 | }; 53 | export default Channel; 54 | -------------------------------------------------------------------------------- /src/ui/components/Channel/Footer.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | const Footer = ({ channel: { name: channelName } }) => ( 4 |
5 |
9 |

10 | Message Input 11 |

12 | 13 | 26 | 27 | 30 | 31 | 37 | 38 | 44 |
45 |
46 | ); 47 | 48 | export default Footer; 49 | -------------------------------------------------------------------------------- /src/ui/components/Channel/Header.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | const Header = ({ title, description }) => ( 4 |
5 |
6 |

7 | 8 | {title} 9 |

10 |

11 | {description} 12 |

13 |
14 |
15 | ); 16 | 17 | export default Header; 18 | -------------------------------------------------------------------------------- /src/ui/components/Channel/Message.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { formatTimestamp } from '../../../utils/date'; 3 | 4 | const Message = ({ user, date, body }) => ( 5 |
9 |
10 | {user.name} 15 |
16 | 17 |
18 |
19 | 23 | {user.name} 24 | 25 | at 26 | 29 |
30 | 31 |

32 | {body} 33 |

34 |
35 | 36 | 42 |
43 | ); 44 | 45 | export default Message; 46 | -------------------------------------------------------------------------------- /src/ui/components/Loading.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | const Loading = ({ message = 'Loading...', children }) => ( 4 |

5 | {message}... 6 | {children} 7 |

8 | ); 9 | export default Loading; 10 | -------------------------------------------------------------------------------- /src/ui/components/SelectedChannel.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Channel from './Channel'; 3 | 4 | const SelectedChannel = ({ match, channels }) => { 5 | if (!channels) throw new Error('no channels'); 6 | if (!match) throw new Error('no match'); 7 | 8 | const { params } = match; 9 | if (!match) return

No match params

; 10 | const { channelId: selectedChannelId } = params; 11 | if (!selectedChannelId) return

Invalid channelId

; 12 | const selectedChannel = channels.find( 13 | (c) => c.id === selectedChannelId, 14 | ); 15 | if (!selectedChannel) 16 | return ( 17 |
18 |

Could not find channel with id {selectedChannelId}

19 |
{JSON.stringify(channels, null, '  ')}
20 |
21 | ); 22 | 23 | return ; 24 | }; 25 | 26 | export default SelectedChannel; 27 | -------------------------------------------------------------------------------- /src/ui/components/SelectedTeam.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Team from './Team'; 3 | 4 | const SelectedTeam = ({ match, teams }) => { 5 | if (!match) throw new Error('no match'); 6 | 7 | const { params } = match; 8 | if (!params) throw new Error('no match params'); 9 | 10 | const { teamId: selectedTeamId } = params; 11 | if (!selectedTeamId) throw new Error(`undefined teamId`); 12 | 13 | const selectedTeam = teams.find((t) => t.id === selectedTeamId); 14 | if (!selectedTeam) 15 | throw new Error( 16 | `Invalid could not find team with id {selectedTeamId}`, 17 | ); 18 | 19 | return ; 20 | }; 21 | 22 | export default SelectedTeam; 23 | -------------------------------------------------------------------------------- /src/ui/components/Team.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Route, Switch } from 'react-router-dom'; 3 | import SelectedChannel from './SelectedChannel'; 4 | import TeamSidebar from './TeamSidebar'; 5 | 6 | const Team = ({ team }) => { 7 | console.log( 8 | `%c TEAM render: ${team.name}`, 9 | 'background-color: blue; color: white', 10 | ); 11 | const { channels } = team; 12 | return ( 13 |
14 | 15 | 16 | 17 |

Please select a channel

18 |
19 | ( 23 | 24 | )} 25 | /> 26 |
27 |
28 | ); 29 | }; 30 | export default Team; 31 | -------------------------------------------------------------------------------- /src/ui/components/TeamSelector.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import TeamLink from './TeamSelector/TeamLink'; 3 | 4 | const TeamSelector = ({ teams }) => ( 5 | 24 | ); 25 | 26 | export default TeamSelector; 27 | -------------------------------------------------------------------------------- /src/ui/components/TeamSelector/TeamLink.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Link, useRouteMatch } from 'react-router-dom'; 3 | 4 | const TeamLink = ({ team }) => { 5 | const match = useRouteMatch({ 6 | path: `/team/${team.id}`, 7 | exact: false, 8 | }); 9 | 10 | return ( 11 | 18 |
19 | {`Join 24 |
25 | 26 | ); 27 | }; 28 | 29 | export default TeamLink; 30 | -------------------------------------------------------------------------------- /src/ui/components/TeamSidebar.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import ChannelLink from './TeamSidebar/ChannelLink'; 3 | 4 | const TeamSidebar = ({ team }) => { 5 | return ( 6 |
7 |
8 |
9 |

10 | {team.name} 11 |

12 | 13 |
14 | 21 | 22 | Chris User 23 | 24 |
25 |
26 | 27 | 39 |
40 | 41 | 68 |
69 | 72 |
73 |
74 | ); 75 | }; 76 | 77 | export default TeamSidebar; 78 | -------------------------------------------------------------------------------- /src/ui/components/TeamSidebar/ChannelLink.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Link, useRouteMatch } from 'react-router-dom'; 3 | 4 | const ChannelLink = ({ to, channel }) => { 5 | const match = useRouteMatch(to); 6 | return ( 7 | 14 | 15 | {channel.name} 16 | 17 | ); 18 | }; 19 | 20 | export default ChannelLink; 21 | -------------------------------------------------------------------------------- /src/utils/api.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable promise/always-return */ 2 | import { useEffect } from 'react'; 3 | import Deferred from './deferred'; 4 | 5 | /** 6 | * 7 | * @param {() => Promise} getData 8 | * @param {{ 9 | stateName: string; 10 | otherStatesToMonitor?: unknown[]; 11 | setter: (arg: x) => void; 12 | }} options 13 | @return {void} 14 | */ 15 | export function useAsyncDataEffect(getData, options) { 16 | let cancelled = false; 17 | const { setter, stateName } = options; 18 | useEffect(() => { 19 | const d = new Deferred(); 20 | 21 | getData() 22 | .then((jsonData) => { 23 | if (cancelled) return; 24 | else d.resolve(jsonData); 25 | }) 26 | .catch(d.reject); 27 | 28 | d.promise 29 | .then((data) => { 30 | if (!cancelled) { 31 | console.info( 32 | '%c Updating state: ' + stateName, 33 | 'background: green; color: white; display: block;', 34 | ); 35 | setter(data); 36 | } 37 | }) 38 | .catch(console.error); 39 | return () => { 40 | cancelled = true; 41 | }; 42 | }, [...(options.otherStatesToMonitor || []), stateName]); 43 | } 44 | -------------------------------------------------------------------------------- /src/utils/date.js: -------------------------------------------------------------------------------- 1 | import { format } from 'date-fns'; 2 | 3 | /** 4 | * Format a timestamp as a string 5 | * @param {Date} date 6 | * 7 | * @return {string} 8 | */ 9 | export function formatTimestamp(date) { 10 | return format(date, 'MMM dd, yyyy HH:MM:SS a'); 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/deferred.js: -------------------------------------------------------------------------------- 1 | class Deferred { 2 | #_promise; 3 | #_resolve; 4 | #_reject; 5 | constructor() { 6 | this.#_promise = new Promise((resolve, reject) => { 7 | this.#_resolve = resolve; 8 | this.#_reject = reject; 9 | }); 10 | } 11 | get promise() { 12 | return this.#_promise; 13 | } 14 | get resolve() { 15 | return this.#_resolve; 16 | } 17 | get reject() { 18 | return this.#_reject; 19 | } 20 | } 21 | export default Deferred; 22 | -------------------------------------------------------------------------------- /src/utils/error.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Stringify an Error instance 5 | * @param {Error} err - The error to stringify 6 | * @return {string} 7 | */ 8 | function stringifyErrorValue(err) { 9 | return `${err.name.toUpperCase()}: ${err.message} 10 | ${err.stack || '(no stack trace information)'}`; 11 | } 12 | 13 | /** 14 | * Stringify a thrown value 15 | * 16 | * @param {string} errorDescription 17 | * @param {any} err 18 | * 19 | * @return {string} 20 | */ 21 | export function stringifyError(errorDescription, err) { 22 | return `${errorDescription}\n${ 23 | err instanceof Error 24 | ? stringifyErrorValue(err) 25 | : err 26 | ? '' + err 27 | : '(missing error information)' 28 | }`; 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/http-error.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @enum {number} 3 | */ 4 | export const HTTPErrorKind = { 5 | Information: 100, 6 | Success: 200, 7 | Redirect: 300, 8 | Client: 400, 9 | Server: 500, 10 | }; 11 | 12 | /** 13 | * 14 | * @param {number} status 15 | * @return {HTTPErrorKind} 16 | */ 17 | function determineKind(status) { 18 | if (status >= 100 && status < 200) return HTTPErrorKind.Information; 19 | else if (status < 300) return HTTPErrorKind.Success; 20 | else if (status < 400) return HTTPErrorKind.Redirect; 21 | else if (status < 500) return HTTPErrorKind.Client; 22 | else if (status < 600) return HTTPErrorKind.Server; 23 | else throw new Error(`Unknown HTTP status code ${status}`); 24 | } 25 | 26 | /** @param {HTTPErrorKind} kind */ 27 | export default class HTTPError extends Error { 28 | /** 29 | * 30 | * @param {Response} info 31 | * @param {string} message 32 | */ 33 | constructor(info, message) { 34 | super( 35 | `HTTPError [status: ${info.statusText} (${info.status})]\n${message}`, 36 | ); 37 | this.kind = determineKind(info.status); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/utils/networking.js: -------------------------------------------------------------------------------- 1 | import { stringifyError } from './error'; 2 | import HTTPError from './http-error'; 3 | 4 | /** 5 | * 6 | * @param {RequestInfo} input 7 | * @param {RequestInit} [init] 8 | */ 9 | async function getJSON(input, init) { 10 | try { 11 | const response = await fetch(input, init); 12 | const responseJSON = await response.json(); 13 | return { response, json: responseJSON }; 14 | } catch (err) { 15 | throw new Error( 16 | stringifyError( 17 | `Networking/getJSON: An error was encountered while fetching ${JSON.stringify( 18 | input, 19 | )}`, 20 | err, 21 | ), 22 | ); 23 | } 24 | } 25 | 26 | /** 27 | * 28 | * @param {string} path 29 | * @param {RequestInit} [init] 30 | */ 31 | export async function apiCall(path, init) { 32 | let response; 33 | let json; 34 | try { 35 | const jsonRespInfo = await getJSON(`/api/${path}`, init); 36 | response = jsonRespInfo.response; 37 | json = jsonRespInfo.json; 38 | } catch (err) { 39 | if (err instanceof HTTPError) throw err; 40 | throw new Error( 41 | stringifyError( 42 | `Networking/apiCall: An error was encountered while making api call to ${path}`, 43 | err, 44 | ), 45 | ); 46 | } 47 | if (!response.ok) 48 | throw new HTTPError(response, 'Problem while making API call'); 49 | return json; 50 | } 51 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const { colors } = require('tailwindcss/defaultTheme'); 2 | 3 | module.exports = { 4 | future: { 5 | removeDeprecatedGapUtilities: true, 6 | purgeLayersByDefault: true, 7 | }, 8 | purge: { 9 | enabled: true, 10 | content: ['index.html', './src/ui/**/*.jsx', './src/ui/**/*.tsx'], 11 | mode: 'all', 12 | }, 13 | theme: { 14 | extend: { 15 | colors: { 16 | black: colors.black, 17 | white: colors.white, 18 | 19 | red: { 20 | ...colors.red, 21 | ...{ 22 | 400: '#ef5753', 23 | 100: '#fcebea', 24 | }, 25 | }, 26 | 27 | indigo: { 28 | ...colors.indigo, 29 | ...{ 30 | 800: '#2f365f', 31 | 900: '#191e38', 32 | }, 33 | }, 34 | 35 | purple: { 36 | ...colors.purple, 37 | ...{ 38 | 300: '#d6bbfc', 39 | }, 40 | }, 41 | }, 42 | }, 43 | }, 44 | variants: { 45 | appearance: [ 46 | 'responsive', 47 | 'hover', 48 | 'focus', 49 | 'active', 50 | 'group-hover', 51 | ], 52 | backgroundAttachment: ['responsive'], 53 | backgroundColors: ['responsive', 'hover', 'focus'], 54 | backgroundPosition: ['responsive'], 55 | backgroundRepeat: [], 56 | backgroundSize: ['responsive'], 57 | borderCollapse: [], 58 | borderColors: ['responsive', 'hover', 'focus'], 59 | borderRadius: ['responsive'], 60 | borderStyle: ['responsive', 'hover', 'focus'], 61 | borderWidths: ['responsive'], 62 | cursor: ['responsive'], 63 | display: ['responsive'], 64 | flexbox: ['responsive'], 65 | float: ['responsive'], 66 | fonts: ['responsive'], 67 | fontWeights: ['responsive', 'hover', 'focus'], 68 | height: ['responsive'], 69 | leading: ['responsive'], 70 | lists: ['responsive'], 71 | margin: ['responsive'], 72 | maxHeight: ['responsive'], 73 | maxWidth: ['responsive'], 74 | minHeight: ['responsive'], 75 | minWidth: ['responsive'], 76 | negativeMargin: ['responsive'], 77 | objectFit: false, 78 | objectPosition: false, 79 | opacity: ['responsive', 'hover'], 80 | outline: ['focus'], 81 | overflow: ['responsive'], 82 | padding: ['responsive'], 83 | pointerEvents: ['responsive'], 84 | position: ['responsive'], 85 | resize: ['responsive'], 86 | shadows: ['responsive', 'hover', 'focus'], 87 | svgFill: [], 88 | svgStroke: [], 89 | tableLayout: ['responsive'], 90 | textAlign: ['responsive'], 91 | textColors: ['responsive', 'hover', 'focus'], 92 | textSizes: ['responsive'], 93 | textStyle: ['responsive', 'hover', 'focus'], 94 | tracking: ['responsive'], 95 | userSelect: ['responsive'], 96 | verticalAlign: ['responsive'], 97 | visibility: ['responsive'], 98 | whitespace: ['responsive'], 99 | width: ['responsive'], 100 | zIndex: ['responsive'], 101 | }, 102 | plugins: [], 103 | }; 104 | -------------------------------------------------------------------------------- /tests/components/Channel.test.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as renderer from 'react-test-renderer'; 3 | import Channel from '../../src/ui/components/Channel'; 4 | 5 | test('Link changes the class when hovered', () => { 6 | const component = renderer.create( 7 | , 15 | ); 16 | const tree = component.toJSON(); 17 | expect(tree).toMatchSnapshot(); 18 | }); 19 | -------------------------------------------------------------------------------- /tests/components/ChannelFooter.test.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as renderer from 'react-test-renderer'; 3 | import Footer from '../../src/ui/components/Channel/Footer'; 4 | 5 | test('Link changes the class when hovered', () => { 6 | const component = renderer.create( 7 |