├── examples ├── quickFixes.md ├── testingTypes.md ├── typeAssertions.md ├── whyTypescript.md ├── boundedTypeParameters.md ├── typesWithoutImplementations.md ├── missingDeclarations.md ├── refactoring.md ├── definitionFiles.md ├── whatTypesafeMeans.md ├── monacoEditor.md ├── whenToBailOut.md ├── dependentProperties.md ├── extendingModules.md ├── voidType.md ├── objectType.md ├── arrayElementAccess.md ├── limitations.md ├── never.md ├── tsxFiles.md ├── typeGuards.md ├── filterReduce.md ├── returnValues.md ├── generics.md ├── index.tsx ├── buildUpObject.md ├── safePropertyAccess.md ├── widening.md ├── excessPropertyChecking.md ├── indexSignatures.md └── objectKeysAndEntries.md ├── .eslintignore ├── .prettierrc ├── .storybook ├── addons.js ├── preview-head.html ├── config.js └── webpack.config.js ├── babel.config.js ├── .gitignore ├── types ├── raw-loader.d.ts └── markdown.d.ts ├── diagnostic-codes ├── 2339.md └── 2345.md ├── stories ├── about.stories.tsx ├── examples.stories.tsx └── components │ ├── MarkdownDocs.tsx │ └── Editor.tsx ├── tsconfig.json ├── jargon.md ├── package.json └── README.md /examples/quickFixes.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/testingTypes.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/typeAssertions.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/whyTypescript.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/boundedTypeParameters.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/typesWithoutImplementations.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .circleci 3 | .next -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all" 3 | } 4 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import "@storybook/addon-links/register"; 2 | -------------------------------------------------------------------------------- /examples/missingDeclarations.md: -------------------------------------------------------------------------------- 1 | // diagnostic codes: ts2307 2 | 3 | import something from "something"; 4 | -------------------------------------------------------------------------------- /examples/refactoring.md: -------------------------------------------------------------------------------- 1 | ```tsx 2 | const doStuff = () => { 3 | return Math.max(4 + 1, 4); 4 | } 5 | ``` -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@babel/env", "@babel/react", "@babel/typescript"], 3 | }; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | .vscode 4 | yarn-error.log 5 | cypress/screenshots 6 | cypress/videos 7 | storybook-static -------------------------------------------------------------------------------- /types/raw-loader.d.ts: -------------------------------------------------------------------------------- 1 | declare module "!raw-loader!*" { 2 | declare const contents: string; 3 | export default contents; 4 | } 5 | -------------------------------------------------------------------------------- /examples/definitionFiles.md: -------------------------------------------------------------------------------- 1 | // go over what's different about .d.ts 2 | 3 | // declaration files 4 | 5 | // first: if a declaration file is within -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /examples/whatTypesafeMeans.md: -------------------------------------------------------------------------------- 1 | // avoid polluting other files 2 | export {}; 3 | 4 | // go over: it's not about your one usage, it's about making 5 | // wide-scale changes without worry 6 | -------------------------------------------------------------------------------- /types/markdown.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.md" { 2 | const contents: string; 3 | export default contents; 4 | } 5 | 6 | declare module "!file-loader!*" { 7 | const contents: string; 8 | export default contents; 9 | } -------------------------------------------------------------------------------- /examples/monacoEditor.md: -------------------------------------------------------------------------------- 1 | [TypeScript Playground source](https://github.com/microsoft/TypeScript-Website/blob/master/playground/playground.html) 2 | 3 | Lot of errors in here and references to things that don't exist though 4 | -------------------------------------------------------------------------------- /diagnostic-codes/2339.md: -------------------------------------------------------------------------------- 1 | # Error TS2339: `Property '{0}' does not exist on type '{1}'.` 2 | 3 | This occurs when 4 | 5 | Likely causes: 6 | 7 | ### You have a typo in either the code or the type. 8 | 9 | ### You have a union type of two objects, and 10 | -------------------------------------------------------------------------------- /stories/about.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import readme from "../README.md"; 3 | import { MarkdownDocs } from "./components/MarkdownDocs"; 4 | 5 | export default { 6 | title: "About", 7 | }; 8 | 9 | export const errors = () => {readme}; 10 | -------------------------------------------------------------------------------- /examples/whenToBailOut.md: -------------------------------------------------------------------------------- 1 | example: camelize/decamelize. not worth typing. do it 2 | before reaching typed code 3 | 4 | ```tsx 5 | declare const camelizeKeys: (obj: object) => object; 6 | 7 | interface UserResponse { 8 | [key: string]: { 9 | id: string; 10 | name: string; 11 | phone?: string; 12 | }; 13 | } 14 | 15 | fetch("/users") 16 | .then(response => response.json()) 17 | .then(json => camelizeKeys(json) as UserResponse); 18 | ``` 19 | -------------------------------------------------------------------------------- /stories/examples.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { storiesOf } from "@storybook/react"; 3 | import { MarkdownDocs } from "./components/MarkdownDocs"; 4 | import * as examples from "../examples"; 5 | import { startCase } from "lodash"; 6 | 7 | Object.entries(examples).reduce((stories, [name, contents]) => { 8 | return stories.add(startCase(name), () => ( 9 | {contents} 10 | )); 11 | }, storiesOf("Examples", module)); 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "allowSyntheticDefaultImports": true, 5 | "jsx": "preserve", 6 | "lib": ["dom", "esnext"], 7 | "module": "esnext", 8 | "moduleResolution": "node", 9 | "noEmit": true, 10 | "noUnusedLocals": false, 11 | "noUnusedParameters": false, 12 | "preserveConstEnums": true, 13 | "removeComments": false, 14 | "skipLibCheck": true, 15 | "sourceMap": true, 16 | "strict": true, 17 | "target": "esnext" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { configure } from "@storybook/react"; 3 | import { addDecorator } from "@storybook/react"; 4 | 5 | // automatically import all files ending in *.stories.js 6 | configure(require.context("../stories", true, /\.stories\.tsx$/), module); 7 | 8 | const styles = { 9 | color: "#333", 10 | fontFamily: "Open Sans, sans-serif", 11 | fontSize: "1rem", 12 | maxWidth: 1000, 13 | paddingLeft: 20, 14 | paddingRight: 20, 15 | margin: "0 auto", 16 | }; 17 | const LayoutDecorator = storyFn =>
{storyFn()}
; 18 | addDecorator(LayoutDecorator); 19 | -------------------------------------------------------------------------------- /examples/dependentProperties.md: -------------------------------------------------------------------------------- 1 | // avoid polluting other files 2 | export {}; 3 | import React from "react"; 4 | 5 | // case: you have two properties which depend on each other 6 | // with optionals: 7 | interface UserWithOptionals { 8 | id: string; 9 | name?: string; 10 | phone?: string; 11 | } 12 | 13 | interface UnregisteredUser { 14 | id: string; 15 | name: null; 16 | phone: null; 17 | } 18 | 19 | interface RegisteredUser { 20 | id: string; 21 | name: string; 22 | phone: string; 23 | } 24 | 25 | const UserTile = (user: UserWithOptionals) => { 26 | return ( 27 |
28 |

{user.name}

