├── challenges ├── json-types │ ├── src │ │ └── index.ts │ ├── test │ │ ├── index.d.ts │ │ ├── tslint.json │ │ ├── tsconfig.json │ │ └── json-types.test.ts │ ├── tsconfig.json │ └── package.json ├── advanced-types │ ├── test │ │ ├── index.d.ts │ │ ├── tslint.json │ │ ├── tsconfig.json │ │ ├── mix.test.ts │ │ ├── optional-property-names-of.test.ts │ │ ├── required-property-names-of.test.ts │ │ └── extract-property-names-assignable-to.test.ts │ ├── tsconfig.json │ ├── package.json │ └── src │ │ └── index.ts ├── dict │ ├── tsconfig.json │ ├── test │ │ ├── mocha.opts │ │ └── dict.test.ts │ ├── src │ │ └── index.ts │ └── package.json └── address-book │ ├── tsconfig.json │ ├── test │ ├── mocha.opts │ └── address-book.test.ts │ ├── README.md │ ├── package.json │ └── src │ └── index.js ├── .gitignore ├── renovate.json ├── lerna.json ├── notes ├── package.json ├── tsconfig.json ├── 9-compiler-api.ts ├── 4-class-basics.ts ├── 8-declaration-merging.ts ├── 7-advanced-types.ts ├── 2-function-basics.ts ├── 3-interface-type-basics.ts ├── 5-generics-basics.ts ├── 1-basics.ts └── 6-guards-and-extreme-types.ts ├── tsconfig.json ├── examples └── hello-ts │ ├── README.md │ ├── src │ └── index.ts │ └── package.json ├── .eslintrc ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── .travis.yml ├── exerciseLICENSE ├── CHANGELOG.md ├── LICENSE ├── package.json ├── ts.code-workspace └── README.md /challenges/json-types/src/index.ts: -------------------------------------------------------------------------------- 1 | // 💡 HINT: number[] and Array mean the same thing 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/out 3 | **/lib 4 | .vscode 5 | lerna-debug.log 6 | **/tempCodeRunnerFile.ts -------------------------------------------------------------------------------- /challenges/json-types/test/index.d.ts: -------------------------------------------------------------------------------- 1 | // TypeScript Version: 2.8 2 | 3 | export * from "../src/index"; 4 | -------------------------------------------------------------------------------- /challenges/advanced-types/test/index.d.ts: -------------------------------------------------------------------------------- 1 | // TypeScript Version: 2.8 2 | 3 | export * from "../src/index"; 4 | -------------------------------------------------------------------------------- /challenges/dict/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true 4 | }, 5 | "include": ["src"] 6 | } 7 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@mike-works/js-lib-renovate-config"], 3 | "baseBranches": ["master", "v2", "v1"] 4 | } 5 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["examples/*", "challenges/*"], 3 | "version": "0.0.0", 4 | "useWorkspaces": true, 5 | "npmClient": "yarn" 6 | } 7 | -------------------------------------------------------------------------------- /challenges/address-book/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": true 5 | }, 6 | "include": ["src"] 7 | } 8 | -------------------------------------------------------------------------------- /challenges/dict/test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require ts-node/register 2 | --require source-map-support/register 3 | --timeout 60000 4 | --watch-extensions js,ts,json 5 | **/*.test.ts -------------------------------------------------------------------------------- /challenges/address-book/test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require ts-node/register 2 | --require source-map-support/register 3 | --timeout 60000 4 | --watch-extensions js,ts,json 5 | test/**/*.test.ts -------------------------------------------------------------------------------- /notes/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-workshop-notes", 3 | "private": true, 4 | "version": "0.0.0-development", 5 | "devDependencies": { 6 | "typescript": "3.7.7" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /challenges/dict/src/index.ts: -------------------------------------------------------------------------------- 1 | export type Dict = {}; 2 | 3 | // Array.prototype.map, but for Dict 4 | export function mapDict() {} 5 | 6 | // Array.prototype.reduce, but for Dict 7 | export function reduceDict() {} 8 | -------------------------------------------------------------------------------- /challenges/json-types/test/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "dtslint/dtslint.json", // Or "dtslint/dt.json" if on DefinitelyTyped 3 | "rules": { 4 | "semicolon": false, 5 | "indent": [true, "tabs"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /challenges/advanced-types/test/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "dtslint/dtslint.json", // Or "dtslint/dt.json" if on DefinitelyTyped 3 | "rules": { 4 | "semicolon": false, 5 | "indent": false, 6 | "prefer-const": false 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /notes/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "strictNullChecks": true, 5 | "noUnusedLocals": false, 6 | "noUnusedParameters": false, 7 | "strictBindCallApply": true, 8 | "strictPropertyInitialization": true 9 | }, 10 | "include": ["**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "strictNullChecks": true, 5 | "strict": true, 6 | "strictFunctionTypes": true, 7 | "strictBindCallApply": true, 8 | "noUnusedLocals": false, 9 | "noUnusedParameters": false 10 | }, 11 | "include": ["examples/*/src", "challenges/*/src", "notes"] 12 | } 13 | -------------------------------------------------------------------------------- /challenges/json-types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "commonjs", 5 | "outDir": "lib", 6 | "strict": true, 7 | "strictBindCallApply": true, 8 | "strictFunctionTypes": true, 9 | "strictNullChecks": true, 10 | "declaration": true, 11 | "sourceMap": true 12 | }, 13 | "include": ["src"] 14 | } 15 | -------------------------------------------------------------------------------- /challenges/advanced-types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "commonjs", 5 | "outDir": "lib", 6 | "strict": true, 7 | "strictBindCallApply": true, 8 | "strictFunctionTypes": true, 9 | "strictNullChecks": true, 10 | "declaration": true, 11 | "sourceMap": true 12 | }, 13 | "include": ["src"] 14 | } 15 | -------------------------------------------------------------------------------- /examples/hello-ts/README.md: -------------------------------------------------------------------------------- 1 | # `tsc` examples 2 | 3 | ## TODO 4 | 5 | - [ ] compile with `lib` as the output directory 6 | - [ ] compile in "watch mode" 7 | - [ ] compile with declaration file output 8 | - [ ] compile to ES2015 vs commonjs modules 9 | - [ ] use a `tsconfig.json` 10 | - [ ] source maps 11 | - [ ] declaration maps 12 | - [ ] module target (`es3` vs `es5` vs `es2017`) 13 | -------------------------------------------------------------------------------- /challenges/address-book/README.md: -------------------------------------------------------------------------------- 1 | # `tsc` examples 2 | 3 | ## TODO 4 | 5 | - [ ] compile with `lib` as the output directory 6 | - [ ] compile in "watch mode" 7 | - [ ] compile with declaration file output 8 | - [ ] compile to ES2015 vs commonjs modules 9 | - [ ] use a `tsconfig.json` 10 | - [ ] source maps 11 | - [ ] declaration maps 12 | - [ ] module target (`es3` vs `es5` vs `es2017`) 13 | -------------------------------------------------------------------------------- /challenges/address-book/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "address-book", 3 | "version": "0.0.0", 4 | "private": true, 5 | "directories": { 6 | "lib": "lib", 7 | "test": "test" 8 | }, 9 | "files": [ 10 | "lib" 11 | ], 12 | "scripts": { 13 | "clean": "rimraf lib", 14 | "build": "tsc src/index.ts --outDir lib --types node --module commonjs --target ES2017", 15 | "test": "mocha" 16 | }, 17 | "devDependencies": {} 18 | } 19 | -------------------------------------------------------------------------------- /challenges/json-types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-types", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "> TODO: description", 6 | "author": "Mike North ", 7 | "homepage": "", 8 | "directories": { 9 | "lib": "lib", 10 | "test": "test" 11 | }, 12 | "files": [ 13 | "lib" 14 | ], 15 | "scripts": { 16 | "clean": "rimraf lib", 17 | "build": "tsc", 18 | "test": "dtslint test" 19 | }, 20 | "devDependencies": {} 21 | } 22 | -------------------------------------------------------------------------------- /challenges/advanced-types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "advanced-type-challenges", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "> TODO: description", 6 | "author": "Mike North ", 7 | "homepage": "", 8 | "directories": { 9 | "lib": "lib", 10 | "test": "test" 11 | }, 12 | "files": [ 13 | "lib" 14 | ], 15 | "scripts": { 16 | "clean": "rimraf lib", 17 | "build": "tsc", 18 | "test": "dtslint test" 19 | }, 20 | "devDependencies": {} 21 | } 22 | -------------------------------------------------------------------------------- /challenges/dict/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dict", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "> TODO: description", 6 | "author": "Mike North ", 7 | "homepage": "", 8 | "directories": { 9 | "lib": "lib", 10 | "test": "test" 11 | }, 12 | "files": [ 13 | "lib" 14 | ], 15 | "scripts": { 16 | "clean": "rimraf lib", 17 | "build": "tsc src/index.ts --outDir lib --types node --module commonjs --target ES2017", 18 | "test": "mocha" 19 | }, 20 | "devDependencies": {} 21 | } 22 | -------------------------------------------------------------------------------- /examples/hello-ts/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a promise that resolves after some time 3 | * @param n number of milliseconds before promise resolves 4 | */ 5 | function timeout(n: number) { 6 | return new Promise(res => setTimeout(res, n)); 7 | } 8 | 9 | /** 10 | * Add three numbers 11 | * @param a first number 12 | * @param b second 13 | */ 14 | export async function addNumbers(a: number, b: number) { 15 | await timeout(500); 16 | return a + b; 17 | } 18 | 19 | //== Run the program ==// 20 | (async () => { 21 | console.log(await addNumbers(3, 4)); 22 | })(); 23 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": ["@typescript-eslint"], 4 | "extends": ["eslint:recommended"], 5 | "parserOptions": { 6 | "ecmaVersion": 9, 7 | "sourceType": "module", 8 | "project": "tsconfig.json" 9 | }, 10 | "env": { 11 | "shared-node-browser": true, 12 | "node": true, 13 | "es6": true 14 | }, 15 | "rules": { 16 | "no-console": "off", 17 | "no-debugger": "off", 18 | "no-unused-vars": "off", 19 | "no-undef": "off", 20 | "no-redeclare": "off", 21 | "prefer-const": "off" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /challenges/json-types/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": ["es6"], 5 | "noImplicitAny": true, 6 | "noImplicitThis": true, 7 | "strictNullChecks": true, 8 | "strictFunctionTypes": true, 9 | "noEmit": true, 10 | "types": [], 11 | 12 | // If the library is an external module (uses `export`), this allows your test file to import "mylib" instead of "./index". 13 | // If the library is global (cannot be imported via `import` or `require`), leave this out. 14 | "baseUrl": "..", 15 | "paths": { "json-types": ["./src"] } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/hello-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-ts", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "> TODO: description", 6 | "author": "Mike North ", 7 | "homepage": "", 8 | "license": "ISC", 9 | "main": "lib/hello-ts.js", 10 | "directories": { 11 | "lib": "lib", 12 | "test": "test" 13 | }, 14 | "files": [ 15 | "lib" 16 | ], 17 | "scripts": { 18 | "clean": "rimraf lib", 19 | "build": "tsc src/index.ts --outDir lib --types node --module commonjs --target ES2017", 20 | "test": "echo 'No tests'" 21 | }, 22 | "devDependencies": {} 23 | } 24 | -------------------------------------------------------------------------------- /challenges/advanced-types/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": ["es6"], 5 | "noImplicitAny": true, 6 | "noImplicitThis": true, 7 | "strictNullChecks": true, 8 | "strictFunctionTypes": true, 9 | "noEmit": true, 10 | "types": [], 11 | 12 | // If the library is an external module (uses `export`), this allows your test file to import "mylib" instead of "./index". 13 | // If the library is global (cannot be imported via `import` or `require`), leave this out. 14 | "baseUrl": "..", 15 | "paths": { "advanced-type-olympics": ["./src"] } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /challenges/advanced-types/test/mix.test.ts: -------------------------------------------------------------------------------- 1 | import { Mix } from "advanced-type-olympics"; 2 | 3 | /** 4 | * Mix should require two template params. 5 | * The following examples should result in TS errors 6 | */ 7 | let a: Mix; // $ExpectError 8 | let b: Mix; // $ExpectError 9 | 10 | // Should be ok (but a bit pointless) 11 | let c: Mix<{}, {}>; 12 | 13 | let d: Mix<{ a: number; b: string }, { a: string; c: number[] }> = { 14 | ...{ 15 | a: 42, 16 | b: "abc" 17 | }, 18 | ...{ a: "abc", c: [1, 2, 3] } 19 | }; 20 | 21 | // Known properties should have the correct types 22 | d.a; // $ExpectType string 23 | d.b; // $ExpectType string 24 | d.c; // $ExpectType number[] 25 | 26 | // Unknown properties should not be present (i.e., no index signature) 27 | d.foo; // $ExpectError 28 | -------------------------------------------------------------------------------- /challenges/advanced-types/test/optional-property-names-of.test.ts: -------------------------------------------------------------------------------- 1 | import { OptionalPropertyNamesOf } from "advanced-type-olympics"; 2 | 3 | /** 4 | * OptionalPropertyNamesOf should require two template params. 5 | * The following examples should result in TS errors 6 | */ 7 | let a: OptionalPropertyNamesOf; // $ExpectError 8 | 9 | // Should be ok (but a bit pointless) 10 | let c: OptionalPropertyNamesOf<{}>; 11 | 12 | type e = OptionalPropertyNamesOf<{ a: number; b?: string }>; // $ExpectType "b" 13 | 14 | // $ExpectType "a" | "b" 15 | type f = OptionalPropertyNamesOf>; 16 | 17 | // $ExpectType never 18 | type g = OptionalPropertyNamesOf<{ a: number | number[]; b: string }>; 19 | 20 | // == BONUS == // 21 | // $ExpectType never 22 | type d = OptionalPropertyNamesOf<{ a: number | undefined; b: string }>; 23 | -------------------------------------------------------------------------------- /challenges/advanced-types/test/required-property-names-of.test.ts: -------------------------------------------------------------------------------- 1 | import { RequiredPropertyNamesOf } from "advanced-type-olympics"; 2 | 3 | /** 4 | * RequiredPropertyNamesOf should require two template params. 5 | * The following examples should result in TS errors 6 | */ 7 | let a: RequiredPropertyNamesOf; // $ExpectError 8 | 9 | // Should be ok (but a bit pointless) 10 | let c: RequiredPropertyNamesOf<{}>; 11 | 12 | type e = RequiredPropertyNamesOf<{ a: number; b?: string }>; // $ExpectType "a" 13 | 14 | // $ExpectType never 15 | type f = RequiredPropertyNamesOf>; 16 | 17 | // $ExpectType "a" | "b" 18 | type g = RequiredPropertyNamesOf<{ a: number | number[]; b: string }>; 19 | 20 | // == BONUS == // 21 | type d = RequiredPropertyNamesOf<{ a: number | undefined; b: string }>; // $ExpectType "a" | "b" 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 10 3 | 4 | cache: 5 | yarn: true 6 | sudo: required 7 | dist: trusty 8 | 9 | stages: 10 | - Tests 11 | - name: Release 12 | if: branch = v2 and type = push 13 | 14 | jobs: 15 | fail_fast: true 16 | include: 17 | - stage: Tests 18 | name: Conventional Commits 19 | node_js: 10 20 | script: 21 | - commitlint-travis 22 | - name: Fixed Dependencies 23 | - name: Floating Dependencies 24 | install: yarn install --no-lockfile --non-interactive 25 | - node_js: 8 26 | - node_js: stable 27 | 28 | - stage: Release 29 | name: Github Release 30 | script: yarn semantic-release 31 | 32 | before_install: 33 | - curl -o- -L https://yarnpkg.com/install.sh | bash 34 | - export PATH=$HOME/.yarn/bin:$PATH 35 | 36 | install: 37 | - yarn install --non-interactive 38 | 39 | script: 40 | - lerna run test --scope=hello-ts 41 | -------------------------------------------------------------------------------- /challenges/advanced-types/test/extract-property-names-assignable-to.test.ts: -------------------------------------------------------------------------------- 1 | import { ExtractPropertyNamesAssignableTo } from "advanced-type-olympics"; 2 | 3 | /** 4 | * ExtractPropertyNamesAssignableTo should require two template params. 5 | * The following examples should result in TS errors 6 | */ 7 | let a: ExtractPropertyNamesAssignableTo; // $ExpectError 8 | let b: ExtractPropertyNamesAssignableTo; // $ExpectError 9 | 10 | // Should be ok (but a bit pointless) 11 | let c: ExtractPropertyNamesAssignableTo<{}, {}>; 12 | 13 | type d = ExtractPropertyNamesAssignableTo<{ a: number; b: string }, string>; // $ExpectType "b" 14 | type e = ExtractPropertyNamesAssignableTo<{ a: number; b: string }, number>; // $ExpectType "a" 15 | // $ExpectType "a" | "b" 16 | type f = ExtractPropertyNamesAssignableTo< 17 | { a?: number | number[]; b?: string }, 18 | undefined 19 | >; 20 | // $ExpectType "a" 21 | type g = ExtractPropertyNamesAssignableTo< 22 | { a?: number | number[]; b?: string }, 23 | number[] 24 | >; 25 | -------------------------------------------------------------------------------- /challenges/json-types/test/json-types.test.ts: -------------------------------------------------------------------------------- 1 | import { JSONValue, JSONObject, JSONArray } from "json-types"; 2 | 3 | function isJSONValue(val: JSONValue) {} 4 | function isJSONArray(val: JSONArray) {} 5 | function isJSONObject(val: JSONObject) {} 6 | 7 | isJSONValue([]); 8 | isJSONValue(4); 9 | isJSONValue("abc"); 10 | isJSONValue(false); 11 | isJSONValue({ hello: ["world"] }); 12 | isJSONValue(() => 3); // $ExpectError 13 | 14 | isJSONArray([]); 15 | isJSONArray(["abc", 4]); 16 | isJSONArray(4); // $ExpectError 17 | isJSONArray("abc"); // $ExpectError 18 | isJSONArray(false); // $ExpectError 19 | isJSONArray({ hello: ["world"] }); // $ExpectError 20 | isJSONArray(() => 3); // $ExpectError 21 | 22 | isJSONObject([]); // $ExpectError 23 | isJSONObject(["abc", 4]); // $ExpectError 24 | isJSONObject(4); // $ExpectError 25 | isJSONObject("abc"); // $ExpectError 26 | isJSONObject(false); // $ExpectError 27 | isJSONObject({ hello: ["world"] }); 28 | isJSONObject({ 3: ["hello, world"] }); 29 | isJSONObject(() => 3); // $ExpectError 30 | -------------------------------------------------------------------------------- /exerciseLICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, Mike Works, Inc. All rights reserved. 2 | 3 | This training material (all exercises and accompanying tests) is licensed to 4 | the individual who purchased it. We don't copy-protect it because that would 5 | limit your ability to use it for your own purposes. Please don't break this 6 | trust - don't allow others to use these exercises without purchasing their own 7 | license. Thanks. 8 | 9 | Redistribution and use in source and binary forms, with or without 10 | modification, are not permitted provided without an individual license. It may 11 | not be used to create training material, courses, books, articles, and the like. 12 | Contact us if you are in doubt. We make no guarantees that this code is 13 | fit for any purpose. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 19 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [2.0.0](https://github.com/mike-works/typescript-fundamentals/compare/v1.0.3...v2.0.0) (2019-03-08) 2 | 3 | ### Bug Fixes 4 | 5 | - align sequence of notes with order of slides ([313d994](https://github.com/mike-works/typescript-fundamentals/commit/313d994)) 6 | - move "interfaces vs types" back to the appropriate section" ([ba80c45](https://github.com/mike-works/typescript-fundamentals/commit/ba80c45)) 7 | - rename workspace ([646951a](https://github.com/mike-works/typescript-fundamentals/commit/646951a)) 8 | - rename workspace ([8ce8966](https://github.com/mike-works/typescript-fundamentals/commit/8ce8966)) 9 | - tests should fail in starting point code ([cfece55](https://github.com/mike-works/typescript-fundamentals/commit/cfece55)) 10 | - tests should fail in starting point code ([454ce39](https://github.com/mike-works/typescript-fundamentals/commit/454ce39)) 11 | - update installation instructions ([1706f7f](https://github.com/mike-works/typescript-fundamentals/commit/1706f7f)) 12 | - update slides link ([10ca9c2](https://github.com/mike-works/typescript-fundamentals/commit/10ca9c2)) 13 | 14 | ### Features 15 | 16 | - clean slate ([514d236](https://github.com/mike-works/typescript-fundamentals/commit/514d236)) 17 | - v2 of workshop ([27f7df3](https://github.com/mike-works/typescript-fundamentals/commit/27f7df3)) 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Something broke while you were progressing through the workshop 4 | 5 | --- 6 | 7 | 8 | 11 | 12 | - [ ] **System Information** 13 | - [ ] Browser type and version 14 | - [ ] OS type and version 15 | - [ ] WINDOWS: be sure to indicate which terminal you're using -- (i.e., cmd.exe, powershell, git- bash, cygwin, Ubuntu via windows subsystem for linux, etc...) 16 | - [ ] Node version 17 | - [ ] Any error messages that may be in the console where you ran npm start 18 | - [ ] Any error messages in the JS console 19 | 20 | - [ ] **Describe the bug** 21 | 22 | 23 | - [ ] **To Reproduce** 24 | Steps to reproduce the behavior: 25 | 1. Go to '...' 26 | 2. Click on '....' 27 | 3. Scroll down to '....' 28 | 4. See error 29 | 30 | - [ ] **Expected behavior** 31 | A clear and concise description of what you expected to happen. 32 | 33 | - [ ] **Screenshots (optional)** 34 | If applicable, add screenshots to help explain your problem. 35 | 36 | - [ ] **Additional context (optional)** 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Mike Works, Inc. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mike-works/typescript-fundamentals", 3 | "version": "2.0.0", 4 | "description": "Mike.Works typescript workshop", 5 | "repository": "https://github.com/mike-works/typescript-fundamentals", 6 | "author": "Mike North (https://mike.works)", 7 | "private": true, 8 | "devDependencies": { 9 | "@commitlint/cli": "8.3.5", 10 | "@commitlint/config-conventional": "8.3.4", 11 | "@commitlint/travis-cli": "8.3.5", 12 | "@mike-works/js-lib-renovate-config": "2.0.0", 13 | "@mike-works/workshop-semantic-release-config": "1.0.0", 14 | "@types/chai": "4.2.22", 15 | "@types/mocha": "5.2.7", 16 | "@types/node": "12.20.16", 17 | "@typescript-eslint/eslint-plugin": "2.34.0", 18 | "@typescript-eslint/parser": "2.34.0", 19 | "chai": "4.3.4", 20 | "dtslint": "0.9.9", 21 | "eslint": "6.8.0", 22 | "husky": "3.1.0", 23 | "lerna": "3.22.1", 24 | "mocha": "6.2.3", 25 | "rimraf": "3.0.2", 26 | "semantic-release": "15.14.0", 27 | "source-map-support": "0.5.19", 28 | "ts-node": "8.10.2", 29 | "typescript": "3.7.7" 30 | }, 31 | "workspaces": [ 32 | "examples/*", 33 | "challenges/*", 34 | "notes" 35 | ], 36 | "scripts": { 37 | "clean": "lerna run clean && lerna clean --yes && lerna bootstrap", 38 | "build": "lerna run build", 39 | "test": "lerna run test", 40 | "postinstall": "lerna link", 41 | "semantic-release": "semantic-release" 42 | }, 43 | "release": { 44 | "extends": "@mike-works/workshop-semantic-release-config", 45 | "branch": "v2" 46 | }, 47 | "commitlint": { 48 | "extends": [ 49 | "@commitlint/config-conventional" 50 | ] 51 | }, 52 | "husky": { 53 | "hooks": { 54 | "commit-msg": "./node_modules/.bin/commitlint -e $HUSKY_GIT_PARAMS" 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /notes/9-compiler-api.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as ts from "typescript"; 3 | 4 | function isDefined(x: T | undefined): x is T { 5 | return typeof x !== "undefined"; 6 | } 7 | 8 | // (1) Create the program 9 | const program = ts.createProgram({ 10 | options: { 11 | target: ts.ScriptTarget.ESNext 12 | }, 13 | rootNames: [ 14 | // path to ../examples/hello-ts/src/index.ts 15 | path.join(__dirname, "..", "examples", "hello-ts", "src", "index.ts") 16 | ] 17 | }); 18 | 19 | // // (2) Get the non-declaration (.d.ts) source files (.ts) 20 | // const nonDeclFiles = program 21 | // .getSourceFiles() 22 | // .filter(sf => !sf.isDeclarationFile); 23 | 24 | // // (3) get the type-checker 25 | // const checker = program.getTypeChecker(); 26 | 27 | // /** 28 | // * (4) use the type checker to obtain the 29 | // * - appropriate ts.Symbol for each SourceFile 30 | // */ 31 | // const sfSymbols = nonDeclFiles 32 | // .map(f => checker.getSymbolAtLocation(f)) 33 | // .filter(isDefined); // here's the type guard to filter out undefined 34 | 35 | // // (5) for each SourceFile Symbol 36 | // sfSymbols.forEach(sfSymbol => { 37 | // const { exports: fileExports } = sfSymbol; 38 | // console.log(sfSymbol.name); 39 | // if (fileExports) { 40 | // // - if there are exports 41 | // console.log("== Exports =="); 42 | // fileExports.forEach((value, key) => { 43 | // // - for each export 44 | // console.log( 45 | // key, // - log its name 46 | 47 | // // - and its type (stringified) 48 | // checker.typeToString(checker.getTypeAtLocation(value.valueDeclaration)) 49 | // ); 50 | // const jsDocTags = value.getJsDocTags(); 51 | // if (jsDocTags.length > 0) { 52 | // // - if there are JSDoc comment tags 53 | // console.log( 54 | // // - log them out as key-value pairs 55 | // jsDocTags.map(tag => `\t${tag.name}: ${tag.text}`).join("\n") 56 | // ); 57 | // } 58 | // }); 59 | // } 60 | // }); 61 | -------------------------------------------------------------------------------- /ts.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": { 8 | "eslint.enable": true, 9 | "eslint.validate": ["javascript", "typescript"], 10 | "eslint.packageManager": "yarn", 11 | "eslint.autoFixOnSave": true, 12 | "typescript.tsdk": "node_modules/typescript/lib", 13 | "files.exclude": { 14 | "**/.git": true, 15 | "**/.DS_Store": true, 16 | "yarn.lock": true 17 | }, 18 | "search.exclude": { 19 | "**/node_modules": true, 20 | "**/lib": true 21 | }, 22 | "files.watcherExclude": { 23 | "**/.git/objects/**": true, 24 | "**/.git/subtree-cache/**": true, 25 | "**/node_modules/**": true, 26 | "**/examples/*/lib/**": true, 27 | "**/challenges/*/lib/**": true 28 | }, 29 | "better-comments.tags": [ 30 | { 31 | "tag": "🚨", 32 | "color": "#FF0000", 33 | "strikethrough": false 34 | }, 35 | { 36 | "tag": "✅", 37 | "color": "#00FF00", 38 | "strikethrough": false 39 | }, 40 | { 41 | "tag": "💡", 42 | "color": "#FFFF00", 43 | "strikethrough": false 44 | }, 45 | { 46 | "tag": "‣", 47 | "color": "#00FFFF", 48 | "backgroundColor": "#333333", 49 | "strikethrough": false 50 | }, 51 | { 52 | "tag": "(", 53 | "color": "#FFFF00", 54 | "strikethrough": false 55 | }, 56 | { 57 | "tag": "-", 58 | "color": "#FFFF00", 59 | "strikethrough": false 60 | }, 61 | { 62 | "tag": "==", 63 | "backgroundColor": "#FFFFFF", 64 | "color": "#000000", 65 | "strikethrough": false 66 | } 67 | ], 68 | "code-runner.executorMap": { 69 | "javascript": "node", 70 | "typescript": "node_modules/.bin/ts-node" 71 | } 72 | }, 73 | "extensions": { 74 | "recommendations": [ 75 | "aaron-bond.better-comments", 76 | "dbaeumer.vscode-eslint", 77 | "formulahendry.code-runner" 78 | ] 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /challenges/advanced-types/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * - Mix: Provide a type consistent with the result of `Object.assign(A, B)` 3 | * @template A first object type 4 | * @template B second object type 5 | * 6 | * @example 7 | * 8 | * type Example = Mix< 9 | * { a: number; b: string }, 10 | * { a: string; c: number[] } 11 | * >; 12 | * // results in 13 | * { 14 | * a: string; // second object wins in event of collision 15 | * b: string; 16 | * c: number; 17 | * } 18 | */ 19 | 20 | type ProblemMerge = { a: number; b: string } & { a: string; c: number[] }; 21 | 22 | console.log( 23 | Object.assign({ a: 44, b: "hello" }, { a: "from second object", c: 99 }) 24 | ); 25 | 26 | // IMPLEMENT ME 27 | export type Mix = never; 28 | 29 | /** 30 | * - ExtractPropertyNamesAssignableTo: obtain the names of properties assignable to a type 31 | * @template T object type to operate on 32 | * @template S type to check property values against 33 | * 34 | * @example 35 | * type Example = ExtractPropertyNamesAssignableTo< 36 | * { 37 | * a(): Promise; 38 | * b: PromiseLike; 39 | * c(): number; 40 | * d: Array>; 41 | * }, (...args: any[]) => any 42 | * >; 43 | * // results in 44 | * "a" | "c" 45 | */ 46 | interface Foo { 47 | x: string; 48 | y: number; 49 | } 50 | 51 | // IMPLEMENT ME 52 | export type ExtractPropertyNamesAssignableTo = never; 53 | 54 | type X = ExtractPropertyNamesAssignableTo< 55 | Window, 56 | (a: Function, b: number) => any 57 | >; 58 | 59 | /** 60 | * - OptionalPropertyNamesOf: Extract the property names of an object type that are optional 61 | * 62 | * @template T object type to extract optional property names from 63 | * 64 | * @example 65 | * 66 | * const x: OptionalPropertyNamesOf<{ a: string; b?: number }>; 67 | * // results in 68 | * 'b' 69 | * 70 | */ 71 | 72 | // IMPLEMENT ME 73 | export type OptionalPropertyNamesOf = never; 74 | 75 | /** 76 | * - RequiredPropertyNamesOf: Extract the property names of an object type that are required 77 | * 78 | * @template T object type to extract required property names from 79 | * 80 | * @example 81 | * 82 | * const y: RequiredPropertyNamesOf<{ a: string; b?: number }>; 83 | * // results in 84 | * 'a' 85 | */ 86 | 87 | // IMPLEMENT ME 88 | export type RequiredPropertyNamesOf = never; 89 | -------------------------------------------------------------------------------- /challenges/address-book/src/index.js: -------------------------------------------------------------------------------- 1 | export class AddressBook { 2 | contacts = []; 3 | 4 | addContact(contact) { 5 | this.contacts.push(contact); 6 | } 7 | 8 | findContactByName(filter) { 9 | return this.contacts.filter(c => { 10 | if ( 11 | typeof filter.firstName !== "undefined" && 12 | c.firstName !== filter.firstName 13 | ) { 14 | return false; 15 | } 16 | if ( 17 | typeof filter.lastName !== "undefined" && 18 | c.lastName !== filter.lastName 19 | ) { 20 | return false; 21 | } 22 | return true; 23 | }); 24 | } 25 | } 26 | 27 | export function formatDate(date) { 28 | return ( 29 | date 30 | .toISOString() 31 | .replace(/[-:]+/g, "") 32 | .split(".")[0] + "Z" 33 | ); 34 | } 35 | 36 | function getFullName(contact) { 37 | return [contact.firstName, contact.middleName, contact.lastName] 38 | .filter(Boolean) 39 | .join(" "); 40 | } 41 | 42 | export function getVcardText(contact, date = new Date()) { 43 | const parts = [ 44 | "BEGIN:VCARD", 45 | "VERSION:2.1", 46 | `N:${contact.lastName};${contact.firstName};${contact.middleName || 47 | ""};${contact.salutation || ""}`, 48 | `FN:${getFullName(contact)}`, 49 | ...Object.keys(contact.phones).map( 50 | phName => `TEL;${phName.toUpperCase()};VOICE:${contact.phones[phName]}` 51 | ), 52 | ...Object.keys(contact.addresses) 53 | .map(addrName => { 54 | const address = contact.addresses[addrName]; 55 | if (address) { 56 | return `ADR;${addrName.toUpperCase()}:;;${address.houseNumber} ${ 57 | address.street 58 | };${address.city};${address.state};${address.postalCode};${ 59 | address.country 60 | }\nLABEL;${addrName.toUpperCase()};ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:${ 61 | address.houseNumber 62 | } ${address.street}.=0D=0A=${address.city}, ${address.state} ${ 63 | address.postalCode 64 | }=0D=0A${address.country}`; 65 | } else { 66 | return ""; 67 | } 68 | }) 69 | .filter(Boolean) 70 | ]; 71 | 72 | if (contact.email) { 73 | parts.push(`EMAIL:${contact.email}`); 74 | } 75 | const d = new Date(); 76 | parts.push(`REV:${formatDate(date)}`); 77 | parts.push("END:VCARD"); 78 | return parts.join("\n"); 79 | } 80 | -------------------------------------------------------------------------------- /notes/4-class-basics.ts: -------------------------------------------------------------------------------- 1 | import { HasPhoneNumber, HasEmail } from "./1-basics"; 2 | 3 | // == CLASSES == // 4 | 5 | /** 6 | * (1) Classes work similarly to what you're used to seeing in JS 7 | * - They can "implement" interfaces 8 | */ 9 | 10 | // export class Contact implements HasEmail { 11 | // email: string; 12 | // name: string; 13 | // constructor(name: string, email: string) { 14 | // this.email = email; 15 | // this.name = name; 16 | // } 17 | // } 18 | 19 | /** 20 | * (2) This looks a little verbose -- we have to specify the words "name" and "email" 3x. 21 | * - Typescript has a shortcut: PARAMETER PROPERTIES 22 | */ 23 | 24 | /** 25 | * (3) Access modifier keywords - "who can access this thing" 26 | * 27 | * - public - everyone 28 | * - protected - me and subclasses 29 | * - private - only me 30 | */ 31 | 32 | // class ParamPropContact implements HasEmail { 33 | // constructor(public name: string, public email: string = "no email") { 34 | // // nothing needed 35 | // } 36 | // } 37 | 38 | /** 39 | * (4) Class fields can have initializers (defaults) 40 | */ 41 | // class OtherContact implements HasEmail, HasPhoneNumber { 42 | // protected age: number = 0; 43 | // // private password: string; 44 | // constructor(public name: string, public email: string, public phone: number) { 45 | // // () password must either be initialized like this, or have a default value 46 | // // this.password = Math.round(Math.random() * 1e14).toString(32); 47 | // } 48 | // } 49 | 50 | /** 51 | * (5) TypeScript even allows for abstract classes, which have a partial implementation 52 | */ 53 | 54 | // abstract class AbstractContact implements HasEmail, HasPhoneNumber { 55 | // public abstract phone: number; // must be implemented by non-abstract subclasses 56 | 57 | // constructor( 58 | // public name: string, 59 | // public email: string // must be public to satisfy HasEmail 60 | // ) {} 61 | 62 | // abstract sendEmail(): void; // must be implemented by non-abstract subclasses 63 | // } 64 | 65 | /** 66 | * (6) implementors must "fill in" any abstract methods or properties 67 | */ 68 | // class ConcreteContact extends AbstractContact { 69 | // constructor( 70 | // public phone: number, // must happen before non property-parameter arguments 71 | // name: string, 72 | // email: string 73 | // ) { 74 | // super(name, email); 75 | // } 76 | // sendEmail() { 77 | // // mandatory! 78 | // console.log("sending an email"); 79 | // } 80 | // } 81 | -------------------------------------------------------------------------------- /notes/8-declaration-merging.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * (1) "identifiers" (i.e., a variable, class, function, interface) 3 | * - can be associated with three things: value, type and namespace 4 | */ 5 | 6 | // function foo() {} 7 | // interface bar {} 8 | // namespace baz { 9 | // export const biz = "hello"; 10 | // } 11 | 12 | // // how to test for a value 13 | // const x = foo; // foo is in the value position (RHS). 14 | 15 | // // how to test for a type 16 | // const y: bar = {}; // bar is in the type position (LHS). 17 | 18 | // // how to test for a namespace (hover over baz symbol) 19 | // baz; 20 | 21 | // export { foo, bar, baz }; // all are importable/exportable 22 | 23 | /** 24 | * (2) Functions and variables are purely values. 25 | * - Their types may only be extracted using type queries 26 | */ 27 | // const xx = 4; 28 | // const yy: typeof xx = 4; 29 | 30 | /** 31 | * (3) Interfaces are purely types 32 | */ 33 | // interface Address { 34 | // street: string; 35 | // } 36 | 37 | // const z = Address; // 🚨 ERROR (fails value test) 38 | 39 | /** 40 | * (4) Classes are both types _and_ values 41 | */ 42 | 43 | // class Contact { 44 | // name: string; 45 | // } 46 | 47 | // // passes both the value and type tests 48 | 49 | // const contactClass = Contact; // value relates to the factory for creating instances 50 | // const contactInstance: Contact = new Contact(); // interface relates to instances 51 | 52 | /** 53 | * (5) declarations with the same name can be merged, to occupy the same identifier 54 | */ 55 | 56 | // class Album { 57 | // label: Album.AlbumLabel = new Album.AlbumLabel(); 58 | // } 59 | // namespace Album { 60 | // export class AlbumLabel {} 61 | // } 62 | // interface Album { 63 | // artist: string; 64 | // } 65 | 66 | // let al: Album; // type test 67 | // let alValue = Album; // value test 68 | 69 | // export { Album }; // 👈 hover over the "Album" -- all three slots filled 70 | 71 | /** 72 | * (6) Namespaces have their own slot, and are also values 73 | */ 74 | 75 | // // 💡 they can be merged with classes 76 | 77 | // class AddressBook { 78 | // contacts!: Contact[]; 79 | // } 80 | // namespace AddressBook { 81 | // export class ABContact extends Contact {} // inner class 82 | // } 83 | 84 | // const ab = new AddressBook(); 85 | // ab.contacts.push(new AddressBook.ABContact()); 86 | 87 | // // 💡 or functions 88 | 89 | // function format(amt: number) { 90 | // return `${format.currency}${amt.toFixed(2)}`; 91 | // } 92 | // namespace format { 93 | // export const currency: string = "$ "; 94 | // } 95 | 96 | // format(2.314); // $ 2.31 97 | // format.currency; // $ 98 | -------------------------------------------------------------------------------- /notes/7-advanced-types.ts: -------------------------------------------------------------------------------- 1 | import { HasEmail, HasPhoneNumber } from "./1-basics"; 2 | 3 | /** 4 | * (1) MAPPED TYPES allow the use of an interface to transform keys into values 5 | */ 6 | 7 | // interface CommunicationMethods { 8 | // email: HasEmail; 9 | // phone: HasPhoneNumber; 10 | // fax: { fax: number }; 11 | // } 12 | 13 | // function contact( 14 | // method: K, 15 | // contact: CommunicationMethods[K] // 💡turning key into value -- a *mapped type* 16 | // ) { 17 | // //... 18 | // } 19 | // contact("email", { name: "foo", email: "mike@example.com" }); 20 | // contact("phone", { name: "foo", phone: 3213332222 }); 21 | // contact("fax", { fax: 1231 }); 22 | 23 | // // we can get all values by mapping through all keys 24 | // type AllCommKeys = keyof CommunicationMethods; 25 | // type AllCommValues = CommunicationMethods[keyof CommunicationMethods]; 26 | 27 | /** 28 | * (2) Type queries allow us to obtain the type from a value using typeof 29 | */ 30 | 31 | // const alreadyResolvedNum = Promise.resolve(4); 32 | 33 | // type ResolveType = typeof Promise.resolve; 34 | 35 | // const x: ResolveType = Promise.resolve; 36 | // x(42).then(y => y.toPrecision(2)); 37 | 38 | /** 39 | * (3) Conditional types allow for the use of a ternary operator w/ types 40 | * We can also extract type parameters using the _infer_ keyword 41 | */ 42 | 43 | // type EventualType = T extends Promise // if T extends Promise 44 | // ? S // extract the type the promise resolves to 45 | // : T; // otherwise just let T pass through 46 | 47 | // let a: EventualType>; 48 | // let b: EventualType; 49 | 50 | //== Built-in Utility Types ==// 51 | 52 | /** 53 | * (4) Partial allows us to make all properties on an object optional 54 | */ 55 | // type MayHaveEmail = Partial; 56 | // const me: MayHaveEmail = {}; // everything is optional 57 | 58 | /** 59 | * (5) Pick allows us to select one or more properties from an object type 60 | */ 61 | 62 | // type HasThen = Pick, "then" | "catch">; 63 | 64 | // let hasThen: HasThen = Promise.resolve(4); 65 | // hasThen.then; 66 | 67 | /** 68 | * (6) Extract lets us obtain a subset of types that are assignable to something 69 | */ 70 | 71 | // type OnlyStrings = Extract<"a" | "b" | 1 | 2, number>; 72 | 73 | /** 74 | * (7) Exclude lets us obtain a subset of types that are NOT assignable to something 75 | */ 76 | // type NotStrings = Exclude<"a" | "b" | 1 | 2, string>; 77 | 78 | /** 79 | * (8) Record helps us create a type with specified property keys and the same value type 80 | */ 81 | // type ABCPromises = Record<"a" | "b" | "c", Promise>; 82 | -------------------------------------------------------------------------------- /challenges/dict/test/dict.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from "mocha"; 2 | import { expect } from "chai"; 3 | import { Dict } from "../src/index"; 4 | import * as indexExports from "../src/index"; 5 | 6 | describe("index.ts module", () => { 7 | it("should have a Dict export (type or interface)", () => { 8 | expect((indexExports as any).Dict).to.eq(undefined); 9 | const x: Dict = {}; 10 | }); 11 | it("should have a mapDict export (function)", () => { 12 | expect(!!indexExports.mapDict).to.eq(true, "export exists"); 13 | expect(typeof indexExports.mapDict).to.eq("function", "it is a function"); 14 | }); 15 | 16 | it("should have a reduceDict export (function)", () => { 17 | expect(!!indexExports.reduceDict).to.eq(true, "export exists"); 18 | expect(typeof indexExports.reduceDict).to.eq( 19 | "function", 20 | "it is a function" 21 | ); 22 | }); 23 | }); 24 | 25 | describe("Dict type", () => { 26 | it("(compile) should be able to hold a dictionary of string[]", () => { 27 | const x: Dict = { 28 | abc: ["def"], 29 | ghi: ["jkl", "mno"] 30 | }; 31 | }); 32 | it("(compile) should have a default typeParam", () => { 33 | const x: Dict = { 34 | abc: ["def"], 35 | ghi: ["jkl", "mno"] 36 | }; 37 | }); 38 | it("(compile) should have a default typeParam of `any`", () => { 39 | const x: Dict = { 40 | abc: "def", 41 | ghi: ["jkl", "mno"], 42 | pqr: 4, 43 | stu: () => false 44 | }; 45 | }); 46 | }); 47 | 48 | describe("mapDict", () => { 49 | it("should receive two arguments", () => { 50 | expect(indexExports.mapDict.length).to.eq(2); 51 | }); 52 | it("should return a dictionary", () => { 53 | expect(indexExports.mapDict({ abc: 4 }, x => x)).to.deep.eq({ abc: 4 }); 54 | }); 55 | it("should use the second argument (fn) to transform each value", () => { 56 | expect(indexExports.mapDict({ abc: 4 }, x => `${x}`)).to.deep.eq({ 57 | abc: "4" 58 | }); 59 | }); 60 | it("should not invoke the transform function for undefined values", () => { 61 | let invokeCount = 0; 62 | expect( 63 | indexExports.mapDict({ abc: 4, def: undefined }, x => { 64 | invokeCount++; 65 | 66 | return `${x}`; 67 | }) 68 | ).to.deep.include({ 69 | abc: "4" 70 | }); 71 | expect(invokeCount).to.eq(1); 72 | }); 73 | }); 74 | 75 | describe("reduceDict", () => { 76 | it("should receive three arguments", () => { 77 | expect(indexExports.reduceDict.length).to.eq(3); 78 | }); 79 | it("should use the second argument (fn) as a reducera, and third argument as initial value", () => { 80 | expect( 81 | indexExports.reduceDict( 82 | { abc: 4, def: 3, ghi: 19 }, 83 | (x, sum) => sum + x, 84 | 0 85 | ) 86 | ).to.eq(26); 87 | }); 88 | it("should not invoke the reducer function for undefined values", () => { 89 | let invokeCount = 0; 90 | expect( 91 | indexExports.reduceDict( 92 | { abc: 4, def: undefined }, 93 | (x, acc) => { 94 | invokeCount++; 95 | return `${acc}, ${x}`; 96 | }, 97 | "" 98 | ) 99 | ).to.eq(", 4"); 100 | expect(invokeCount).to.eq(1); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /notes/2-function-basics.ts: -------------------------------------------------------------------------------- 1 | import { HasEmail, HasPhoneNumber } from "./1-basics"; 2 | 3 | //== FUNCTIONS ==// 4 | 5 | // (1) function arguments and return values can have type annotations 6 | // function sendEmail(to: HasEmail): { recipient: string; body: string } { 7 | // return { 8 | // recipient: `${to.name} <${to.email}>`, // Mike 9 | // body: "You're pre-qualified for a loan!" 10 | // }; 11 | // } 12 | 13 | // (2) or the arrow-function variant 14 | // const sendTextMessage = ( 15 | // to: HasPhoneNumber 16 | // ): { recipient: string; body: string } => { 17 | // return { 18 | // recipient: `${to.name} <${to.phone}>`, 19 | // body: "You're pre-qualified for a loan!" 20 | // }; 21 | // }; 22 | 23 | // (3) return types can almost always be inferred 24 | // function getNameParts(contact: { name: string }) { 25 | // const parts = contact.name.split(/\s/g); // split @ whitespace 26 | // if (parts.length < 2) { 27 | // throw new Error(`Can't calculate name parts from name "${contact.name}"`); 28 | // } 29 | // return { 30 | // first: parts[0], 31 | // middle: 32 | // parts.length === 2 33 | // ? undefined 34 | // : // everything except first and last 35 | // parts.slice(1, parts.length - 2).join(" "), 36 | // last: parts[parts.length - 1] 37 | // }; 38 | // } 39 | 40 | // (4) rest params work just as you'd think. Type must be array-ish 41 | // const sum = (...vals: number[]) => vals.reduce((sum, x) => sum + x, 0); 42 | // console.log(sum(3, 4, 6)); // 13 43 | 44 | // (5) we can even provide multiple function signatures 45 | // "overload signatures" 46 | // function contactPeople(method: "email", ...people: HasEmail[]): void; 47 | // function contactPeople(method: "phone", ...people: HasPhoneNumber[]): void; 48 | 49 | // "function implementation" 50 | // function contactPeople( 51 | // method: "email" | "phone", 52 | // ...people: (HasEmail | HasPhoneNumber)[] 53 | // ): void { 54 | // if (method === "email") { 55 | // (people as HasEmail[]).forEach(sendEmail); 56 | // } else { 57 | // (people as HasPhoneNumber[]).forEach(sendTextMessage); 58 | // } 59 | // } 60 | 61 | // ✅ email works 62 | // contactPeople("email", { name: "foo", email: "" }); 63 | 64 | // ✅ phone works 65 | // contactPeople("phone", { name: "foo", phone: 12345678 }); 66 | 67 | // 🚨 mixing does not work 68 | // contactPeople("email", { name: "foo", phone: 12345678 }); 69 | 70 | // (6) the lexical scope (this) of a function is part of its signature 71 | 72 | // function sendMessage( 73 | // this: HasEmail & HasPhoneNumber, 74 | // preferredMethod: "phone" | "email" 75 | // ) { 76 | // if (preferredMethod === "email") { 77 | // console.log("sendEmail"); 78 | // sendEmail(this); 79 | // } else { 80 | // console.log("sendTextMessage"); 81 | // sendTextMessage(this); 82 | // } 83 | // } 84 | // const c = { name: "Mike", phone: 3215551212, email: "mike@example.com" }; 85 | 86 | // function invokeSoon(cb: () => any, timeout: number) { 87 | // setTimeout(() => cb.call(null), timeout); 88 | // } 89 | 90 | // 🚨 this is not satisfied 91 | // invokeSoon(() => sendMessage("email"), 500); 92 | 93 | // ✅ creating a bound function is one solution 94 | // const bound = sendMessage.bind(c, "email"); 95 | // invokeSoon(() => bound(), 500); 96 | 97 | // ✅ call/apply works as well 98 | // invokeSoon(() => sendMessage.apply(c, ["phone"]), 500); 99 | 100 | export default {}; 101 | -------------------------------------------------------------------------------- /notes/3-interface-type-basics.ts: -------------------------------------------------------------------------------- 1 | import { HasPhoneNumber, HasEmail } from "./1-basics"; 2 | 3 | //== TYPE ALIAS ==// 4 | /** 5 | * (1) Type aliases allow us to give a type a name 6 | */ 7 | // type StringOrNumber = string | number; 8 | 9 | // // this is the ONLY time you'll see a type on the RHS of assignment 10 | // type HasName = { name: string }; 11 | 12 | // NEW in TS 3.7: Self-referencing types! 13 | type NumVal = 1 | 2 | 3 | NumVal[]; 14 | 15 | // == INTERFACE == // 16 | /** 17 | * (2) Interfaces can extend from other interfaces 18 | */ 19 | 20 | // export interface HasInternationalPhoneNumber extends HasPhoneNumber { 21 | // countryCode: string; 22 | // } 23 | 24 | /** 25 | * (3) they can also be used to describe call signatures 26 | */ 27 | 28 | // interface ContactMessenger1 { 29 | // (contact: HasEmail | HasPhoneNumber, message: string): void; 30 | // } 31 | 32 | // type ContactMessenger2 = ( 33 | // contact: HasEmail | HasPhoneNumber, 34 | // message: string 35 | // ) => void; 36 | 37 | // // NOTE: we don't need type annotations for contact or message 38 | // const emailer: ContactMessenger1 = (_contact, _message) => { 39 | // /** ... */ 40 | // }; 41 | 42 | /** 43 | * (4) construct signatures can be described as well 44 | */ 45 | 46 | // interface ContactConstructor { 47 | // new (...args: any[]): HasEmail | HasPhoneNumber; 48 | // } 49 | 50 | /** 51 | * (5) index signatures describe how a type will respond to property access 52 | */ 53 | 54 | /** 55 | * @example 56 | * { 57 | * iPhone: { areaCode: 123, num: 4567890 }, 58 | * home: { areaCode: 123, num: 8904567 }, 59 | * } 60 | */ 61 | 62 | // interface PhoneNumberDict { 63 | // // arr[0], foo['myProp'] 64 | // [numberName: string]: 65 | // | undefined 66 | // | { 67 | // areaCode: number; 68 | // num: number; 69 | // }; 70 | // } 71 | 72 | // const phoneDict: PhoneNumberDict = { 73 | // office: { areaCode: 321, num: 5551212 }, 74 | // home: { areaCode: 321, num: 5550010 } // try editing me 75 | // }; 76 | 77 | // at most, a type may have one string and one number index signature 78 | 79 | /** 80 | * (6) they may be used in combination with other types 81 | */ 82 | 83 | // // augment the existing PhoneNumberDict 84 | // // i.e., imported it from a library, adding stuff to it 85 | // interface PhoneNumberDict { 86 | // home: { 87 | // /** 88 | // * (7) interfaces are "open", meaning any declarations of the 89 | // * - same name are merged 90 | // */ 91 | // areaCode: number; 92 | // num: number; 93 | // }; 94 | // office: { 95 | // areaCode: number; 96 | // num: number; 97 | // }; 98 | // } 99 | 100 | // phoneDict.home; // definitely present 101 | // phoneDict.office; // definitely present 102 | // phoneDict.mobile; // MAYBE present 103 | 104 | // == TYPE ALIASES vs INTERFACES == // 105 | 106 | /** 107 | * (7) Type aliases are initialized synchronously, but 108 | * - can reference themselves 109 | */ 110 | 111 | // type NumberVal = 1 | 2 | 3 | NumberVal[]; 112 | 113 | /** 114 | * (8) Interfaces are initialized lazily, so combining it 115 | * - w/ a type alias allows for recursive types! 116 | */ 117 | 118 | // type StringVal = "a" | "b" | "c" | StringArr; 119 | 120 | // // type StringArr = StringVal[]; 121 | // interface StringArr { 122 | // // arr[0] 123 | // [k: number]: "a" | "b" | "c" | StringVal[]; 124 | // } 125 | 126 | // const x: StringVal = Math.random() > 0.5 ? "b" : ["a"]; // ✅ ok! 127 | 128 | export default {}; 129 | -------------------------------------------------------------------------------- /notes/5-generics-basics.ts: -------------------------------------------------------------------------------- 1 | import { HasEmail } from "./1-basics"; 2 | 3 | /** 4 | * (1) Generics allow us to parameterize types in the same way that 5 | * - functions parameterize values 6 | */ 7 | 8 | // // param determines the value of x 9 | // function wrappedValue(x: any) { 10 | // return { 11 | // value: x 12 | // }; 13 | // } 14 | 15 | // // type param determines the type of x 16 | // interface WrappedValue { 17 | // value: X; 18 | // } 19 | 20 | // let val: WrappedValue = { value: [] }; 21 | // val.value; 22 | 23 | /** 24 | * we can name these params whatever we want, but a common convention 25 | * is to use capital letters starting with `T` (a C++ convention from "templates") 26 | */ 27 | 28 | /** 29 | * (2) Type parameters can have default types 30 | * - just like function parameters can have default values 31 | */ 32 | 33 | // // for Array.prototype.filter 34 | // interface FilterFunction { 35 | // (val: T): boolean; 36 | // } 37 | 38 | // const stringFilter: FilterFunction = val => typeof val === "string"; 39 | // stringFilter(0); // 🚨 ERROR 40 | // stringFilter("abc"); // ✅ OK 41 | 42 | // // can be used with any value 43 | // const truthyFilter: FilterFunction = val => val; 44 | // truthyFilter(0); // false 45 | // truthyFilter(1); // true 46 | // truthyFilter(""); // false 47 | // truthyFilter(["abc"]); // true 48 | 49 | /** 50 | * (3) You don't have to use exactly your type parameter as an arg 51 | * - things that are based on your type parameter are fine too 52 | */ 53 | 54 | // function resolveOrTimeout(promise: Promise, timeout: number): Promise { 55 | // return new Promise((resolve, reject) => { 56 | // // start the timeout, reject when it triggers 57 | // const task = setTimeout(() => reject("time up!"), timeout); 58 | 59 | // promise.then(val => { 60 | // // cancel the timeout 61 | // clearTimeout(task); 62 | 63 | // // resolve with the value 64 | // resolve(val); 65 | // }); 66 | // }); 67 | // } 68 | // resolveOrTimeout(fetch(""), 3000); 69 | 70 | /** 71 | * (4) Type parameters can have constraints 72 | */ 73 | 74 | // function arrayToDict(array: T[]): { [k: string]: T } { 75 | // const out: { [k: string]: T } = {}; 76 | // array.forEach(val => { 77 | // out[val.id] = val; 78 | // }); 79 | // return out; 80 | // } 81 | 82 | // const myDict = arrayToDict([ 83 | // { id: "a", value: "first", lisa: "Huang" }, 84 | // { id: "b", value: "second" } 85 | // ]); 86 | 87 | /** 88 | * (5) Type parameters are associated with scopes, just like function arguments 89 | */ 90 | 91 | // function startTuple(a: T) { 92 | // return function finishTuple(b: U) { 93 | // return [a, b] as [T, U]; 94 | // }; 95 | // } 96 | // const myTuple = startTuple(["first"])(42); 97 | 98 | /** 99 | * (6) When to use generics 100 | * 101 | * - Generics are necessary when we want to describe a relationship between 102 | * - two or more types (i.e., a function argument and return type). 103 | * 104 | * - aside from interfaces and type aliases, If a type parameter is used only once 105 | * - it can probably be eliminated 106 | */ 107 | 108 | // interface Shape { 109 | // draw(); 110 | // } 111 | // interface Circle extends Shape { 112 | // radius: number; 113 | // } 114 | 115 | // function drawShapes1(shapes: S[]) { 116 | // shapes.forEach(s => s.draw()); 117 | // } 118 | 119 | // function drawShapes2(shapes: Shape[]) { 120 | // // this is simpler. Above type param is not necessary 121 | // shapes.forEach(s => s.draw()); 122 | // } 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 11 | 12 |
4 | ⚠️ A NOTE ABOUT VERSIONS ️️️⚠️ 5 |
9 | This branch is for the 2.x version of this workshop released in 2019 (for TypeScript 3.x). If you're looking for the 1.x version, released in 2017, click here 10 |
13 | 14 |