29 | {user.phone} 30 |
31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /examples/extendingModules.md: -------------------------------------------------------------------------------- 1 | # Extending Modules (Feature) 2 | 3 | go over all this "declare" stuff 4 | show how you can extend interfaces in modules 5 | 6 | ```tsx 7 | import { Action, createStore, Dispatch } from "redux"; 8 | 9 | declare module "redux" { 10 | type ThunkAction = ( 11 | dispatch: Dispatch, 12 | getState: () => TState, 13 | ) => TAction; 14 | type AnyThunk = ThunkAction; 15 | 16 | export interface Dispatch< 17 | A extends Action = AnyAction, 18 | T extends ThunkAction = AnyThunk, 19 | TState = any 20 | > { 21 | (action: T): (dispatch: Dispatch, getState: () => TState) => T; 22 | } 23 | } 24 | ``` 25 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin"); 3 | const MONACO_DIR = path.resolve(__dirname, "./node_modules/monaco-editor"); 4 | const util = require("util"); 5 | 6 | module.exports = async ({ config }) => { 7 | config.module.rules.push({ 8 | test: /\.tsx?$/, 9 | use: ["babel-loader"], 10 | }); 11 | config.module.rules.push({ 12 | test: /\.css$/, 13 | use: ["style-loader", "css-loader"], 14 | include: MONACO_DIR, 15 | }); 16 | config.plugins.push(new MonacoWebpackPlugin()); 17 | config.resolve.extensions = [".ts", ".tsx", ".js"]; 18 | config.externals = { "@microsoft/typescript-etw": "FakeModule" }; 19 | config.stats = "errors-only"; 20 | return config; 21 | }; 22 | -------------------------------------------------------------------------------- /examples/voidType.md: -------------------------------------------------------------------------------- 1 | The `void` type is the absence of a type. It has one purpose: to 2 | allow functions to return values which are ignored. 3 | A practical use for this is an event handler which may want 4 | to use a braceless arrow function. 5 | 6 | Contrast this with `undefined`, which enforces a return statement. 7 | 8 | ```tsx 9 | type OnMouseEnter = () => void; 10 | const onMouseEnter: OnMouseEnter = () => false; // fine 11 | const returnValue = onMouseEnter(); // returnValue === void 12 | 13 | type OnMouseLeave = () => undefined; 14 | const onMouseLeave: OnMouseLeave = () => false; // error: false !== undefined 15 | 16 | // Note that void is more strict when put directly on a function. 17 | const returnUndefined = (): undefined => { 18 | return false; // error: false !== undefined 19 | }; 20 | const returnVoid = (): void => { 21 | return false; // error: false !== void 22 | }; 23 | ``` 24 | -------------------------------------------------------------------------------- /jargon.md: -------------------------------------------------------------------------------- 1 | # Common Terms 2 | 3 | ## Structural Typing 4 | 5 | ## Subtype 6 | 7 | In structural typing, a type which matches another type, but contains extra properties, is considered a subtype. 8 | In TypeScript 9 | Example: 10 | 11 | ```ts 12 | const; 13 | ``` 14 | 15 | ## Excess Property Checking 16 | 17 | The 18 | 19 | ## Freshness 20 | 21 | ## Inference 22 | 23 | ## Invariant 24 | 25 | ## Type Parameter 26 | 27 | Code which can be checked at runtime. 28 | 29 | Type safe function: 30 | 31 | ```tsx 32 | const; 33 | ``` 34 | 35 | ## Type Safe 36 | 37 | # Obscure Terms 38 | 39 | ## Covariant/Contravariant 40 | 41 | # Really Obscure Terms 42 | 43 | ## Homomorphic 44 | 45 | ## Naked Type Parameter 46 | 47 | A type parameter (see above) which is not wrapped in something like an array. 48 | 49 | Example: 50 | 51 | ```ts 52 | // T here is any element in the array 53 | // T[] is the array itself 54 | const filter = (array: T[], func: (T): boolean): T[] => {} 55 | ``` 56 | -------------------------------------------------------------------------------- /examples/objectType.md: -------------------------------------------------------------------------------- 1 | # Object Type (Quirk) 2 | 3 | The `object` type is when you have no way of knowing 4 | what an object is ahead of time. 5 | 6 | If you've used Flow, don't be confused by this. This is not 7 | the weak `Object` type (which is now just an alias for `any`). 8 | It is closer to the empty object `{}`, and any property accesses 9 | will result in an error. 10 | 11 | ```tsx 12 | const getEnabled = (data: object) => { 13 | data.enabled; // error 14 | if ("enabled" in data) { 15 | return data.enabled; // still an error 16 | } 17 | }; 18 | ``` 19 | 20 | ```tsx 21 | // Example: You don't know what the properties of an object 22 | // are, but you know which ones you care about. 23 | // This one's easy: see excess-property-checking.ts 24 | const getEnabled = (data: { enabled?: boolean }) => { 25 | return !!data.enabled; 26 | }; 27 | const data1 = { enabled: true, name: "test1" }; 28 | const data2 = { name: "test1" }; 29 | getEnabled(data1); 30 | getEnabled(data2); 31 | ``` 32 | -------------------------------------------------------------------------------- /examples/arrayElementAccess.md: -------------------------------------------------------------------------------- 1 | # Unsafe Array Access (Limitation) 2 | 3 | In this example 4 | 5 | ```tsx 6 | const array = [0, 1]; 7 | array[0]; 8 | array[1]; 9 | array[2]; // hey! this should be an error! 10 | ``` 11 | 12 | but just like objects are mutable in TS, so are arrays. 13 | Imagine this case: 14 | 15 | ```tsx 16 | const pushToArray = (arr: T[], element: T): T[] => { 17 | arr.push(element); 18 | return arr; 19 | }; 20 | 21 | const arr = [0, 1]; 22 | pushToArray(arr, 2); // now arr[2] exists 23 | arr[2]; // if TS showed an error here, it would be extremely annoying 24 | ``` 25 | 26 | TypeScript would have to do a lot more work to analyze your code to figure 27 | out safe array access - and it's just not a common enough case to be worth 28 | the cost. 29 | 30 | However, tuple types exist, which have a known length that can never change. 31 | Checking access on these is cheap, so use `as const` to change from number[] 32 | (array of numbers of any length) to [number, number] (array of numbers with 33 | a length of 2). 34 | (you'll see the term "const context" floating around: it means "immutable") 35 | 36 | ```tsx 37 | const tuple = [0, 1] as const; 38 | 39 | tuple[0]; 40 | tuple[1]; 41 | tuple[2]; // correctly an error 42 | ``` 43 | -------------------------------------------------------------------------------- /examples/limitations.md: -------------------------------------------------------------------------------- 1 | go over APIs which can't be modeled at all 2 | example: camelize 3 | issue: https://github.com/microsoft/TypeScript/issues/12754 4 | 5 | ```tsx 6 | // from: https://stackoverflow.com/questions/2970525/converting-any-string-into-camel-case 7 | const camelize = (str: string) => { 8 | return str 9 | .replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => { 10 | return index == 0 ? word.toLowerCase() : word.toUpperCase(); 11 | }) 12 | .replace(/\s+/g, ""); 13 | }; 14 | const kebabObj = { 15 | "kebab-key": 1, 16 | }; 17 | const camelObj = { 18 | camelKey: 1, 19 | }; 20 | const key = "kebab-key"; 21 | kebabObj[key]; // OK! 22 | const camelKey = camelize("kebab-key"); // now "string" instead of "camelKey" 23 | camelObj[camelKey]; // can't index by string - must be a known property 24 | ``` 25 | 26 | ```tsx 27 | // example: string concatenation used as key 28 | const sizes = { 29 | "size-sm": 1, 30 | "size-md": 2, 31 | "size-lg": 3, 32 | }; 33 | 34 | const getSize = (size: "sm" | "md" | "lg") => { 35 | return sizes[`size-${size}`]; 36 | }; 37 | ``` 38 | 39 | example: string corresponding to nested keys in object 40 | 41 | ```tsx 42 | const getIn = () => {}; 43 | ``` 44 | 45 | example: array corresponding to nested keys in object 46 | 47 | ```tsx 48 | const getIn = (path: string) => {}; 49 | ``` 50 | -------------------------------------------------------------------------------- /examples/never.md: -------------------------------------------------------------------------------- 1 | The `never` type is a condition which should never happen. This most commonly happens when a function throws an error, or when if/else/switch/case runs out of options and hits an `else` or `default` case. 2 | 3 | ```tsx 4 | function fn(x: string | number): string { 5 | if (typeof x === "string") { 6 | return x; 7 | } else if (typeof x === "number") { 8 | return x.toString(); 9 | } 10 | } 11 | ``` 12 | 13 | Functions which throw will always have a `never` return type. 14 | 15 | ```tsx 16 | function fail(msg: string): never { 17 | throw new Error(msg); 18 | } 19 | ``` 20 | 21 | The other time you'll see `never` is when creating a new empty array, when 22 | noImplicityAny is set to `false`. It is unlikely you will see this, but the 23 | type defaults to `never[]` to prevent it from being inferred as `any[]`. 24 | Simple fix: give it a type, or set `noImplicitAny` to true. 25 | https://github.com/microsoft/TypeScript/pull/8944 26 | 27 | Sometimes `never[]` is really a symptom of another error. Here's a case where 28 | an object is 29 | 30 | ```tsx 31 | type Resolver = { 32 | location: { city: string; state: string }; 33 | }; 34 | 35 | const resolver: Resolver = { 36 | location: [], 37 | }; 38 | ``` 39 | 40 | ## References 41 | 42 | [https://microsoft.github.io/TypeScript-New-Handbook/chapters/more-on-functions/#never](Never Type) 43 | -------------------------------------------------------------------------------- /examples/tsxFiles.md: -------------------------------------------------------------------------------- 1 | If you're using .tsx files (TypeScript React), and you're using a syntax 2 | where angle brackets appear before a type, TypeScript needs a bit of extra 3 | help to distinguish JSX from type parameters. 4 | 5 | The first case this can happen is when defining a generic function inline: 6 | 7 | ```tsx 8 | import React from "react"; 9 | 10 | const Button: React.FC = ({ children }) => { 11 | return ; 12 | } 13 | 14 | // uncomment this block to see syntax error 15 | const identity = (item: T): T => { 16 | return item; 17 | }; 18 | ``` 19 | 20 | Adding `extends any` works. This doesn't force the parameter to `any` - it 21 | just allows anything to be used as a parameter. See: [Bounded Type Parameters](boundedTypeParameters). 22 | 23 | ```tsx 24 | const identity = (item: T): T => { 25 | return item; 26 | }; 27 | 28 | // The following is fine, as JSX wouldn't be valid here: 29 | identity("hi"); 30 | 31 | // Same here: 32 | type RouterLocation = Location & { query: TQuery }; 33 | let routerLocation: RouterLocation<{ referrer: string }>; 34 | ``` 35 | 36 | The second case is when using an older style of type assertions. To fix, just use the newer `as` style. 37 | 38 | ```tsx 39 | // uncomment to see syntax error 40 | // const untypedLocation = window.location; 41 | 42 | const anyLocation = location as any; 43 | ``` 44 | -------------------------------------------------------------------------------- /diagnostic-codes/2345.md: -------------------------------------------------------------------------------- 1 | # Error (ts)2345: `Argument of type '{0}' is not assignable to parameter of type '{1}'.` 2 | 3 | Likely causes: 4 | 5 | ### You've created an array of specific strings, and you're calling `map` or `forEach` with a function that expects those strings. 6 | 7 |
Details 8 |