15 | 16 | 17 | 18 |

19 |

20 | 21 | 22 | 23 |

24 |

25 | 26 | 27 | 28 | 29 | 30 | 31 |

32 |

33 | This is the example project used for the Mike.Works TypeScript Fundamentals course. 34 |

35 | 36 | # Course outline and slides 37 | 38 | - [View course outline here](https://mike.works/course/typescript-fundamentals-7832c19) 39 | - [View slides here](https://docs.mike.works/typescript-slides-v2) 40 | 41 | ## Dependencies 42 | 43 | Make sure your system is set up with 44 | - *Node.js* - using [Volta](http://volta.sh) makes setup extremely easy! 45 | - [Yarn](https://yarnpkg.com/en/) 46 | - [Visual Studio Code](https://code.visualstudio.com/) 47 | - [TypeScript](https://www.typescriptlang.org/index.html#download-links) (should be globally installed) 48 | - [ESLint](https://eslint.org/docs/user-guide/getting-started#installation-and-usage) (should be globally installed) 49 | 50 | ## Project setup 51 | 52 | First, clone this project from Github 53 | 54 | ``` 55 | git clone https://github.com/mike-works/typescript-fundamentals 56 | cd typescript-fundamentals 57 | ``` 58 | 59 | Finally, while in the top-level folder of this project, download and install this project's dependencies by running 60 | 61 | ``` 62 | yarn 63 | ``` 64 | 65 | # License 66 | 67 | While the general license for this project is the BSD 3-clause, the exercises 68 | themselves are proprietary and are licensed on a per-individual basis, usually 69 | as a result of purchasing a ticket to a public workshop, or being a participant 70 | in a private training. 71 | 72 | Here are some guidelines for things that are **OK** and **NOT OK**, based on our 73 | understanding of how these licenses work: 74 | 75 | ### OK 76 | 77 | - Using everything in this project other than the exercises (or accompanying tests) 78 | to build a project used for your own free or commercial training material 79 | - Copying code from build scripts, configuration files, tests and development 80 | harnesses that are not part of the exercises specifically, for your own projects 81 | - As an owner of an individual license, using code from tests, exercises, or 82 | exercise solutions for your own non-training-related project. 83 | 84 | ### NOT OK (without express written consent) 85 | 86 | - Using this project, or any subset of 87 | exercises contained within this project to run your own workshops 88 | - Writing a book that uses the code for these exercises 89 | - Recording a screencast that contains one or more of this project's exercises 90 | 91 | # Copyright 92 | 93 | © 2018 [Mike.Works](https://mike.works), All Rights Reserved 94 | 95 | ###### This material may not be used for workshops, training, or any other form of instructing or teaching developers, without express written consent 96 | -------------------------------------------------------------------------------- /notes/1-basics.ts: -------------------------------------------------------------------------------- 1 | //== BASICS ==// 2 | 3 | /** 4 | * (1) x is a string, b/c we’ve initialized it 5 | */ 6 | // let x = "hello world"; 7 | 8 | /** 9 | * (2) reassignment is fine 10 | */ 11 | // x = "hello mars"; 12 | 13 | /** 14 | * (3) but if we try to change type 15 | */ 16 | // x = 42; // 🚨 ERROR 17 | 18 | /** 19 | * (4) let's look at const. The type is literally 'hello world' 20 | */ 21 | // const y = "hello world"; 22 | 23 | /** 24 | * This is called a 'string literal type'. y can never be reassigned since it's a const, 25 | * so we can regard it as only ever holding a value that's literally the string 'hello world' 26 | * and no other possible value 27 | */ 28 | 29 | /** 30 | * (5) sometimes we need to declare a variable w/o initializing it 31 | */ 32 | // let z; 33 | // z = 41; 34 | // z = "abc"; // (6) oh no! This isn't good 35 | 36 | /** 37 | * If we look at the type of z, it's `any`. This is the most flexible type 38 | * in TypeScript (think of it like a JavaScript `let`) 39 | */ 40 | 41 | /** 42 | * (7) we could improve this situation by providing a type annotation 43 | * when we declare our variable 44 | */ 45 | // let zz: number; 46 | // zz = 41; 47 | // zz = "abc"; // 🚨 ERROR Type '"abc"' is not assignable to type 'number'. 48 | 49 | //== SIMPLE ARRAYS ==// 50 | 51 | /** 52 | * (8) simple array types can be expressed using [] 53 | */ 54 | // let aa: number[] = []; 55 | // aa.push(33); 56 | // aa.push("abc"); // 🚨 ERROR: Argument of type '"abc"' is not assignable to parameter of type 'number'. 57 | 58 | /** 59 | * (9) we can even define a tuple, which has a fixed length 60 | */ 61 | // let bb: [number, string, string, number] = [ 62 | // 123, 63 | // "Fake Street", 64 | // "Nowhere, USA", 65 | // 10110 66 | // ]; 67 | 68 | // bb = [1, 2, 3]; // 🚨 ERROR: Type 'number' is not assignable to type 'string'. 69 | 70 | /** 71 | * (10) Tuple values often require type annotations ( : [number, number] ) 72 | */ 73 | // const xx = [32, 31]; // number[]; 74 | // const yy: [number, number] = [32, 31]; 75 | 76 | //== OBJECTS ==// 77 | 78 | /** 79 | * (11) object types can be expressed using {} and property names 80 | */ 81 | // let cc: { houseNumber: number; streetName: string }; 82 | // cc = { 83 | // streetName: "Fake Street", 84 | // houseNumber: 123 85 | // }; 86 | 87 | // cc = { 88 | // houseNumber: 33 89 | // }; 90 | /** 91 | * 🚨 Property 'streetName' 92 | * 🚨 is missing in type '{ houseNumber: number; }' 93 | * 🚨 but required in type '{ houseNumber: number; streetName: string; }'. 94 | */ 95 | 96 | /** 97 | * (12) You can use the optional operator (?) to 98 | * indicate that something may or may not be there 99 | */ 100 | // let dd: { houseNumber: number; streetName?: string }; 101 | // dd = { 102 | // houseNumber: 33 103 | // }; 104 | 105 | // (13) if we want to re-use this type, we can create an interface 106 | // interface Address { 107 | // houseNumber: number; 108 | // streetName?: string; 109 | // } 110 | // // and refer to it by name 111 | // let ee: Address = { houseNumber: 33 }; 112 | 113 | //== UNION & INTERSECTION ==// 114 | 115 | /** 116 | * (14) Union types 117 | * Sometimes we have a type that can be one of several things 118 | */ 119 | 120 | // export interface HasPhoneNumber { 121 | // name: string; 122 | // phone: number; 123 | // } 124 | 125 | // export interface HasEmail { 126 | // name: string; 127 | // email: string; 128 | // } 129 | 130 | // let contactInfo: HasEmail | HasPhoneNumber = 131 | // Math.random() > 0.5 132 | // ? { 133 | // // we can assign it to a HasPhoneNumber 134 | // name: "Mike", 135 | // phone: 3215551212 136 | // } 137 | // : { 138 | // // or a HasEmail 139 | // name: "Mike", 140 | // email: "mike@example.com" 141 | // }; 142 | 143 | // contactInfo.name; // NOTE: we can only access the .name property (the stuff HasPhoneNumber and HasEmail have in common) 144 | 145 | /** 146 | * (15) Intersection types 147 | */ 148 | // let otherContactInfo: HasEmail & HasPhoneNumber = { 149 | // // we _must_ initialize it to a shape that's asssignable to HasEmail _and_ HasPhoneNumber 150 | // name: "Mike", 151 | // email: "mike@example.com", 152 | // phone: 3215551212 153 | // }; 154 | 155 | // otherContactInfo.name; // NOTE: we can access anything on _either_ type 156 | // otherContactInfo.email; 157 | // otherContactInfo.phone; 158 | // const zzz: any = {} as never; 159 | 160 | export default {}; 161 | -------------------------------------------------------------------------------- /notes/6-guards-and-extreme-types.ts: -------------------------------------------------------------------------------- 1 | import { HasEmail } from "./1-basics"; 2 | 3 | //== TOP TYPES ==// 4 | 5 | /** 6 | * (1) "Top types" are types that can hold any value. Typescript has two of them 7 | */ 8 | 9 | // let myAny: any = 32; 10 | // let myUnknown: unknown = "hello, unknown"; 11 | 12 | // Note that we can do whatever we want with an any, but nothing with an unknown 13 | 14 | // myAny.foo.bar.baz; 15 | // myUnknown.foo; 16 | 17 | /** 18 | * (2) When to use `any` 19 | * Anys are good for areas of our programs where we want maximum flexibility 20 | * Example: sometimes a Promise is fine when we don't care at all about the resolved value 21 | */ 22 | // async function logWhenResolved(p: Promise) { 23 | // const val = await p; 24 | // console.log("Resolved to: ", val); 25 | // } 26 | 27 | /** 28 | * (3) When to use `unknown` 29 | * Unknowns are good for "private" values that we don't want to expose through a public API. 30 | * They can still hold any value, we just must narrow the type before we're able to use it. 31 | * 32 | * We'll do htis with a type guard. 33 | */ 34 | 35 | // myUnknown.split(", "); // 🚨 ERROR 36 | 37 | /** 38 | * (4) Built-in type guards 39 | */ 40 | // if (typeof myUnknown === "string") { 41 | // // in here, myUnknown is of type string 42 | // myUnknown.split(", "); // ✅ OK 43 | // } 44 | // if (myUnknown instanceof Promise) { 45 | // // in here, myUnknown is of type Promise 46 | // myUnknown.then(x => console.log(x)); 47 | // } 48 | 49 | /** 50 | * (5) User-defined type guards 51 | * We can also create our own type guards, using functions that return booleans 52 | */ 53 | 54 | // // 💡 Note return type 55 | // function isHasEmail(x: any): x is HasEmail { 56 | // return typeof x.name === "string" && typeof x.email === "string"; 57 | // } 58 | 59 | // if (isHasEmail(myUnknown)) { 60 | // // In here, myUnknown is of type HasEmail 61 | // console.log(myUnknown.name, myUnknown.email); 62 | // } 63 | 64 | // // my most common guard 65 | // function isDefined(arg: T | undefined): arg is T { 66 | // return typeof arg !== "undefined"; 67 | // } 68 | 69 | // // NEW TS 3.7: assertion-based type guards! 70 | // function assertIsStringArray(arr: any[]): asserts arr is string[] { 71 | // if (!arr) throw new Error('not an array!'); 72 | // const strings = arr.filter(i => typeof i === 'string'); 73 | // if (strings.length !== arr.length) throw new Error('not an array of strings'); 74 | // } 75 | 76 | // const arr: (string|number)[] = ['3', 12, '21']; 77 | // assertIsStringArray(arr); 78 | // arr; 79 | 80 | /** 81 | * (6) Dealing with multiple unknowns 82 | * - We kind of lose some of the benefits of structural typing when using `unknown`. 83 | * - Look how we can get mixed up below 84 | */ 85 | 86 | // let aa: unknown = 41; 87 | // let bb: unknown = ["a", "string", "array"]; 88 | // bb = aa; // 🚨 yikes 89 | 90 | /** 91 | * (7) Alternative to unknowns - branded types 92 | * - One thing we can do to avoid this is to create types with structures that 93 | * - are difficult to accidentally match. This involves unsafe casting, but it's ok 94 | * - if we do things carefully 95 | */ 96 | 97 | /* two branded types, each with "brand" and "unbrand" functions */ 98 | // interface BrandedA { 99 | // __this_is_branded_with_a: "a"; 100 | // } 101 | // function brandA(value: string): BrandedA { 102 | // return (value as unknown) as BrandedA; 103 | // } 104 | // function unbrandA(value: BrandedA): string { 105 | // return (value as unknown) as string; 106 | // } 107 | 108 | // interface BrandedB { 109 | // __this_is_branded_with_b: "b"; 110 | // } 111 | // function brandB(value: { abc: string }): BrandedB { 112 | // return (value as unknown) as BrandedB; 113 | // } 114 | // function unbrandB(value: BrandedB): { abc: string } { 115 | // return (value as unknown) as { abc: string }; 116 | // } 117 | 118 | // let secretA = brandA("This is a secret value"); 119 | // let secretB = brandB({ abc: "This is a different secret value" }); 120 | 121 | // secretA = secretB; // ✅ No chance of getting these mixed up 122 | // unbrandB(secretA); 123 | // unbrandA(secretB); 124 | 125 | // // back to our original values 126 | // let revealedA = unbrandA(secretA); 127 | // let revealedB = unbrandB(secretB); 128 | 129 | // 💡 PROTIP - always brand/unbrand casting in exactly one place. 130 | 131 | //== BOTTOM TYPE: never ==// 132 | 133 | /** 134 | * (8) Bottom types can hold no values. TypeScript has one of these: `never` 135 | */ 136 | // let n: never = 4; 137 | 138 | /** 139 | * A common place where you'll end up with a never 140 | * is through narrowing exhaustively 141 | */ 142 | 143 | // let x = "abc" as string | number; 144 | 145 | // if (typeof x === "string") { 146 | // // x is a string here 147 | // x.split(", "); 148 | // } else if (typeof x === "number") { 149 | // // x is a number here 150 | // x.toFixed(2); 151 | // } else { 152 | // // x is a never here 153 | // } 154 | 155 | /** 156 | * (9) We can use this to our advantage to create exhaustive conditionals and switches 157 | */ 158 | 159 | // class UnreachableError extends Error { 160 | // constructor(val: never, message: string) { 161 | // super(`TypeScript thought we could never end up here\n${message}`); 162 | // } 163 | // } 164 | 165 | // let y = 4 as string | number; 166 | 167 | // if (typeof y === "string") { 168 | // // y is a string here 169 | // y.split(", "); 170 | // } else if (typeof y === "number") { 171 | // // y is a number here 172 | // y.toFixed(2); 173 | // } else { 174 | // throw new UnreachableError(y, "y should be a string or number"); 175 | // } 176 | -------------------------------------------------------------------------------- /challenges/address-book/test/address-book.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { beforeEach, describe, it } from "mocha"; 3 | import * as indexExports from "../src/index"; 4 | 5 | describe("index.ts module", () => { 6 | // it("should have a Person export (type or interface)", () => { 7 | // expect((indexExports as any).Person).to.eq(undefined); 8 | // const x: indexExports.Person = {} as any; 9 | // }); 10 | // it("should have a Address export (type or interface)", () => { 11 | // expect((indexExports as any).Address).to.eq(undefined); 12 | // const x: indexExports.Address = {} as any; 13 | // }); 14 | it("should have a AddressBook export (class)", () => { 15 | expect(!!indexExports.AddressBook).to.eq(true, "export exists"); 16 | expect(typeof indexExports.AddressBook).to.eq("function", "export exists"); 17 | const ab = new indexExports.AddressBook(); 18 | expect(!!ab).to.eq(true, "can instantiate from AddressBook as a class"); 19 | expect(ab.constructor.name).to.eq("AddressBook"); 20 | expect(Object.keys(indexExports.AddressBook.prototype)).to.deep.eq( 21 | ["addContact", "findContactByName"], 22 | "should have addContact and findContactByName methods" 23 | ); 24 | }); 25 | it("should have a getVcardText export (function)", () => { 26 | expect(!!indexExports.getVcardText).to.eq(true, "export exists"); 27 | expect(typeof indexExports.getVcardText).to.eq( 28 | "function", 29 | "it is a function" 30 | ); 31 | expect(indexExports.getVcardText.prototype).to.deep.eq({}); 32 | 33 | // expect(indexExports.getVcardText.prototype.name).to.eq("function"); 34 | }); 35 | }); 36 | 37 | describe("AddressBook", () => { 38 | let ab: indexExports.AddressBook; 39 | beforeEach(() => { 40 | ab = new indexExports.AddressBook(); 41 | }); 42 | 43 | it("should have an addContact() function", () => { 44 | expect(typeof ab.addContact).to.eq("function", "addContact is a function"); 45 | expect( 46 | indexExports.AddressBook.prototype.hasOwnProperty("addContact") 47 | ).to.eq(true, "function is on the AddressBook prototype"); 48 | }); 49 | 50 | it("should have an findContactByName() function", () => { 51 | expect(typeof ab.findContactByName).to.eq( 52 | "function", 53 | "findContactByName is a function" 54 | ); 55 | expect( 56 | indexExports.AddressBook.prototype.hasOwnProperty("findContactByName") 57 | ).to.eq(true, "function is on the AddressBook prototype"); 58 | }); 59 | 60 | it('should have "contacts" member data', () => { 61 | expect(ab.hasOwnProperty("contacts")).to.eq( 62 | true, 63 | "it is not on the prototype" 64 | ); 65 | }); 66 | 67 | it('"contacts" should be an array', () => { 68 | expect(Array.isArray(ab.contacts)).to.eq(true); 69 | }); 70 | 71 | it('"contacts" array should initially be empty', () => { 72 | expect(ab.contacts.length).to.eq(0); 73 | }); 74 | 75 | it('adding a contact should increase the "contacts" array length', () => { 76 | expect(ab.contacts.length).to.eq(0); 77 | ab.addContact({ 78 | firstName: "Mike", 79 | lastName: "North", 80 | addresses: {}, 81 | phones: {} 82 | }); 83 | expect(ab.contacts.length).to.eq(1); 84 | }); 85 | 86 | it("findContactByName lookup by last name", () => { 87 | ab.addContact({ 88 | firstName: "Mike", 89 | lastName: "North", 90 | addresses: {}, 91 | phones: {} 92 | }); 93 | ab.addContact({ 94 | firstName: "Nobody", 95 | lastName: "North", 96 | addresses: {}, 97 | phones: {} 98 | }); 99 | expect(ab.findContactByName({ lastName: "" }).length).to.eq(0); 100 | expect(ab.findContactByName({ lastName: "North" }).length).to.eq(2); 101 | }); 102 | 103 | it("findContactByName lookup by first name", () => { 104 | ab.addContact({ 105 | firstName: "Mike", 106 | lastName: "North", 107 | addresses: {}, 108 | phones: {} 109 | }); 110 | ab.addContact({ 111 | firstName: "Nobody", 112 | lastName: "North", 113 | addresses: {}, 114 | phones: {} 115 | }); 116 | expect(ab.findContactByName({ firstName: "Mike" }).length).to.eq(1); 117 | expect(ab.findContactByName({ lastName: "Steve" }).length).to.eq(0); 118 | }); 119 | 120 | it("findContactByName lookup by both first and last name", () => { 121 | ab.addContact({ 122 | firstName: "Mike", 123 | lastName: "North", 124 | addresses: {}, 125 | phones: {} 126 | }); 127 | ab.addContact({ 128 | firstName: "Nobody", 129 | lastName: "North", 130 | addresses: {}, 131 | phones: {} 132 | }); 133 | expect( 134 | ab.findContactByName({ firstName: "Mike", lastName: "North" }).length 135 | ).to.eq(1); 136 | expect( 137 | ab.findContactByName({ firstName: "Nobody", lastName: "North" }).length 138 | ).to.eq(1); 139 | }); 140 | }); 141 | 142 | describe("getVcardText", () => { 143 | it("should properly serialize a contact w/ no addresses or phones", () => { 144 | const date = new Date(); 145 | expect( 146 | indexExports.getVcardText( 147 | { 148 | firstName: "Mike", 149 | lastName: "North", 150 | addresses: {}, 151 | phones: {} 152 | }, 153 | date 154 | ) 155 | ).to.eq( 156 | `BEGIN:VCARD 157 | VERSION:2.1 158 | N:North;Mike;; 159 | FN:Mike North 160 | REV:${indexExports.formatDate(date)} 161 | END:VCARD` 162 | ); 163 | }); 164 | 165 | it("should properly serialize a contact w/ some phones", () => { 166 | const date = new Date(); 167 | expect( 168 | indexExports.getVcardText( 169 | { 170 | firstName: "Mike", 171 | lastName: "North", 172 | addresses: {}, 173 | phones: { 174 | home: "3215551212", 175 | office: "3215551200" 176 | } 177 | }, 178 | date 179 | ) 180 | ).to.eq( 181 | `BEGIN:VCARD 182 | VERSION:2.1 183 | N:North;Mike;; 184 | FN:Mike North 185 | TEL;HOME;VOICE:3215551212 186 | TEL;OFFICE;VOICE:3215551200 187 | REV:${indexExports.formatDate(date)} 188 | END:VCARD` 189 | ); 190 | }); 191 | it("should properly serialize a contact w/ some addresses and phones", () => { 192 | const date = new Date(); 193 | expect( 194 | indexExports.getVcardText( 195 | { 196 | firstName: "Mike", 197 | lastName: "North", 198 | addresses: { 199 | home: { 200 | houseNumber: 123, 201 | street: "Fake Street", 202 | state: "MN", 203 | city: "Anytown", 204 | country: "United States of America", 205 | postalCode: 123456 206 | }, 207 | work: { 208 | houseNumber: 456, 209 | street: "Not Real Street", 210 | state: "MN", 211 | city: "Anytown", 212 | country: "United States of America", 213 | postalCode: 123456 214 | } 215 | }, 216 | phones: { 217 | home: "3215551212", 218 | office: "3215551200" 219 | } 220 | }, 221 | date 222 | ) 223 | ).to.eq( 224 | `BEGIN:VCARD 225 | VERSION:2.1 226 | N:North;Mike;; 227 | FN:Mike North 228 | TEL;HOME;VOICE:3215551212 229 | TEL;OFFICE;VOICE:3215551200 230 | ADR;HOME:;;123 Fake Street;Anytown;MN;123456;United States of America 231 | LABEL;HOME;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:123 Fake Street.=0D=0A=Anytown, MN 123456=0D=0AUnited States of America 232 | ADR;WORK:;;456 Not Real Street;Anytown;MN;123456;United States of America 233 | LABEL;WORK;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:456 Not Real Street.=0D=0A=Anytown, MN 123456=0D=0AUnited States of America 234 | REV:${indexExports.formatDate(date)} 235 | END:VCARD` 236 | ); 237 | }); 238 | }); 239 | --------------------------------------------------------------------------------