9 | 10 | TypeScript currently 11 | 12 | #### GitHub Issues 13 | 14 | [Assume arity of tuples when declared as literal](https://github.com/Microsoft/TypeScript/issues/24350) 15 | 16 | #### Example 17 | 18 | [TypeScript Playground]() 19 | 20 | ```typescript 21 | const strictFunction = (item: "one" | "two") => { 22 | return item; 23 | }; 24 | 25 | const items = ["one", "two"]; 26 | items.map(item => strictFunction(item)); 27 | ``` 28 | 29 | #### Quick Fix 30 | 31 | Typescript 3.4+: 32 | 33 | ```typescript 34 | const items = ["one", "two"] as const; 35 | // or inline 36 | (["one", "two"] as const).map(item => strictFunction(item)); 37 | ``` 38 | 39 | TypeScript < 3.4: 40 | 41 | ```typescript 42 | const items: ["one", "two"] = ["one", "two"]; 43 | items.map(item => strictFunction(item)); 44 | ``` 45 | 46 |

47 |
48 | -------------------------------------------------------------------------------- /stories/components/MarkdownDocs.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, ReactNode } from "react"; 2 | import { Editor } from "./Editor"; 3 | import Markdown from "markdown-to-jsx"; 4 | import LinkTo from "@storybook/addon-links/dist/react"; 5 | import { kebabCase } from "lodash"; 6 | 7 | interface LinkProps { 8 | href?: string; 9 | children?: ReactNode; 10 | } 11 | const Link = ({ href, children }: LinkProps) => { 12 | if (!href) { 13 | return
{children}
; 14 | } 15 | if (href.startsWith("http")) { 16 | return {children}; 17 | } 18 | return {children}; 19 | }; 20 | 21 | interface EditorSwitchProps { 22 | children?: ReactNode; 23 | className?: string; 24 | } 25 | const EditorSwitch = ({ 26 | children, 27 | className, 28 | }: EditorSwitchProps): ReactElement => { 29 | if (typeof children === "string" && className === "lang-tsx") { 30 | return {children}; 31 | } 32 | return ( 33 | 41 | {children} 42 | 43 | ); 44 | }; 45 | 46 | interface MarkdownDocsProps { 47 | children: ReactNode; 48 | } 49 | export const MarkdownDocs = ({ children }: MarkdownDocsProps) => { 50 | return ( 51 | 59 | {children} 60 | 61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /examples/typeGuards.md: -------------------------------------------------------------------------------- 1 | ## Diagnostic Codes 2 | 3 | ts2339 4 | 5 | go over: why property accesses are checked for union types 6 | it's important for refactoring, types go both ways 7 | 8 | Why can't I just access a property? 9 | because you're not just changing your code, you want to 10 | be able to change the underlying data as well 11 | 12 | ```tsx 13 | interface VideoWithType { 14 | type: "video"; 15 | url: string; 16 | length: number; 17 | } 18 | 19 | interface PhotoWithType { 20 | kind: "photo"; // I refactored the above and forgot this one 21 | url: string; 22 | } 23 | 24 | const showMedia = (media: PhotoWithType | VideoWithType) => { 25 | // error - if no property check were done here, this would 26 | // never be true 27 | if (media.type === "photo") { 28 | media; // Photo 29 | } 30 | }; 31 | 32 | interface Video { 33 | type: "video"; 34 | url: string; 35 | length: number; 36 | } 37 | 38 | interface Photo { 39 | type: "photo"; 40 | url: string; 41 | } 42 | ``` 43 | 44 | using `in` is "less safe" for refactoring than other guards. 45 | You can check any property, even something that doesn't exist on 46 | either object. Use with caution. 47 | 48 | ```tsx 49 | const showMoreMedia = (media: Photo | Video) => { 50 | if (media.type === "photo") { 51 | media; // Photo 52 | } 53 | // error: "Photo" does not contain property "length" 54 | if (media.length) { 55 | media; 56 | } 57 | if (media.url) { 58 | media; // Photo | Video 59 | } 60 | if ("length" in media) { 61 | media; // Video 62 | } 63 | if ("url" in media) { 64 | media; // Photo | Video 65 | } 66 | }; 67 | ``` 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-js-patterns", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "storybook": "start-storybook -p 6006", 9 | "build-storybook": "build-storybook" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@babel/preset-env": "^7.7.6", 16 | "@babel/preset-react": "^7.7.4", 17 | "@babel/preset-typescript": "^7.7.4", 18 | "@storybook/cli": "^5.2.8", 19 | "@types/lodash": "^4.14.149", 20 | "@types/markdown-to-jsx": "^6.9.1", 21 | "@types/react": "^16.9.16", 22 | "@typescript-eslint/eslint-plugin": "^2.11.0", 23 | "@typescript-eslint/parser": "^2.11.0", 24 | "eslint": "^6.7.2", 25 | "eslint-plugin-jsx-a11y": "^6.2.3", 26 | "eslint-plugin-react": "^7.17.0", 27 | "eslint-plugin-react-hooks": "^2.3.0", 28 | "file-loader": "^5.0.2", 29 | "lodash": "^4.17.15", 30 | "markdown-to-jsx": "^6.10.3", 31 | "monaco-editor": "^0.18.1", 32 | "monaco-editor-webpack-plugin": "^1.7.0", 33 | "prettier": "^1.19.1", 34 | "raw-loader": "^4.0.0", 35 | "react": "^16.12.0", 36 | "react-monaco-editor": "^0.32.1", 37 | "redux": "^4.0.4", 38 | "redux-thunk": "^2.3.0", 39 | "typescript": "^3.7.3" 40 | }, 41 | "devDependencies": { 42 | "@babel/core": "^7.7.5", 43 | "@storybook/addon-actions": "^5.2.8", 44 | "@storybook/addon-links": "^5.2.8", 45 | "@storybook/addons": "^5.2.8", 46 | "@storybook/react": "^5.2.8", 47 | "babel-loader": "^8.0.6" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/filterReduce.md: -------------------------------------------------------------------------------- 1 | # Filter and Reduce (Tradeoff) 2 | 3 | ## Related Issues 4 | 5 | [Tradeoffs in Control Flow Analysis](https://github.com/Microsoft/TypeScript/issues/9998) 6 | 7 | [Poor type inference for reduce for object](https://github.com/Microsoft/TypeScript/issues/25454) 8 | 9 | ```tsx 10 | function toStrings(arr: object[]): string[] { 11 | return arr.reduce((acc, obj) => { 12 | acc.push(obj.toString()); 13 | return acc; 14 | }, [] as string[]); 15 | } 16 | 17 | // for dictionaries, this is fine 18 | const arr = [1, 2]; 19 | arr.reduce((acc, obj) => { 20 | acc[obj] = true; 21 | return acc; 22 | }, {}); 23 | 24 | // for objects with known properties, you can't initialize the object 25 | // see: build-up-object (type assertions) 26 | interface User { 27 | name: string; 28 | phone: string; 29 | } 30 | const fields = [ 31 | { name: "name", value: "bob" }, 32 | { name: "phone", value: "8005552000" }, 33 | ] as const; 34 | 35 | fields.reduce((acc, field) => { 36 | acc[field.name] = field.value; 37 | return acc; 38 | // error: can't initialize because "name" and "phone" are required 39 | }, {}); 40 | 41 | arr.reduce((acc, obj) => { 42 | // error: No index signature with a parameter of type 'number' was found on type '{}' 43 | acc[obj] = true; 44 | return acc; 45 | }, {}); 46 | 47 | const arrayWithNulls = [1, 2, null]; 48 | // filter is a type guard = needs to explictly know how to remove the union 49 | const stillHasNulls: number[] = arrayWithNulls.filter(x => !!x); 50 | const withoutNulls: number[] = arrayWithNulls.filter( 51 | (x): x is number => x !== null, 52 | ); 53 | 54 | // since this is tedious, and boolean cast wipes out 0 and "" anyway: 55 | const filterNulls = Array.prototype.filter(item => item != null); 56 | const noNulls: number[] = arrayWithNulls.filter(filterNulls); 57 | ``` 58 | -------------------------------------------------------------------------------- /examples/returnValues.md: -------------------------------------------------------------------------------- 1 | # Return Value Types (Concept) 2 | 3 | go over: return values can be inferred, but the more complex 4 | the function, the more you're going to want to specify them. 5 | 6 | TypeScript isn't going to get the return value wrong... 7 | but mistakes in the function will shift the error from 8 | the function (where the bug is) to the rest of the code! 9 | 10 | ```tsx 11 | // Simple functions don't 12 | const add = (x: number, y: number) => { 13 | // return value is number 14 | return x + y; 15 | }; 16 | 17 | const result = add(1, 2); // simple enough 18 | ``` 19 | 20 | ```tsx 21 | interface Person { 22 | firstName: string; 23 | lastName: string; 24 | address?: { 25 | address1?: string; 26 | address2?: string; 27 | }; 28 | } 29 | const selectPerson = (person: Person) => { 30 | return { 31 | firstName: person.firstName, 32 | lastName: person.lastName, 33 | // next line was meant to be "address" 34 | location: person.address 35 | ? `${person.address.address1} ${person.address.address2}` 36 | : "", 37 | }; 38 | }; 39 | // error is now showing up here, but the bug is in the function 40 | selectPerson({ firstName: "Jackson", lastName: "Diamond" }).address; 41 | ``` 42 | 43 | ```tsx 44 | interface Person { 45 | firstName: string; 46 | lastName: string; 47 | address?: { 48 | address1?: string; 49 | address2?: string; 50 | }; 51 | } 52 | interface PersonLocation { 53 | firstName: string; 54 | lastName: string; 55 | address: string; 56 | } 57 | const selectPerson = (person: Person): PersonLocation => { 58 | return { 59 | firstName: person.firstName, 60 | lastName: person.lastName, 61 | // there, now the bug shows up in here instead 62 | location: person.address 63 | ? `${person.address.address1} ${person.address.address2}` 64 | : "", 65 | }; 66 | }; 67 | selectPerson({ firstName: "Jackson", lastName: "Diamond" }).address; 68 | ``` 69 | -------------------------------------------------------------------------------- /examples/generics.md: -------------------------------------------------------------------------------- 1 | Let's say you start out with a functions that works with strings: 2 | 3 | ```tsx 4 | const last = (item: string) => { 5 | return item[item.length - 1]; 6 | }; 7 | 8 | const lastChar = last("things"); // OK! 9 | lastChar.toString(); // also OK 10 | ``` 11 | 12 | After using it for a bit, you find a usage for arrays of numbers: but you realize that the return value is now `string | number` 13 | 14 | ```tsx 15 | const last = (item: string | number[]) => { 16 | return item[item.length - 1]; 17 | }; 18 | 19 | const lastChar = last("things"); // OK! 20 | lastChar.length; // error - this doesn't exist on `number` 21 | ``` 22 | 23 | It would be really nice if TypeScript could figure this stuff out. This is where generics come in. A generic is a type which accepts arguments, just as a function accepts arguments. 24 | 25 | Simple example: 26 | 27 | ```tsx 28 | interface Location { 29 | pathname: string; 30 | query: Query; 31 | } 32 | 33 | const locationWithoutQuery: Location = { 34 | pathname: "/", 35 | query: null, // OK! 36 | }; 37 | 38 | const locationWithQuery: Location<{ id: string }> = { 39 | pathname: "/", 40 | query: { personId: "1" }, // error: query must include "id" 41 | }; 42 | ``` 43 | 44 | Let's use this above: 45 | 46 | ```tsx 47 | type Last = (item: TItem) => TItem; 48 | const last: Last<> = item => { 49 | return item[item.length - 1]; 50 | }; 51 | 52 | const lastChar = last("things"); // OK! 53 | lastChar.toString(); // also OK 54 | ``` 55 | 56 | An important note is that generics aren't a replacement for union types. TS doesn't currently treat a generic function as a union type. 57 | 58 | ```tsx 59 | interface PacketMap { 60 | Foo: { 61 | prop: string; 62 | }; 63 | Bar: number; 64 | } 65 | 66 | type PacketType = keyof PacketMap; 67 | 68 | const encodePacket = (packet: { 69 | type: T; 70 | data: PacketMap[T]; 71 | }) => { 72 | if (packet.type === "Foo") { 73 | return `0${packet.data.prop}`; 74 | } else if (packet.type === "Bar") { 75 | return `1${packet.data}`; 76 | } 77 | }; 78 | ``` 79 | -------------------------------------------------------------------------------- /examples/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as arrayElementAccess } from "./arrayElementAccess.md"; 2 | export { default as boundedTypeParameters } from "./boundedTypeParameters.md"; 3 | export { default as buildUpObject } from "./buildUpObject.md"; 4 | // export { default as definitionFiles } from "./definitionFiles.md"; 5 | // export { default as dependentProperties } from "./dependentProperties.md"; 6 | // export { default as errorCodes } from "./errorCodes.md"; 7 | export { default as excessPropertyChecking } from "./excessPropertyChecking.md"; 8 | export { default as extendingModules } from "./extendingModules.md"; 9 | export { default as filterReduce } from "./filterReduce.md"; 10 | export { default as generics } from "./generics.md"; 11 | export { default as indexSignatures } from "./indexSignatures.md"; 12 | export { default as limitations } from "./limitations.md"; 13 | // export { default as missingDeclarations } from "./missingDeclarations.md"; 14 | // export { default as monacoEditor } from "./monacoEditor.md"; 15 | export { default as never } from "./never.md"; 16 | export { default as objectKeysAndEntries } from "./objectKeysAndEntries.md"; 17 | export { default as objectType } from "./objectType.md"; 18 | // export { default as quickFixes } from "./quickFixes.md"; 19 | // export { default as refactoring } from "./refactoring.md"; 20 | export { default as returnValues } from "./returnValues.md"; 21 | export { default as safePropertyAccess } from "./safePropertyAccess.md"; 22 | // export { default as testingTypes } from "./testingTypes.md"; 23 | export { default as tsxFiles } from "./tsxFiles.md"; 24 | // export { default as typeAssertions } from "./typeAssertions.md"; 25 | export { default as typeGuards } from "./typeGuards.md"; 26 | // export { default as typesWithoutImplementations } from "./typesWithoutImplementations.md"; 27 | export { default as widening } from "./widening.md"; 28 | export { default as voidType } from "./voidType.md"; 29 | // export { default as whatTypesafeMeans } from "./whatTypesafeMeans.md"; 30 | // export { default as whenToBailOut } from "./whenToBailOut.md"; 31 | // export { default as whyTypescript } from "./whyTypescript.md"; 32 | -------------------------------------------------------------------------------- /examples/buildUpObject.md: -------------------------------------------------------------------------------- 1 | # Building Up an Object (Limitation) 2 | 3 | ## Related Issues 4 | 5 | diagnostic codes: ts2339, ts2741 6 | 7 | [Issue #9998: Tradeoffs in Control Flow Analysis](https://github.com/Microsoft/TypeScript/issues/9998) 8 | 9 | ## The Problem 10 | 11 | Sometimes, you need to initialize an object and add properties one by one, often with a check each time. 12 | 13 | ```tsx 14 | interface Result { 15 | name?: string; 16 | value: string | number; 17 | } 18 | 19 | const startWithEmpty = (name?: string) => { 20 | const result = {}; 21 | // error, this property doesn't exist on {} 22 | if (name) { 23 | result.name = name; 24 | } 25 | result.value = 1; 26 | }; 27 | 28 | const addType = (name?: string) => { 29 | const result: Result = {}; // error, missing property "value" 30 | if (name) { 31 | result.name = name; 32 | } 33 | result.value = 1; 34 | }; 35 | 36 | const addAssertion = (name?: string): Result => { 37 | const result = {} as Result; 38 | if (name) { 39 | result.name = name; 40 | } 41 | result.value = 1; // comment out this line: note, no error 42 | return result; 43 | }; 44 | ``` 45 | 46 | ## Why does this happen? 47 | 48 | ## What do I do about it? 49 | 50 | For simple cases, just have two returns. 51 | 52 | ```tsx 53 | buildResult = (name?: string): Result => { 54 | if (name) { 55 | return { name, value: 1 }; 56 | } 57 | return { value: 1 }; 58 | }; 59 | ``` 60 | 61 | ...or redefine a variable. 62 | 63 | ```tsx 64 | buildResult = (name?: string): Result => { 65 | let result: Result; 66 | if (name) { 67 | result = { name, value: 1 }; 68 | } else { 69 | result = { value: 1 }; 70 | } 71 | return result; 72 | }; 73 | ``` 74 | 75 | ...or use a utility. 76 | 77 | ```tsx 78 | // remember keys + entries and open types: 79 | // https://github.com/Microsoft/TypeScript/pull/12253#issuecomment-263132208 80 | const keys = Object.keys as (o: T) => Extract[]; 81 | const entries = Object.entries as < 82 | T extends { [key: string]: any }, 83 | K extends keyof T 84 | >( 85 | o: T, 86 | ) => [keyof T, T[K]][]; 87 | 88 | const filterNulls = ( 89 | obj: T, 90 | ): { [K in keyof T]: T[K] extends any ? T[K] : null } => { 91 | return entries(obj).reduce((acc, [key, value]) => { 92 | // == null matches null or undefined 93 | if (value != null) { 94 | acc[key] = value; 95 | } 96 | return acc; 97 | }, {} as T); 98 | }; 99 | 100 | buildResult = (name?: string): Result => { 101 | return filterNulls({ 102 | name: name, 103 | value: 1, 104 | }); 105 | }; 106 | ``` 107 | -------------------------------------------------------------------------------- /stories/components/Editor.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useRef, useState } from "react"; 2 | import MonacoEditor from "react-monaco-editor"; 3 | import { languages, editor, Uri } from "monaco-editor"; 4 | import esObject from "!raw-loader!typescript/lib/lib.es2017.object.d.ts"; 5 | import es5 from "!raw-loader!typescript/lib/lib.es5.d.ts"; 6 | import csstype from "!raw-loader!csstype/index.d.ts"; 7 | import dom from "!raw-loader!typescript/lib/lib.dom.d.ts"; 8 | import react from "!raw-loader!@types/react/index.d.ts"; 9 | import propTypes from "!raw-loader!@types/prop-types/index.d.ts"; 10 | 11 | languages.typescript.typescriptDefaults.setCompilerOptions({ 12 | // target: languages.typescript.ScriptTarget.ESNext, 13 | allowNonTsExtensions: true, 14 | esModuleInterop: true, 15 | jsx: languages.typescript.JsxEmit.Preserve, 16 | moduleResolution: languages.typescript.ModuleResolutionKind.NodeJs, 17 | module: languages.typescript.ModuleKind.CommonJS, 18 | noEmit: true, 19 | strict: true, 20 | // lib: ["dom", "esnext"], 21 | }); 22 | languages.typescript.typescriptDefaults.addExtraLib( 23 | esObject, 24 | "lib.es2017.object.d.ts", 25 | ); 26 | languages.typescript.typescriptDefaults.addExtraLib(es5, "lib.es5.d.ts"); 27 | languages.typescript.typescriptDefaults.addExtraLib(dom, "lib.dom.d.ts"); 28 | languages.typescript.typescriptDefaults.addExtraLib( 29 | `declare module "react" {${react}}`, 30 | "react.d.ts", 31 | ); 32 | languages.typescript.typescriptDefaults.addExtraLib( 33 | `declare module "csstype" {${csstype}}`, 34 | "csstype.d.ts", 35 | ); 36 | languages.typescript.typescriptDefaults.addExtraLib( 37 | `declare module "prop-types" {${propTypes}}`, 38 | "prop-types.d.ts", 39 | ); 40 | 41 | let id = 0; 42 | const generateModel = (code: string) => { 43 | return editor.createModel(code, "typescript", Uri.file(`input${id++}.tsx`)); 44 | }; 45 | 46 | interface EditorProps { 47 | children: string; 48 | } 49 | export const Editor = memo(({ children }: EditorProps) => { 50 | const model = useRef(generateModel(children)); 51 | const [code, setCode] = useState(children); 52 | const height = code.split(/\r\n|\r|\n/).length * 19 + 1; 53 | 54 | return ( 55 |
62 | setCode(newCode)} 66 | height={height} 67 | width="100%" 68 | language="typescript" 69 | options={{ 70 | model: model.current, 71 | scrollbar: { 72 | vertical: "hidden", 73 | verticalHasArrows: false, 74 | verticalScrollbarSize: 0, 75 | verticalSliderSize: 0, 76 | }, 77 | scrollBeyondLastLine: false, 78 | theme: "vs-dark", 79 | minimap: { 80 | enabled: false, 81 | }, 82 | folding: false, 83 | }} 84 | /> 85 |
86 | ); 87 | }); 88 | -------------------------------------------------------------------------------- /examples/safePropertyAccess.md: -------------------------------------------------------------------------------- 1 | ## Diagnostic Codes 2 | 3 | ts2339, ts2531, ts2532 4 | 5 | ## Safely Accessing Properties on Objects 6 | 7 | ```tsx 8 | declare const get: ( 9 | obj: any, 10 | path: string | number | symbol, 11 | defaultValue?: any, 12 | ) => any; 13 | 14 | // go over: long vs. concise ways of dealing with deeply nested objects with conditionals 15 | interface UnreliableData { 16 | mayExist?: { 17 | deepMayExist?: { 18 | nullableString?: string | null | undefined; 19 | nullableNumber?: number | null | undefined; 20 | nullableArray?: string[] | null | undefined; 21 | nullableFunction?: (name: string) => string | null; 22 | }; 23 | }; 24 | } 25 | 26 | const doStuff = (data?: UnreliableData) => { 27 | // error: any nulls will cause runtime errors 28 | const unsafeNum = data.mayExist.deepMayExist.nullableNumber; 29 | 30 | // this is correct (falsy check on object, type check on primitive), but awful 31 | const num = 32 | (data && 33 | data.mayExist && 34 | data.mayExist.deepMayExist && 35 | typeof data.mayExist.deepMayExist.nullableNumber === "number" && 36 | data.mayExist.deepMayExist.nullableNumber) || 37 | 0; 38 | 39 | // option 1: lodash get (or similar) 40 | // returns "any" and doesn't check any properties 41 | const something = get(data, "mayExist.deepMayExist.nullableNumber", 0); 42 | const notAnError = get(data, "x.y.z", "42"); 43 | 44 | // full type safety and autocompletion 45 | const numberWithDefault = data?.mayExist?.deepMayExist?.nullableNumber ?? 0; 46 | const nullableNumber = data?.mayExist?.deepMayExist?.nullableNumber ?? 0; 47 | const nullableFunction = data?.mayExist?.deepMayExist?.nullableFunction?.(""); 48 | 49 | // this catches properties which don't exist 50 | const intentionalError = data?.mayExist?.fdsafdsavbfd?.nullableNumber ?? 0; 51 | // and the default value needs to match the possible values 52 | // (the error message could be better here) 53 | const badDefault: number = 54 | data?.mayExist?.deepMayExist?.nullableNumber ?? "string"; 55 | }; 56 | ``` 57 | 58 | but note that type narrowing is still necessary for union types since 59 | there's no way for typescript to differentiate between unions 60 | without your help. 61 | 62 | ```tsx 63 | interface StructureOne { 64 | deepMayExist?: { 65 | nullableString?: string | null | undefined; 66 | nullableNumber?: number | null | undefined; 67 | nullableArray?: string[] | null | undefined; 68 | nullableFunction?: ((name: string) => string) | string | null; 69 | }; 70 | } 71 | 72 | interface StructureTwo { 73 | somethingElse?: { 74 | one: string; 75 | two?: number; 76 | }; 77 | } 78 | 79 | interface UnreliableUnionData { 80 | mayExist?: StructureOne | StructureTwo; 81 | } 82 | 83 | const doStuffWithUnions = (data?: UnreliableUnionData) => { 84 | data?.mayExist.somethingElse; // still can't just access properties that only exist on one 85 | 86 | // need to split it up 87 | const structure = data?.mayExist; 88 | if (!!structure && "deepMayExist" in structure) { 89 | structure?.deepMayExist.nullableNumber; 90 | } 91 | // or do a type assertion (unsafe) 92 | (structure as StructureOne)?.deepMayExist.nullableNumber ?? 0; 93 | }; 94 | 95 | const doStuffWithDefaults = (data: UnreliableData) => { 96 | const { deepMayExist } = data.mayExist || {}; 97 | }; 98 | ``` 99 | -------------------------------------------------------------------------------- /examples/widening.md: -------------------------------------------------------------------------------- 1 | # Type Widening (Quirk) 2 | 3 | ## Related Issues 4 | 5 | - https://github.com/Microsoft/TypeScript/issues/10938#issuecomment-247476364 6 | - https://github.com/microsoft/TypeScript/issues/20195 7 | - https://twitter.com/kentcdodds/status/1081333326290415618 8 | 9 | ## Typical Problem 10 | 11 | I've created an object with an exact string as one of its properties, like `"one"`. I took that and sent it to a function which expected the string to be `"one"`, and TypeScript is complaining that `Type 'string' is not assignable to type '"one"'`. 12 | 13 | ```tsx 14 | type Options = { 15 | option: "one" | "two"; 16 | }; 17 | const doStuff = (options: Options) => {}; 18 | 19 | const options = { 20 | option: "one", 21 | }; 22 | doStuff(options); 23 | ``` 24 | 25 | ## Reason 26 | 27 | JavaScript has mutable objects - anything can be changed at any time. Objects defined with `const` are still allowed to have values changed at runtime. Imagine if your code looked like this: 28 | 29 | ```tsx 30 | const mutableOptions: Options = { 31 | option: "one", 32 | }; 33 | // no error, but without widening, this would be: 34 | // Type 'two' is not assignable to type '"one"' 35 | mutableOptions.option = "two"; 36 | ``` 37 | 38 | This would be _extremely annoying_ for any code which modified the object after creation. In JS, there is a lot of code like this. 39 | 40 | ## Fixing This Error 41 | 42 | The easiest way to solve this is to avoid intermediate variables where possible. If TypeScript can figure everything out in a single expression, you won't hit this problem. 43 | 44 | ```tsx 45 | doStuff({ option: "one" }); 46 | ``` 47 | 48 | Of course, it's unrealistic to always put all your code on one line, so you'll still need a strategy when using one object literal in multiple places. 49 | 50 | TypeScript 3.4 added "const assertions", which can mark any property, object, or array as immutable. Adding this will prevent the value from being widened. 51 | 52 | ```tsx 53 | const singleConstOptions = { 54 | option: "one" as const, 55 | }; 56 | // hey, no error! 57 | doStuff(singleConstOptions); 58 | ``` 59 | 60 | You can set an entire object as immutable by adding `as const` to the whole object. 61 | 62 | ```tsx 63 | const constOptions = { 64 | option: "one", 65 | } as const; 66 | // also good 67 | doStuff(constOptions); 68 | ``` 69 | 70 | If you already know the type in advance, you can also just annotate with that. Here, we already know what `Options` should be, so let's use that! 71 | 72 | ```tsx 73 | const annotatedOptions: Options = { 74 | option: "one", 75 | }; 76 | // also good 77 | doStuff(annotatedOptions); 78 | ``` 79 | 80 | ## Gotchas 81 | 82 | Arrays are special. 83 | 84 | The TypeScript team is opposed to major breaking changes. This results in some strange tradeoffs. If an array is marked as `readonly`, you can't send a non-`readonly` array into it. 85 | 86 | ```tsx 87 | type ImmutableStrings = readonly string[]; 88 | 89 | const mutateStrings = (strings: string[]) => {}; 90 | 91 | const dontMutateStrings = (strings: readonly string[]) => {}; 92 | 93 | declare const immutableStrings: ImmutableStrings; 94 | declare const mutableStrings: string[]; 95 | 96 | // can't send a readonly array into anything accepting non-readonly 97 | mutateStrings(immutableStrings); 98 | // non-readonly into non-readonly is fine 99 | mutateStrings(mutableStrings); 100 | // readonly and non-readonly are fine into readonly 101 | dontMutateStrings(immutableStrings); 102 | dontMutateStrings(mutableStrings); 103 | ``` 104 | 105 | This does _not_ apply to properties on objects which are not arrays or tuples, despite that the same risk applies here. This is because the scale of the breaking change was much worse with properties when compared to arrays or tuples. See: https://github.com/Microsoft/TypeScript/pull/6532#issuecomment-174356151 106 | 107 | ```tsx 108 | type ImmutableObject = { 109 | readonly name: string; 110 | }; 111 | type MutableObject = { 112 | name: string; 113 | }; 114 | declare const immutableObject: ImmutableObject; 115 | declare const mutableObject: MutableObject; 116 | const mutateObject = (obj: MutableObject) => { 117 | obj.name = "something else!"; 118 | }; 119 | 120 | mutateObject(mutableObject); 121 | // surprise! readonly constraint breaks here 122 | mutateObject(immutableObject); 123 | ``` 124 | -------------------------------------------------------------------------------- /examples/excessPropertyChecking.md: -------------------------------------------------------------------------------- 1 | # Excess Property Checking (Feature) 2 | 3 | Excess Property Checking (abbreviated to EPC from here) is a system which detects when extra properties are added to objects when it's likely a mistake, while still allowing most object to contain extra properties for flexibility. 4 | 5 | At its best, this allows you to reuse the same function with very different inputs, as you define only the subset of data you need from an object. Most functions don't have a problem accepting additional properties on objects. 6 | 7 | At its worst, this is confusing or even unsafe at runtime. 8 | 9 | Knowing how this works is very important. 10 | 11 | ## Open Types 12 | 13 | All types are "open" or "inexact" in TypeScript. There currently are no 14 | "closed" or "exact" types. 15 | 16 | Here, the extra `age` property is not an error: 17 | 18 | ```tsx 19 | const getName = (person: { name: string }) => { 20 | return person.name; 21 | }; 22 | 23 | const person = { 24 | name: "Rock Rockman", 25 | age: "27", // extra property 26 | }; 27 | getName(person); // no error 28 | ``` 29 | 30 | In many cases, this is an advantage - a function specifies what it wants from an object, which allows a single object type to work with a range of possible data. This prevents us from needing to narrow union types: 31 | 32 | ```tsx 33 | const person1 = { 34 | name: "Morgan Stryker", 35 | active: true, 36 | }; 37 | const person2 = { 38 | name: "Jackson Blacklock", 39 | phone: "818-555-2000", 40 | }; 41 | getName(person1); 42 | getName(person2); 43 | ``` 44 | 45 | However, problems happen with "options objects", common in JS: 46 | 47 | ```tsx 48 | interface Options { 49 | firstName?: boolean; 50 | lastName?: boolean; 51 | } 52 | const getSplitName = (person: { name: string }, options: Options = {}) => { 53 | const [firstName, lastName] = person.name.split(" "); 54 | return { 55 | firstName: options.firstName ? firstName : null, 56 | lastName: options.lastName ? lastName : null, 57 | name: person.name, 58 | }; 59 | }; 60 | 61 | const options = { 62 | firstName: true, 63 | // "last" isn't a property, but no error is reported 64 | last: true, 65 | }; 66 | getSplitName(person, options); 67 | ``` 68 | 69 | To prevent this from happening, TypeScript will report errors on extra properties if the object is sent directly into a function/JSX element without being assigned to a variable: 70 | 71 | ```tsx 72 | getSplitName(person, { firstName: true, last: true }); // error! 73 | ``` 74 | 75 | ## Rules of EPC 76 | 77 | From the [pull request](https://github.com/Microsoft/TypeScript/pull/3823): 78 | 79 | - Every object literal is initially considered "fresh". 80 | - When a fresh object literal is assigned to a variable or passed for a parameter of a non-empty target type, it is an error for the object literal to specify properties that don't exist in the target type. 81 | - Freshness disappears in a type assertion or when the type of an object literal is widened. 82 | 83 | If you tend to assign single-use variables before use, you may not get the benefits of EPC automatically. Where possible, it's safer and less verbose to inline options objects (or React component props) instead of assigning to a variable first. 84 | 85 | However, there are times where it makes sense, such as passing the same options / props into multiple functions or components. You can opt-in to EPC here by defining a type on the object at the time of assignment: 86 | 87 | ```tsx 88 | const nameOptions: Options = { 89 | firstName: true, 90 | last: true, // this is now checked 91 | }; 92 | getSplitName(person, nameOptions); 93 | ``` 94 | 95 | ## With Libraries 96 | 97 | When using a library, a couple approaches may work. Most libraries will export type definitions in addition to normal exports. 98 | 99 | ```tsx 100 | import { debounce, DebounceSettings } from "lodash"; 101 | 102 | const settings: DebounceSettings = { 103 | maxWait: 30, 104 | lead: 30, 105 | }; 106 | debounce(() => {}, 30, settings); 107 | ``` 108 | 109 | If they don't, you can still extract specific argument types using the `Parameters<>` utility. Note that this is a last resort. For overloaded functions, the last signature defined is used. This can change from under you without warning! 110 | 111 | ```tsx 112 | type DebounceOptions = Parameters; 113 | const debounceSettings: DebounceOptions[2] = { 114 | maxWait: 4, 115 | lead: 30, 116 | }; 117 | 118 | interface Overloaded { 119 | (x: number, y: number): string; 120 | (x: string): number; 121 | } 122 | 123 | interface ReversedOverloaded { 124 | (x: string): number; 125 | (x: number, y: number): string; 126 | } 127 | 128 | const param1: Parameters[0] = 1; 129 | const param2: Parameters[0] = "string"; 130 | const reversedParam1: Parameters[0] = 1; 131 | const reversedParam2: Parameters[0] = "string"; 132 | ``` 133 | -------------------------------------------------------------------------------- /examples/indexSignatures.md: -------------------------------------------------------------------------------- 1 | # No Index Signature Found (code ts-7053) 2 | 3 | When first trying to access a property by variable, such as `object[key]`, you're likely to hit a `No Index Signature` error when trying to work with `string`: 4 | 5 | ```tsx 6 | export {}; 7 | interface Person { 8 | name: string; 9 | phone: string; 10 | } 11 | const person: Person = { 12 | name: "Rock Rockman", 13 | phone: "800-555-2400", 14 | }; 15 | 16 | const get = (person: Person, key: string) => { 17 | return person[key]; 18 | }; 19 | 20 | get(person, "name"); 21 | ``` 22 | 23 | This error is useful if you're familiar with index signatures, but can misleading if you're not. 24 | 25 | ## What is an index signature? 26 | 27 | An index signature is a way of telling TypeScript "trust me, if you ask for something on this object, it will give you a specific type." 28 | 29 | Here's an example: 30 | 31 | ```tsx 32 | export {}; 33 | interface TileMap { 34 | [key: string]: { item: string }; 35 | } 36 | const tileMap: TileMap = {}; 37 | ``` 38 | 39 | These can be used alongside other keys: 40 | 41 | ```tsx 42 | export {}; 43 | interface CSSProperties { 44 | color: string; 45 | [key: string]: string; 46 | } 47 | ``` 48 | 49 | ## Why an index signature isn't the solution here 50 | 51 | Let's try correcting the error by adding an index signature to our `Person` type. 52 | 53 | ```tsx 54 | export {}; 55 | interface Person { 56 | name: string; 57 | phone: string; 58 | [key: string]: string; 59 | } 60 | const person: Person = { 61 | name: "Big McLargeHuge", 62 | phone: "800-555-2400", 63 | }; 64 | 65 | const get = (person: Person, key: string) => { 66 | return person[key]; 67 | }; 68 | 69 | get(person, "name"); 70 | ``` 71 | 72 | Hey, that worked! Looking a little closer, though... we have a problem. Now we can ask for things that don't really exist. 73 | 74 | ```tsx 75 | export {}; 76 | interface Person { 77 | name: string; 78 | phone: string; 79 | [key: string]: string; 80 | } 81 | const person: Person = { 82 | name: "Big McLargeHuge", 83 | phone: "800-555-2400", 84 | }; 85 | 86 | const get = (person: Person, key: string) => { 87 | return person[key]; 88 | }; 89 | 90 | get(person, "name"); 91 | get(person, "phone"); 92 | // what about other things? 93 | get(person, "pants"); 94 | ``` 95 | 96 | So we haven't really fixed the errors, we've just suppressed them. Instead, we need to look at the signature of `get` - specially, the `key`. 97 | 98 | ## Be More Specific 99 | 100 | We originally typed `key` as `string`. This isn't really what we want, though, as it allows any string - `a`, `pants`, or the entire text of the [complete works of Shakespeare](https://ocw.mit.edu/ans7870/6/6.006/s08/lecturenotes/files/t8.shakespeare.txt). 101 | 102 | Instead, we want to limit it just to known keys of the object. These keys are more specific than `string` - in this case, the only valid keys are `name` or `phone`. Let's start with a union type: 103 | 104 | ```tsx 105 | export {}; 106 | interface Person { 107 | name: string; 108 | phone: string; 109 | } 110 | const person: Person = { 111 | name: "Big McLargeHuge", 112 | phone: "800-555-2400", 113 | }; 114 | 115 | const get = (person: Person, key: "name" | "phone") => { 116 | return person[key]; 117 | }; 118 | 119 | get(person, "name"); 120 | get(person, "phone"); 121 | // good, now we get errors here 122 | get(person, "pants"); 123 | ``` 124 | 125 | That's better. Still, we don't want to repeat this every time. Luckily, there's a `keyof` keyword that does what we're looking for: 126 | 127 | ```tsx 128 | export {}; 129 | interface Person { 130 | name: string; 131 | phone: string; 132 | } 133 | const person: Person = { 134 | name: "Big McLargeHuge", 135 | phone: "800-555-2400", 136 | }; 137 | 138 | // Hover over `key` to see the reduced type: "name" | "phone" 139 | const get = (person: Person, key: keyof Person) => { 140 | return person[key]; 141 | }; 142 | 143 | get(person, "name"); 144 | get(person, "phone"); 145 | get(person, "pants"); 146 | ``` 147 | 148 | Great! So the problem wasn't really the lack of an index signature; our type just wasn't specific enough. 149 | 150 | Index signatures are very useful when you understand their tradeoffs, but can lead to runtime errors and poor results from refactoring tools when misused. Here are a few cases where you might want to use one: 151 | 152 | 1. You don't control an object (it comes from a backend, for instance), and there's no way to know the keys ahead of time. An example: a list of enabled/disabled features, by key/value: 153 | 154 | ```tsx 155 | export {}; 156 | interface FeatureFlags { 157 | [key: string]: boolean | undefined; 158 | } 159 | ``` 160 | 161 | 2. You control an object, but the keys aren't known until runtime. 162 | 163 | In this example, we might have an object of tiles in a game, where the keys correspond to `x, y` coordinates: 164 | 165 | ```tsx 166 | export {}; 167 | const tileMap = { 168 | "1,1": { 169 | item: "player", 170 | }, 171 | "2,2": { 172 | item: "monster", 173 | }, 174 | }; 175 | ``` 176 | 177 | Since the player and monster move around, we couldn't know the structure of this object in advance. So, the index signature ensures we know the type of the value of an object, even if we can't know its keys. 178 | 179 | 3. You're writing to an object that will never be read by your code, and it can contain any keys. Here's a tracking call to an analytics library: 180 | 181 | ```tsx 182 | interface TrackingData { 183 | [key: string]: string | null | undefined; 184 | } 185 | declare global { 186 | interface Window { 187 | analytics: { 188 | trackEvent: (data: TrackingData) => void; 189 | }; 190 | } 191 | } 192 | export {}; 193 | const track = (data: TrackingData) => { 194 | return window.analytics.trackEvent(data); 195 | }; 196 | ``` 197 | 198 | ## Additional Reading 199 | 200 | [TypeScript Deep Dive: Index Signatures](https://basarat.gitbook.io/typescript/type-system/index-signatures) 201 | -------------------------------------------------------------------------------- /examples/objectKeysAndEntries.md: -------------------------------------------------------------------------------- 1 | # Object.keys, Object.entries, Object.fromEntries (Tradeoff) 2 | 3 | ## How are Object.keys and Object.entries typed? 4 | 5 | `Object.keys` returns `string[]`. `Object.entries` returns `[string, Value]` (where `Value` is a union of all possible values). This works fine most of the time. However, problems happens when you're trying to transfer values from object to another by key: 6 | 7 | ```tsx 8 | export {}; 9 | 10 | type FormState = { 11 | name: string; 12 | email: string; 13 | termsAccepted: boolean; 14 | }; 15 | type FieldValidation = { 16 | name: boolean; 17 | email: string; 18 | termsAccepted: boolean; 19 | }; 20 | 21 | declare const formState: FormState; 22 | declare const fieldValidation: FieldValidation; 23 | const valid = Object.keys(formState).forEach(key => { 24 | fieldValidation[key] = !!formState[key]; 25 | }); 26 | ``` 27 | 28 | This error is explained at [Index Signatures](/?path=/story/examples--index-signatures) 29 | 30 | ## Why is `key` typed as `string` instead of `"name" | "age" | "termsAccepted"`? 31 | 32 | Types in TypeScript are allowed to have extra properties. See: [Excess Property Checking](/?path=/story/examples--excess-property-checking). 33 | 34 | With this in mind, typing `key` as only the _known_ keys can cause really unexpected results since you'll also be working with all the unknown ones. Here, we'll take in a `name` and `email` and use a type assertion to force the type to be `keyof FormState`: 35 | 36 | ```tsx 37 | export {}; 38 | 39 | type FormState = { 40 | name: string; 41 | email: string; 42 | }; 43 | type FieldValidation = { 44 | name: boolean; 45 | email: boolean; 46 | authenticated: boolean; 47 | }; 48 | 49 | declare const fieldValidation: FieldValidation; 50 | 51 | const userForm = { 52 | name: "Bob Johnson", 53 | email: "bobjohnson@example.com", 54 | authenticated: true, // extra properties are allowed 55 | }; 56 | const validate = (formState: FormState) => { 57 | Object.keys(formState).forEach(keyArg => { 58 | const key = keyArg as keyof FormState; 59 | // We've unexpectedly overwritten authenticated to be `true` here 60 | fieldValidation[key] = !!formState[key]; 61 | }); 62 | }; 63 | validate(userForm); 64 | ``` 65 | 66 | We've just validated someone as `authenticated` when they shouldn't be. This type of bug can be very difficult to track down. 67 | 68 | ## How likely is this to be a problem? 69 | 70 | This is completely dependent on your project. Look at where you're using `Object.keys` or `Object.entries` and try to think of ways you could end up in bad situations if extra properties are present. 71 | 72 | ## How can I avoid this? 73 | 74 | 1. Consider if you should really be using an array instead of an object. This only works if you control the structure of the data, but arrays were made for iteration and preserving order. 75 | 76 | 2. Reconsider whether iteration is even valuable. The above example written without `Object.keys` avoids any type or runtime issues: 77 | 78 | ```tsx 79 | export {}; 80 | 81 | type FormState = { 82 | name: string; 83 | email: string; 84 | }; 85 | type FieldValidation = { 86 | name: boolean; 87 | email: boolean; 88 | authenticated: boolean; 89 | }; 90 | 91 | declare const fieldValidation: FieldValidation; 92 | 93 | const userForm = { 94 | name: "Bob Johnson", 95 | email: "bobjohnson@example.com", 96 | authenticated: true, // extra properties are OK here 97 | }; 98 | const validate = (formState: FormState) => { 99 | fieldValidation.name = !!formState.name; 100 | fieldValidation.email = !!formState.email; 101 | }; 102 | validate(userForm); 103 | ``` 104 | 105 | 3. Accept `{ [key: string]: unknown }` in functions, which will force some extra checks for values, especially `null` and `undefined`. 106 | 107 | ## OK, I understand the risks. What if I want to just accept them and move on? 108 | 109 | To opt in to riskier but more convenient types case-by-case: 110 | 111 | ```tsx 112 | export {}; 113 | 114 | const exactKeys = Object.keys as (o: T) => Extract[]; 115 | const exactEntries = Object.entries as < 116 | T extends { [key: string]: any }, 117 | K extends keyof T 118 | >( 119 | o: T, 120 | ) => [keyof T, T[K]][]; 121 | type ValueOf = T[keyof T]; 122 | const exactValues = Object.values as ( 123 | o: T, 124 | ) => ValueOf[]; 125 | ``` 126 | 127 | To overwrite built-in definitions, making this behavior always apply: 128 | 129 | ``` 130 | type ValueOf = T[keyof T]; 131 | 132 | declare global { 133 | interface ObjectConstructor { 134 | // keys: (o: T) => Array>; 135 | keys(o: T): Array>; 136 | entries( 137 | o: T, 138 | ): Array<[Extract, T[K]]>; 139 | values( 140 | o: T, 141 | ): ValueOf[] 142 | } 143 | } 144 | ``` 145 | 146 | ## Object.values 147 | 148 | `Object.values` implicitly returns `any` when used on an interface or type without an index signature. 149 | 150 | ```tsx 151 | interface Person { 152 | name: string; 153 | } 154 | const stringifyValues = (person: Person) => { 155 | Object.values(person).map(value => value.toString()); 156 | }; 157 | const person = { 158 | name: "Blast Hardpeck", 159 | authenticated: null, 160 | }; 161 | stringifyValues(person); // crash on null.toString(); 162 | ``` 163 | 164 | ## Object.fromEntries 165 | 166 | The current definition for `Object.fromEntries` has an open issue at [#35745](https://github.com/microsoft/TypeScript/issues/35745). To opt in to more specific types when possible, add this to a `.d.ts` file in your project: 167 | 168 | ``` 169 | type UnionToIntersection = (T extends T 170 | ? (p: T) => void 171 | : never) extends (p: infer U) => void 172 | ? U 173 | : never; 174 | type FromEntries = T extends T 175 | ? Record 176 | : never; 177 | type Flatten = {} & { 178 | [P in keyof T]: T[P]; 179 | }; 180 | 181 | declare global { 182 | interface ObjectConstructor { 183 | fromEntries< 184 | V extends PropertyKey, 185 | T extends [readonly [V, any]] | Array 186 | >( 187 | entries: T, 188 | ): Flatten>>; 189 | } 190 | } 191 | ``` 192 | 193 | `Object.fromEntries` works with tuples, so you'll likely need to use `as const` to mark your return value as a tuple rather than an array. 194 | 195 | ```tsx 196 | export {}; 197 | 198 | declare const people: Array<{ name: string; age: number }>; 199 | const ageMap = Object.fromEntries( 200 | people.map(person => { 201 | return [person.name, person.age] as const; 202 | }), 203 | ); 204 | ``` 205 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The TypeScript Error Guide 2 | 3 | TypeScript makes some normally complex tasks very easy, but it can make some seemingly-simple tasks more difficult, 4 | 5 | ## Some things that were hard: now easy! 6 | 7 | Change an API? Change the type and you'll see everywhere you need to update. Need to rename something used in 500 places? Hit F2 and rename. 8 | 9 | ## Some things that were easy: now hard. 10 | 11 | TypeScript actively discourages highly-dynamic code, redefining variables at will, and APIs that put a lot of meaning into strings. What do these look like? 12 | 13 | ```tsx 14 | const sizes = { 15 | "size-sm": 1, 16 | "size-md": 2, 17 | "size-lg": 3, 18 | }; 19 | 20 | const getSize = (size: "sm" | "md" | "lg") => { 21 | return sizes[`size-${size}`]; 22 | }; 23 | ``` 24 | 25 | This site is all about letting you know about these limitations _without_ all the type-system jargon you'll normally see. 26 | 27 | # Diagnostic Codes 28 | 29 | TypeScript has a code for every error, such as `ts(2741)`. While the error message may look very different each time, the underlying problem is the same. 30 | 31 | If you're hitting a problem, search for that code within this page. 32 | 33 | # For Site / Application Developers 34 | 35 | ## I'm trying to start with an empty object and add properties one by one, and TypeScript gives me errors no matter what I do. I've given up and `// @ts-ignore`d them. 36 | 37 | When you create an empty object, there's just no good way for TS to ensure that all properties have been added _after_ the object has already been specified. Don't worry, though - there are lots of ways to accomplish this, and one of them is very concise as well! See: [Build Up Object](examples/build-up-object.ts). 38 | 39 | Diagnostic codes: `ts(2339)`, `ts(2741)` 40 | 41 | ## What's this `never` type? 42 | 43 | This is covered in the new handbook under the [never type](https://microsoft.github.io/TypeScript-New-Handbook/chapters/more-on-functions/#never). 44 | 45 | Additionally, arrays start out as `never[]` when the type cannot be inferred. See: [Never](examples/never.ts) 46 | 47 | ## Why isn't TypeScript showing errors on extra properties I have on an object? 48 | 49 | TypeScript has a system called "excess property checking" which is a great tradeoff between type safety and developer experience... provided you know how it works. Luckily, it's pretty simple. See: [Excess Property Checking](examples/excess-property-checking.ts). 50 | 51 | ## How can I get a function to accept an object with a couple extra properties? My code works fine! 52 | 53 | Same question as above! Understanding [Excess Property Checking](examples/excess-property-checking.ts) will let you intentionally bypass this check where needed. 54 | 55 | ## I used `filter` but TS errors and says that something could be null/undefined. 56 | 57 | A nullable type is a union type, such as `string | null`. To narrow types in a function like `filter`, an extra annotation is needed for now (TypeScript might make an exception here in the future). See: [Filter and Reduce](examples/filter-reduce.ts). 58 | 59 | ## Why can't I use `reduce` without errors on the result object? 60 | 61 | You can! However, creating an empty object and building it up one property at a time isn't something that can really ever be checked for safety. A simple type assertion is typically all you need. A few different approaches are available at [Filter and Reduce](examples/filter-reduce.ts). 62 | 63 | ## I have an array of mixed object types. Why can't I narrow based on whether or not a property exists? 64 | 65 | You can! You just have to use an `in` check to opt in to less-strict checking of properties. See the tradeoffs and other solutions in [Type Guards](examples/type-guards.ts). 66 | 67 | ## I checked if something is null, and now TypeScript is complaining that something might be null. 68 | 69 | This is usually an example of TS being especially careful with callbacks, as they may or may not be called synchronously. See: [Type Widening](examples/type-widening.ts). 70 | 71 | ## Is it OK to have an object function argument with a bunch of optional properties? 72 | 73 | Maybe! If all the options really are independently optional, that's fine. However, you may have a couple properties which are _exclusive_ of one another. Think of a function that must accept either "a" or "b", but cannot accept both at once. If this covers your case, [Dependent Properties](examples/dependent-properties.ts) can help you. 74 | 75 | ## I'm using a library, and I seem to need to manually type everything for it. 76 | 77 | This can be one of a few issues. 78 | 79 | If the library you're using involves two functions that quietly pass information between each other between imports (example: redux `createStore()` and react-redux `connect()` or `useSelector()`), you might be interested in the [Extending Modules](examples/extending-modules.ts) section. This can let you define types _once_ instead of on every usage. 80 | 81 | If this isn't the case, your library is likely set up to accept generics. These are like function arguments, but for types. The syntax for these involves angle brackets (`<>`) and can be tricky, so see [Generics](examples/generics.ts) for details. 82 | 83 | Finally, some libraries just don't have very good type definitions available, or are so dynamic that creating good library definitions isn't possible, or require types which TypeScript just can't support yet. This is rare, but an example is the [humps](https://github.com/domchristie/humps) library which converts all keys of an object from snake_case to camelCase. For these instances, you might want to use a [Type Assertion](examples/type-assertion.ts). This ties into knowing [when to bail out of types](examples/when-to-bail-out.ts). 84 | 85 | ## My function accepts string | number as a type and returns string | number. I know I gave it a string but now I have to check for numbers every time. 86 | 87 | You're looking for [Generics](examples/generics.ts). These allow you to change the return value based on the arguments. Think of them like function arguments, but for types. 88 | 89 | ## Hey! I used a generic like you said, and now I'm getting type errors inside my function. 90 | 91 | Right! Generics accept anything by default, so your `add()` function suddenly needs to accept arrays, objects, and `null`. You might want to restrict which types you accept using [Bounded Type Parameters](examples/bounded-type-parameters.ts). 92 | 93 | ## I'm using Object.keys() or Object.entries() and there's some index signature error. Why is the key ending up as a `string`? 94 | 95 | This one trips up everyone at some point. There's an explanation at [Object.keys and Object.entries](examples/object-keys-and-entries.ts). Don't worry, there's a reason for it - and a few ways to opt-in to the behavior you want. 96 | 97 | ## I don't control the object being used. How do I use the `object` type? 98 | 99 | ## How can I tell TypeScript about a global variable that exists without using @ts-ignore or `any`? 100 | 101 | You're looking for the jargony `ambient context` and `ambient declaration`. When you put `declare` in front of a function, constant, or module, you are saying "trust me, this exists, and is of this type." This is especially confusing when looking at the differences between .d.ts and .ts files, so see [Types without implementations](examples/types-without-implementations.ts). 102 | 103 | # React-Specific Errors 104 | 105 | For most React errors, there's an excellect [cheat sheet](https://github.com/typescript-cheatsheets/react-typescript-cheatsheet) for using React with TypeScript. This is updated constantly with new information. 106 | 107 | ## How do I work with `children`? All I get are errors trying to access anything. 108 | 109 | A React child is union of many, many possible things which cannot be narrowed except by complex type guards at every object depth. When iterating over children, it's probably best to use `any` and distrust the existence of any property: 110 | 111 | ```tsx 112 | const containsDiv = React.Children.toArray(children).some((child: any) => { 113 | return child?.type?.displayName === "Header"; 114 | }); 115 | ``` 116 | 117 | ## I'm getting a "not a constructor function for JSX elements" error. 118 | 119 | You're returning something from a function component which React doesn't accept, likely `undefined`. Your code might be fine now, but if it ever hits that `undefined` case, you'll get a runtime error. 120 | 121 | ```tsx 122 | import React from "react"; 123 | 124 | interface Props { 125 | text?: string; 126 | } 127 | const Button = ({ text }: Props) => { 128 | return text && ; 129 | }; 130 | ``` 131 | 132 | If `text` isn't supplied to Button, it'll return the current value of `text` (which is `undefined`). React currently throws in this case. 133 | 134 | To get a better error message, add a return type annotation to your function component of either `ReactElement` or `ReactElement | null`. This will move the error to the return statement: 135 | 136 | ```tsx 137 | interface Props { 138 | text?: string; 139 | } 140 | const Button = ({ text }: Props): ReactElement | null => { 141 | return text ? : null; 142 | }; 143 | ``` 144 | 145 | # For Library Authors 146 | 147 | ## I want to make my library work with TypeScript. What's all this .d.ts nonsense? 148 | 149 | d.ts files are interesting. They allow you to declare the external API for your library without needing to specify 150 | 151 | This can be challenging. 152 | 153 | ## How do I test types? 154 | 155 | If you're writing library definitions, [Testing Types](examples/testing-types.ts) can help you, by using the [Conditional Type Checks](https://github.com/dsherret/conditional-type-checks) library. 156 | --------------------------------------------------------------------------------