Repository: jkchao/typescript-book-chinese Branch: master Commit: f1e40c10e31b Files: 122 Total size: 334.3 KB Directory structure: gitextract_e5a6gj6_/ ├── .all-contributorsrc ├── .gitattributes ├── .github/ │ └── workflows/ │ └── gh-pages.yml ├── .gitignore ├── .huskyrc.js ├── .prettierrc ├── LICENSE ├── README.md ├── commitlint.config.js ├── docs/ │ ├── .vuepress/ │ │ ├── config.js │ │ ├── public/ │ │ │ └── manifest.json │ │ └── theme/ │ │ ├── components/ │ │ │ ├── Ads.vue │ │ │ ├── AlgoliaSearchBox.vue │ │ │ ├── DropdownLink.vue │ │ │ ├── DropdownTransition.vue │ │ │ ├── Home.vue │ │ │ ├── NavLink.vue │ │ │ ├── NavLinks.vue │ │ │ ├── Navbar.vue │ │ │ ├── Page.vue │ │ │ ├── Sidebar.vue │ │ │ ├── SidebarButton.vue │ │ │ ├── SidebarGroup.vue │ │ │ ├── SidebarLink.vue │ │ │ └── SidebarLinks.vue │ │ ├── global-components/ │ │ │ └── Badge.vue │ │ ├── index.js │ │ ├── layouts/ │ │ │ ├── 404.vue │ │ │ └── Layout.vue │ │ ├── styles/ │ │ │ ├── arrow.styl │ │ │ ├── code.styl │ │ │ ├── custom-blocks.styl │ │ │ ├── mobile.styl │ │ │ ├── theme.styl │ │ │ ├── toc.styl │ │ │ └── wrapper.styl │ │ └── util/ │ │ └── index.js │ ├── README.md │ ├── compiler/ │ │ ├── ast.md │ │ ├── binder.md │ │ ├── checker.md │ │ ├── emitter.md │ │ ├── overview.md │ │ ├── parser.md │ │ ├── program.md │ │ └── scanner.md │ ├── error/ │ │ ├── common.md │ │ └── interpreting.md │ ├── faqs/ │ │ ├── class.md │ │ ├── commandline-behavior.md │ │ ├── comments.md │ │ ├── common-bug-not-bugs.md │ │ ├── common-feature-request.md │ │ ├── decorators.md │ │ ├── enums.md │ │ ├── function.md │ │ ├── generics.md │ │ ├── glossary-and-terms.md │ │ ├── jsx-and-react.md │ │ ├── modules.md │ │ ├── thing-that-dont-work.md │ │ ├── tsconfig-behavior.md │ │ ├── type-guards.md │ │ └── type-system-behavior.md │ ├── jsx/ │ │ ├── nonReactJSX.md │ │ ├── reactJSX.md │ │ └── support.md │ ├── new/ │ │ ├── typescript-3.7.md │ │ ├── typescript-3.8.md │ │ └── typescript-3.9.md │ ├── project/ │ │ ├── compilationContext.md │ │ ├── declarationspaces.md │ │ ├── dynamicImportExpressions.md │ │ ├── modules.md │ │ └── namespaces.md │ ├── tips/ │ │ ├── avoidExportDefault.md │ │ ├── barrel.md │ │ ├── bind.md │ │ ├── buildToggles.md │ │ ├── classAreUseful.md │ │ ├── covarianceAndContravariance.md │ │ ├── createArrays.md │ │ ├── curry.md │ │ ├── functionParameters.md │ │ ├── infer.md │ │ ├── lazyObjectLiteralInitialization.md │ │ ├── limitPropertySetters.md │ │ ├── metadata.md │ │ ├── nominalTyping.md │ │ ├── outFileCaution.md │ │ ├── singletonPatern.md │ │ ├── statefulFunctions.md │ │ ├── staticConstructors.md │ │ ├── stringBasedEmuns.md │ │ ├── truthy.md │ │ ├── typeInstantiation.md │ │ └── typesafeEventEmitter.md │ └── typings/ │ ├── ambient.md │ ├── callable.md │ ├── discrominatedUnion.md │ ├── enums.md │ ├── exceptionsHanding.md │ ├── freshness.md │ ├── functions.md │ ├── generices.md │ ├── indexSignatures.md │ ├── interfaces.md │ ├── lib.md │ ├── literals.md │ ├── migrating.md │ ├── mixins.md │ ├── movingTypes.md │ ├── neverType.md │ ├── overview.md │ ├── readonly.md │ ├── thisType.md │ ├── typeAssertion.md │ ├── typeCompatibility.md │ ├── typeGuard.md │ ├── typeInference.md │ └── types.md └── package.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .all-contributorsrc ================================================ { "projectName": "typescript-book-chinese", "projectOwner": "jkchao", "repoType": "github", "repoHost": "https://github.com", "files": [ "README.md" ], "imageSize": 64, "commit": false, "contributors": [ { "login": "jkchao", "name": "三毛", "avatar_url": "https://avatars3.githubusercontent.com/u/22933931?v=4", "profile": "https://jkchao.cn", "contributions": [ "doc" ] }, { "login": "KnorienChang", "name": "KnorienChang", "avatar_url": "https://avatars3.githubusercontent.com/u/22536347?v=4", "profile": "https://github.com/KnorienChang", "contributions": [ "doc" ] }, { "login": "magic-akari", "name": "阿卡琳", "avatar_url": "https://avatars0.githubusercontent.com/u/7829098?v=4", "profile": "https://github.com/magic-akari", "contributions": [ "doc" ] }, { "login": "hopalay", "name": "hopalay", "avatar_url": "https://avatars1.githubusercontent.com/u/2362335?v=4", "profile": "https://github.com/hopalay", "contributions": [ "doc" ] }, { "login": "S1ngS1ng", "name": "Xing Liu", "avatar_url": "https://avatars1.githubusercontent.com/u/13592559?v=4", "profile": "http://singsing.io/blog", "contributions": [ "doc" ] }, { "login": "chenwangji", "name": "chenwangji", "avatar_url": "https://avatars1.githubusercontent.com/u/23144765?v=4", "profile": "https://github.com/chenwangji", "contributions": [ "doc" ] }, { "login": "helloforrestworld", "name": "老农爱盐碱地", "avatar_url": "https://avatars3.githubusercontent.com/u/28107509?v=4", "profile": "https://github.com/helloforrestworld", "contributions": [ "doc" ] }, { "login": "itxuye", "name": "Necros", "avatar_url": "https://avatars2.githubusercontent.com/u/9589686?v=4", "profile": "http://www.itxuye.com", "contributions": [ "doc" ] }, { "login": "XuToTo", "name": "Xu Jihan", "avatar_url": "https://avatars1.githubusercontent.com/u/8039013?v=4", "profile": "https://xutoto.im", "contributions": [ "doc" ] }, { "login": "Yiiu", "name": "Yu", "avatar_url": "https://avatars0.githubusercontent.com/u/7876498?v=4", "profile": "https://wanan.me/", "contributions": [ "doc" ] }, { "login": "YogaLin", "name": "Yoga Lin", "avatar_url": "https://avatars0.githubusercontent.com/u/11386122?v=4", "profile": "https://github.com/YogaLin", "contributions": [ "doc" ] }, { "login": "galenjiang", "name": "G", "avatar_url": "https://avatars2.githubusercontent.com/u/12699258?v=4", "profile": "http://galenjiang.github.io", "contributions": [ "doc" ] }, { "login": "Undrum", "name": "Undrum", "avatar_url": "https://avatars3.githubusercontent.com/u/37131109?v=4", "profile": "https://github.com/Undrum", "contributions": [ "doc" ] }, { "login": "zongzi531", "name": "Zong", "avatar_url": "https://avatars2.githubusercontent.com/u/22429236?v=4", "profile": "https://zongzi531.com", "contributions": [ "doc" ] }, { "login": "LXVC", "name": "LXVC", "avatar_url": "https://avatars0.githubusercontent.com/u/12185175?v=4", "profile": "http://lxvc.github.io", "contributions": [ "doc" ] }, { "login": "jinrichardJIN", "name": "Whale", "avatar_url": "https://avatars0.githubusercontent.com/u/20592013?v=4", "profile": "https://github.com/jinrichardJIN", "contributions": [ "doc" ] }, { "login": "getdaydream", "name": "getdaydream", "avatar_url": "https://avatars2.githubusercontent.com/u/23274794?v=4", "profile": "https://github.com/getdaydream", "contributions": [ "doc" ] }, { "login": "onlyling", "name": "Ling ZiQing", "avatar_url": "https://avatars3.githubusercontent.com/u/9999765?v=4", "profile": "https://www.onlyling.com", "contributions": [ "doc" ] }, { "login": "izayl", "name": "izayl", "avatar_url": "https://avatars0.githubusercontent.com/u/10740043?v=4", "profile": "https://github.com/izayl", "contributions": [ "doc" ] }, { "login": "shenzekun", "name": "Moorez", "avatar_url": "https://avatars1.githubusercontent.com/u/21151080?v=4", "profile": "http://shenzekun.cn/", "contributions": [ "doc" ] }, { "login": "yepbug", "name": "萤火之未", "avatar_url": "https://avatars3.githubusercontent.com/u/26736658?v=4", "profile": "https://github.com/yepbug", "contributions": [ "doc" ] }, { "login": "xfields", "name": "xfields", "avatar_url": "https://avatars1.githubusercontent.com/u/6301070?v=4", "profile": "https://github.com/xfields", "contributions": [ "doc" ] }, { "login": "ZhaZhengRefn", "name": "ZhaZheng", "avatar_url": "https://avatars0.githubusercontent.com/u/16488686?v=4", "profile": "https://segmentfault.com/u/zhazhengrefn", "contributions": [ "doc" ] }, { "login": "dickeylth", "name": "弘树@阿里", "avatar_url": "https://avatars1.githubusercontent.com/u/2196373?v=4", "profile": "http://webminer.js.org", "contributions": [ "doc" ] }, { "login": "dalphyx", "name": "wangjingchao", "avatar_url": "https://avatars2.githubusercontent.com/u/1576121?v=4", "profile": "https://github.com/dalphyx", "contributions": [ "doc" ] }, { "login": "IWANABETHATGUY", "name": "IWANABETHATGUY", "avatar_url": "https://avatars1.githubusercontent.com/u/17974631?v=4", "profile": "https://github.com/IWANABETHATGUY", "contributions": [ "doc" ] }, { "login": "PaytonTang", "name": "Payton Tang", "avatar_url": "https://avatars3.githubusercontent.com/u/19408042?v=4", "profile": "http://www.pcdeng.com", "contributions": [ "doc" ] }, { "login": "Rem486", "name": "Rem486", "avatar_url": "https://avatars3.githubusercontent.com/u/22462424?v=4", "profile": "https://github.com/Rem486", "contributions": [ "doc" ] }, { "login": "BuptStEve", "name": "Steve Young", "avatar_url": "https://avatars2.githubusercontent.com/u/11501493?v=4", "profile": "https://buptsteve.github.io", "contributions": [ "doc" ] }, { "login": "olivewind", "name": "olive.wang", "avatar_url": "https://avatars0.githubusercontent.com/u/17901361?v=4", "profile": "http://olivewind.com", "contributions": [ "doc" ] }, { "login": "geekrainy", "name": "Rainy", "avatar_url": "https://avatars1.githubusercontent.com/u/7333266?v=4", "profile": "https://rainylog.com", "contributions": [ "doc" ] }, { "login": "daskyrk", "name": "随风", "avatar_url": "https://avatars2.githubusercontent.com/u/3955437?v=4", "profile": "https://github.com/daskyrk", "contributions": [ "doc" ] }, { "login": "JustClear", "name": "大板栗", "avatar_url": "https://avatars2.githubusercontent.com/u/7371867?v=4", "profile": "https://justclear.github.io/", "contributions": [ "doc" ] }, { "login": "superman66", "name": "Superman", "avatar_url": "https://avatars0.githubusercontent.com/u/12592949?v=4", "profile": "https://github.com/superman66", "contributions": [ "doc" ] }, { "login": "PaytonDeng", "name": "Payton Deng", "avatar_url": "https://avatars2.githubusercontent.com/u/18691865?v=4", "profile": "https://github.com/PaytonDeng", "contributions": [ "doc" ] }, { "login": "hgrourou", "name": "Wang Kang", "avatar_url": "https://avatars1.githubusercontent.com/u/11886447?v=4", "profile": "https://github.com/hgrourou", "contributions": [ "doc" ] }, { "login": "w1301625107", "name": "wuchouchou", "avatar_url": "https://avatars2.githubusercontent.com/u/29950066?v=4", "profile": "https://github.com/w1301625107", "contributions": [ "doc" ] }, { "login": "EastblueOkay", "name": "EastblueOkay", "avatar_url": "https://avatars3.githubusercontent.com/u/13050584?v=4", "profile": "https://github.com/EastblueOkay", "contributions": [ "doc" ] }, { "login": "xybin1990", "name": "Nic", "avatar_url": "https://avatars2.githubusercontent.com/u/9530270?v=4", "profile": "https://github.com/xybin1990", "contributions": [ "doc" ] }, { "login": "CQBoyBrand", "name": "重庆崽儿Brand", "avatar_url": "https://avatars2.githubusercontent.com/u/20539402?v=4", "profile": "http://www.brandhuang.com", "contributions": [ "doc" ] }, { "login": "YanYuanFE", "name": "YanYuan", "avatar_url": "https://avatars2.githubusercontent.com/u/17774285?v=4", "profile": "http://yanyuanfe.cn", "contributions": [ "doc" ] }, { "login": "JunaYa", "name": "JunaYa", "avatar_url": "https://avatars0.githubusercontent.com/u/13215737?v=4", "profile": "http://junaya.github.io/", "contributions": [ "doc" ] }, { "login": "licong96", "name": "黎聪", "avatar_url": "https://avatars1.githubusercontent.com/u/28621732?v=4", "profile": "https://github.com/licong96", "contributions": [ "doc" ] }, { "login": "a1055794033", "name": "WANGXUEFENG", "avatar_url": "https://avatars0.githubusercontent.com/u/33458200?v=4", "profile": "https://github.com/a1055794033", "contributions": [ "doc" ] }, { "login": "hansnow", "name": "Xiaohan Li", "avatar_url": "https://avatars2.githubusercontent.com/u/4365899?v=4", "profile": "https://hansnow.me", "contributions": [ "doc" ] }, { "login": "poyiding", "name": "dingkang", "avatar_url": "https://avatars3.githubusercontent.com/u/15643321?v=4", "profile": "https://yuque.com/zhifei", "contributions": [ "doc" ] }, { "login": "zhangciwu", "name": "zhangciwu", "avatar_url": "https://avatars2.githubusercontent.com/u/3340597?v=4", "profile": "http://zcw.me", "contributions": [ "doc" ] }, { "login": "sea-ice", "name": "Jack", "avatar_url": "https://avatars3.githubusercontent.com/u/25540882?v=4", "profile": "http://www.seaiceblog.com", "contributions": [ "doc" ] }, { "login": "masterZSH", "name": "masterZSH", "avatar_url": "https://avatars3.githubusercontent.com/u/27674875?v=4", "profile": "https://github.com/masterZSH", "contributions": [ "doc" ] }, { "login": "Eve-1995", "name": "Eve", "avatar_url": "https://avatars1.githubusercontent.com/u/30228406?v=4", "profile": "http://angular.ink", "contributions": [ "doc" ] }, { "login": "Xuemuyang", "name": "Xuemuyang", "avatar_url": "https://avatars0.githubusercontent.com/u/25718776?v=4", "profile": "http://myoungxue.top", "contributions": [ "doc" ] }, { "login": "Jasery", "name": "Jasery", "avatar_url": "https://avatars2.githubusercontent.com/u/20091279?v=4", "profile": "https://github.com/Jasery", "contributions": [ "doc" ] }, { "login": "lmislm", "name": "Baskerville*", "avatar_url": "https://avatars1.githubusercontent.com/u/21683339?v=4", "profile": "https://lmislm.com", "contributions": [ "doc" ] }, { "login": "FishPlusOrange", "name": "FishPlusOrange", "avatar_url": "https://avatars0.githubusercontent.com/u/22132265?v=4", "profile": "https://github.com/FishPlusOrange", "contributions": [ "doc" ] }, { "login": "CodeDaraW", "name": "月迷津渡", "avatar_url": "https://avatars2.githubusercontent.com/u/12277082?v=4", "profile": "https://blog.daraw.cn/", "contributions": [ "doc" ] }, { "login": "evinma", "name": "evinma", "avatar_url": "https://avatars2.githubusercontent.com/u/16096567?v=4", "profile": "https://github.com/evinma", "contributions": [ "doc" ] }, { "login": "suyanhanx", "name": "Suyan", "avatar_url": "https://avatars3.githubusercontent.com/u/24221472?v=4", "profile": "http://suyan.moe", "contributions": [ "doc" ] }, { "login": "luckymore", "name": "cherry-man", "avatar_url": "https://avatars0.githubusercontent.com/u/5390013?v=4", "profile": "https://github.com/luckymore", "contributions": [ "doc" ] }, { "login": "Cainankun", "name": "蔡南坤", "avatar_url": "https://avatars3.githubusercontent.com/u/21698272?v=4", "profile": "https://github.com/Cainankun", "contributions": [ "doc" ] }, { "login": "cWatermelon", "name": "chenc", "avatar_url": "https://avatars0.githubusercontent.com/u/16097887?v=4", "profile": "https://github.com/cWatermelon", "contributions": [ "doc" ] }, { "login": "lxx2013", "name": "Xinxing Li", "avatar_url": "https://avatars3.githubusercontent.com/u/20529542?v=4", "profile": "https://setsuna.wang", "contributions": [ "doc" ] }, { "login": "BryanAdamss", "name": "GuangHui", "avatar_url": "https://avatars3.githubusercontent.com/u/7441504?v=4", "profile": "https://bryanadamss.github.io/", "contributions": [ "doc" ] }, { "login": "CosSalt", "name": "odzcdut", "avatar_url": "https://avatars3.githubusercontent.com/u/36765589?v=4", "profile": "https://www.hosalt.cn/", "contributions": [ "doc" ] }, { "login": "xhwgood", "name": "项鸿伟", "avatar_url": "https://avatars2.githubusercontent.com/u/39004078?v=4", "profile": "https://github.com/xhwgood", "contributions": [ "doc" ] }, { "login": "xie-zhichao", "name": "xiezhichao", "avatar_url": "https://avatars3.githubusercontent.com/u/15844643?v=4", "profile": "https://github.com/xie-zhichao", "contributions": [ "doc" ] }, { "login": "wqcstrong", "name": "Charles", "avatar_url": "https://avatars1.githubusercontent.com/u/23474513?v=4", "profile": "http://www.geeecko.com", "contributions": [ "doc" ] }, { "login": "Jexxie", "name": "Jexxie", "avatar_url": "https://avatars1.githubusercontent.com/u/31770433?v=4", "profile": "https://twitter.com/Jexxie_woo", "contributions": [ "doc" ] }, { "login": "SeanWangx", "name": "Sean Wong", "avatar_url": "https://avatars3.githubusercontent.com/u/16217316?v=4", "profile": "https://github.com/SeanWangx", "contributions": [ "doc" ] }, { "login": "bluelovers", "name": "bluelovers", "avatar_url": "https://avatars0.githubusercontent.com/u/167966?v=4", "profile": "http://bluelovers.net", "contributions": [ "doc" ] }, { "login": "celery8911", "name": "Celery", "avatar_url": "https://avatars2.githubusercontent.com/u/24710064?v=4", "profile": "https://github.com/celery8911", "contributions": [ "doc" ] }, { "login": "chenxiaochun", "name": "chenxiaochun", "avatar_url": "https://avatars3.githubusercontent.com/u/1744713?v=4", "profile": "https://github.com/chenxiaochun", "contributions": [ "doc" ] }, { "login": "DuYueYu", "name": "Yates", "avatar_url": "https://avatars2.githubusercontent.com/u/41691152?v=4", "profile": "https://github.com/DuYueYu", "contributions": [ "doc" ] }, { "login": "IGoRFonin", "name": "IGoR", "avatar_url": "https://avatars2.githubusercontent.com/u/10962426?v=4", "profile": "https://github.com/IGoRFonin", "contributions": [ "doc" ] }, { "login": "byog", "name": "byog", "avatar_url": "https://avatars0.githubusercontent.com/u/7764115?v=4", "profile": "https://github.com/byog", "contributions": [ "doc" ] }, { "login": "whinc", "name": "whincwu", "avatar_url": "https://avatars2.githubusercontent.com/u/5096493?v=4", "profile": "https://github.com/whinc/blog", "contributions": [ "doc" ] }, { "login": "frontdog", "name": "康东扬", "avatar_url": "https://avatars2.githubusercontent.com/u/13433074?v=4", "profile": "https://github.com/frontdog", "contributions": [ "doc" ] }, { "login": "Kennytian", "name": "Kenny", "avatar_url": "https://avatars3.githubusercontent.com/u/2621619?v=4", "profile": "https://www.jianshu.com/u/f4907e8670cb", "contributions": [ "infra" ] }, { "login": "cangSDARM", "name": "AllenLee", "avatar_url": "https://avatars0.githubusercontent.com/u/20441896?v=4", "profile": "https://www.douban.com/people/driving555/", "contributions": [ "doc" ] }, { "login": "xiangming25", "name": "xiangming25", "avatar_url": "https://avatars3.githubusercontent.com/u/7913751?v=4", "profile": "https://github.com/xiangming25", "contributions": [ "doc" ] }, { "login": "vinzid", "name": "Chanvin Xiao", "avatar_url": "https://avatars1.githubusercontent.com/u/18076739?v=4", "profile": "https://chanvinxiao.com", "contributions": [ "doc" ] }, { "login": "Aaron00101010", "name": "Aaron Xie", "avatar_url": "https://avatars0.githubusercontent.com/u/25996236?v=4", "profile": "http://www.noobcoder.club", "contributions": [ "doc" ] }, { "login": "nulIptr", "name": "nulIptr", "avatar_url": "https://avatars1.githubusercontent.com/u/7925831?v=4", "profile": "https://github.com/nulIptr", "contributions": [ "doc" ] }, { "login": "htoooth", "name": "Tao Huang", "avatar_url": "https://avatars3.githubusercontent.com/u/1717023?v=4", "profile": "http://www.cnblogs.com/htoooth/", "contributions": [ "doc" ] }, { "login": "Yunfly", "name": "Yunfly", "avatar_url": "https://avatars1.githubusercontent.com/u/19400116?v=4", "profile": "https://github.com/Yunfly", "contributions": [ "doc" ] }, { "login": "willww64", "name": "Will Wang", "avatar_url": "https://avatars2.githubusercontent.com/u/9930358?v=4", "profile": "https://github.com/willww64", "contributions": [ "doc" ] }, { "login": "SyMind", "name": "SyMind", "avatar_url": "https://avatars1.githubusercontent.com/u/19852293?v=4", "profile": "https://github.com/SyMind", "contributions": [ "doc" ] }, { "login": "yuhengshen", "name": "yuhengshen", "avatar_url": "https://avatars2.githubusercontent.com/u/29867660?v=4", "profile": "https://github.com/yuhengshen", "contributions": [ "doc" ] }, { "login": "ProfBramble", "name": "chenfeng", "avatar_url": "https://avatars0.githubusercontent.com/u/48615696?v=4", "profile": "https://github.com/ProfBramble", "contributions": [ "doc" ] } ] } ================================================ FILE: .gitattributes ================================================ *.js linguist-language=TypeScript ================================================ FILE: .github/workflows/gh-pages.yml ================================================ name: github pages on: pull_request: types: [closed] branches: - master push: branches: - master jobs: build-deploy: runs-on: ubuntu-18.04 steps: - name: Checkout uses: actions/checkout@master with: ref: master - name: Install Dependencies run: yarn - name: Build run: yarn build - name: Deploy uses: peaceiris/actions-gh-pages@v2 env: ACTIONS_DEPLOY_KEY: ${{ secrets.ACTIONS_DEPLOY_KEY }} PUBLISH_BRANCH: gh-pages PUBLISH_DIR: ./docs/.vuepress/dist ================================================ FILE: .gitignore ================================================ .DS_Store node_modules/ dist npm-debug.log* yarn-debug.log* yarn-error.log* # Editor directories and files .idea .vscode .vs *.suo *.ntvs* *.njsproj *.sln # Others .history ================================================ FILE: .huskyrc.js ================================================ module.exports = { hooks: { 'commit-msg': 'commitlint -E HUSKY_GIT_PARAMS' // 'pre-commit': 'lint-staged' } }; ================================================ FILE: .prettierrc ================================================ { "printWidth": 120, "semi": true, "singleQuote": true, "trailingComma": "none", "bracketSpacing": true, "jsxBracketSameLine": false, "arrowParens": "avoid", "requirePragma": false, "proseWrap": "preserve" } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018 三毛 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # 深入理解 TypeScript [![All Contributors](https://img.shields.io/badge/all_contributors-88-orange.svg?style=flat-square)](#contributors) [![GitHub forks](https://img.shields.io/github/forks/jkchao/typescript-book-chinese.svg?style=flat-square)](https://github.com/jkchao/typescript-book-chinese/network) [![GitHub stars](https://img.shields.io/github/stars/jkchao/typescript-book-chinese.svg?style=flat-square)](https://github.com/jkchao/typescript-book-chinese/stargazers) [![GitHub issues](https://img.shields.io/github/issues/jkchao/typescript-book-chinese.svg?style=flat-square)](https://github.com/jkchao/typescript-book-chinese/issues) [![GitHub last commit](https://img.shields.io/github/last-commit/jkchao/typescript-book-chinese.svg?style=flat-square)](https://github.com/jkchao/typescript-book-chinese/commits/master) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) 此书是 [《TypeScript Deep Dive》](https://github.com/basarat/typescript-book/) 的中文翻译版,感谢作者 [Basarat](https://github.com/basarat) 的付出。 如果你喜欢纸质书籍,可以通过[京东](https://item.jd.com/12755624.html)或者[当当](http://product.m.dangdang.com/28487648.html?t=1574581821),来购买此书。 你可以通过订阅该[公众号](https://cdn.jkchao.cn/nuxt/img/14958af.jpg),来获取更多有趣的内容。 ## Why ![downloads](https://github.com/jkchao/typescript-book-chinese/raw/master/docs/.vuepress/public/typescript-downloads.jpg) > 数据来源:[npm 包下载量](https://npm-stat.com/charts.html?package=typescript&from=2016-01-01&to=2018-07-31) 如你所见,TypeScript 发展至今,已经成为大型项目的标配,其提供的静态类型系统,大大增强了代码的可读性以及可维护性;同时,它提供最新和不断发展的 JavaScript 特性,能让我们建立更健壮的组件。 [《TypeScript Deep Dive》](https://github.com/basarat/typescript-book/) 是一本很好的开源书,从基础到深入,很全面的阐述了 TypeScript 的各种魔法,不管你是新手,还是老鸟,它都将适应你。此外,它不同于 TypeScript 官方给出的文档(当然 TypeScript 给出的文档是很好的),在此书中,结合实际应用下的场景用例,你将能更深入的理解 TypeScript。 如今社区已经存在部分翻译,但都似乎已经停止更新。 于是在某天的某个冲动之下,这个 RP 就诞生了。 ## 翻译内容 《TypeScript Deep Dive》 书中包含一部分 JavaScript Future 和一些其他的内容,在这里,我们并不打算翻译它,如果你有兴趣,可以查看原书中 [JavaScript Future](https://basarat.gitbooks.io/typescript/content/docs/future-javascript.html) 的有关章节。 由于 TypeScript 更新频繁,除了上文中提到翻译部分,将会加入 TypeScript 的 Release,同时我也将总结出工作中一些有意思的点,希望和大家相互学习,一起进步。 此外,在不违背原作者本意前提下,为了更直观的表达,部分内容将采用意译,而非直译。 ## Contributors Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)): | [三毛
三毛](https://jkchao.cn)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=jkchao "Documentation") | [KnorienChang
KnorienChang](https://github.com/KnorienChang)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=KnorienChang "Documentation") | [阿卡琳
阿卡琳](https://github.com/magic-akari)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=magic-akari "Documentation") | [hopalay
hopalay](https://github.com/hopalay)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=hopalay "Documentation") | [Xing Liu
Xing Liu](http://singsing.io/blog)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=S1ngS1ng "Documentation") | [chenwangji
chenwangji](https://github.com/chenwangji)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=chenwangji "Documentation") | [老农爱盐碱地
老农爱盐碱地](https://github.com/helloforrestworld)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=helloforrestworld "Documentation") | | :---: | :---: | :---: | :---: | :---: | :---: | :---: | | [Necros
Necros](http://www.itxuye.com)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=itxuye "Documentation") | [Xu Jihan
Xu Jihan](https://xutoto.im)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=XuToTo "Documentation") | [Yu
Yu](https://wanan.me/)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=Yiiu "Documentation") | [Yoga Lin
Yoga Lin](https://github.com/YogaLin)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=YogaLin "Documentation") | [G
G](http://galenjiang.github.io)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=galenjiang "Documentation") | [Undrum
Undrum](https://github.com/Undrum)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=Undrum "Documentation") | [Zong
Zong](https://zongzi531.com)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=zongzi531 "Documentation") | | [LXVC
LXVC](http://lxvc.github.io)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=LXVC "Documentation") | [Whale
Whale](https://github.com/jinrichardJIN)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=jinrichardJIN "Documentation") | [getdaydream
getdaydream](https://github.com/getdaydream)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=getdaydream "Documentation") | [Ling ZiQing
Ling ZiQing](https://www.onlyling.com)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=onlyling "Documentation") | [izayl
izayl](https://github.com/izayl)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=izayl "Documentation") | [Moorez
Moorez](http://shenzekun.cn/)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=shenzekun "Documentation") | [萤火之未
萤火之未](https://github.com/yepbug)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=yepbug "Documentation") | | [xfields
xfields](https://github.com/xfields)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=xfields "Documentation") | [ZhaZheng
ZhaZheng](https://segmentfault.com/u/zhazhengrefn)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=ZhaZhengRefn "Documentation") | [弘树@阿里
弘树@阿里](http://webminer.js.org)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=dickeylth "Documentation") | [wangjingchao
wangjingchao](https://github.com/dalphyx)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=dalphyx "Documentation") | [IWANABETHATGUY
IWANABETHATGUY](https://github.com/IWANABETHATGUY)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=IWANABETHATGUY "Documentation") | [Payton Tang
Payton Tang](http://www.pcdeng.com)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=PaytonTang "Documentation") | [Rem486
Rem486](https://github.com/Rem486)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=Rem486 "Documentation") | | [Steve Young
Steve Young](https://buptsteve.github.io)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=BuptStEve "Documentation") | [olive.wang
olive.wang](http://olivewind.com)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=olivewind "Documentation") | [Rainy
Rainy](https://rainylog.com)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=geekrainy "Documentation") | [随风
随风](https://github.com/daskyrk)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=daskyrk "Documentation") | [大板栗
大板栗](https://justclear.github.io/)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=JustClear "Documentation") | [Superman
Superman](https://github.com/superman66)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=superman66 "Documentation") | [Payton Deng
Payton Deng](https://github.com/PaytonDeng)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=PaytonDeng "Documentation") | | [Wang Kang
Wang Kang](https://github.com/hgrourou)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=hgrourou "Documentation") | [wuchouchou
wuchouchou](https://github.com/w1301625107)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=w1301625107 "Documentation") | [EastblueOkay
EastblueOkay](https://github.com/EastblueOkay)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=EastblueOkay "Documentation") | [Nic
Nic](https://github.com/xybin1990)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=xybin1990 "Documentation") | [重庆崽儿Brand
重庆崽儿Brand](http://www.brandhuang.com)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=CQBoyBrand "Documentation") | [YanYuan
YanYuan](http://yanyuanfe.cn)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=YanYuanFE "Documentation") | [JunaYa
JunaYa](http://junaya.github.io/)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=JunaYa "Documentation") | | [黎聪
黎聪](https://github.com/licong96)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=licong96 "Documentation") | [WANGXUEFENG
WANGXUEFENG](https://github.com/a1055794033)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=a1055794033 "Documentation") | [Xiaohan Li
Xiaohan Li](https://hansnow.me)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=hansnow "Documentation") | [dingkang
dingkang](https://yuque.com/zhifei)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=poyiding "Documentation") | [zhangciwu
zhangciwu](http://zcw.me)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=zhangciwu "Documentation") | [Jack
Jack](http://www.seaiceblog.com)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=sea-ice "Documentation") | [masterZSH
masterZSH](https://github.com/masterZSH)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=masterZSH "Documentation") | | [Eve
Eve](http://angular.ink)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=Eve-1995 "Documentation") | [Xuemuyang
Xuemuyang](http://myoungxue.top)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=Xuemuyang "Documentation") | [Jasery
Jasery](https://github.com/Jasery)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=Jasery "Documentation") | [Baskerville*
Baskerville*](https://lmislm.com)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=lmislm "Documentation") | [FishPlusOrange
FishPlusOrange](https://github.com/FishPlusOrange)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=FishPlusOrange "Documentation") | [月迷津渡
月迷津渡](https://blog.daraw.cn/)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=CodeDaraW "Documentation") | [evinma
evinma](https://github.com/evinma)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=evinma "Documentation") | | [Suyan
Suyan](http://suyan.moe)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=suyanhanx "Documentation") | [cherry-man
cherry-man](https://github.com/luckymore)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=luckymore "Documentation") | [蔡南坤
蔡南坤](https://github.com/Cainankun)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=Cainankun "Documentation") | [chenc
chenc](https://github.com/cWatermelon)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=cWatermelon "Documentation") | [Xinxing Li
Xinxing Li](https://setsuna.wang)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=lxx2013 "Documentation") | [GuangHui
GuangHui](https://bryanadamss.github.io/)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=BryanAdamss "Documentation") | [odzcdut
odzcdut](https://www.hosalt.cn/)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=CosSalt "Documentation") | | [项鸿伟
项鸿伟](https://github.com/xhwgood)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=xhwgood "Documentation") | [xiezhichao
xiezhichao](https://github.com/xie-zhichao)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=xie-zhichao "Documentation") | [Charles
Charles](http://www.geeecko.com)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=wqcstrong "Documentation") | [Jexxie
Jexxie](https://twitter.com/Jexxie_woo)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=Jexxie "Documentation") | [Sean Wong
Sean Wong](https://github.com/SeanWangx)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=SeanWangx "Documentation") | [bluelovers
bluelovers](http://bluelovers.net)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=bluelovers "Documentation") | [Celery
Celery](https://github.com/celery8911)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=celery8911 "Documentation") | | [chenxiaochun
chenxiaochun](https://github.com/chenxiaochun)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=chenxiaochun "Documentation") | [Yates
Yates](https://github.com/DuYueYu)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=DuYueYu "Documentation") | [IGoR
IGoR](https://github.com/IGoRFonin)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=IGoRFonin "Documentation") | [byog
byog](https://github.com/byog)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=byog "Documentation") | [whincwu
whincwu](https://github.com/whinc/blog)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=whinc "Documentation") | [康东扬
康东扬](https://github.com/frontdog)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=frontdog "Documentation") | [Kenny
Kenny](https://www.jianshu.com/u/f4907e8670cb)
[🚇](#infra-Kennytian "Infrastructure (Hosting, Build-Tools, etc)") | | [AllenLee
AllenLee](https://www.douban.com/people/driving555/)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=cangSDARM "Documentation") | [xiangming25
xiangming25](https://github.com/xiangming25)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=xiangming25 "Documentation") | [Chanvin Xiao
Chanvin Xiao](https://chanvinxiao.com)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=vinzid "Documentation") | [Aaron Xie
Aaron Xie](http://www.noobcoder.club)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=Aaron00101010 "Documentation") | [nulIptr
nulIptr](https://github.com/nulIptr)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=nulIptr "Documentation") | [Tao Huang
Tao Huang](http://www.cnblogs.com/htoooth/)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=htoooth "Documentation") | [Yunfly
Yunfly](https://github.com/Yunfly)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=Yunfly "Documentation") | | [Will Wang
Will Wang](https://github.com/willww64)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=willww64 "Documentation") | [SyMind
SyMind](https://github.com/SyMind)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=SyMind "Documentation") | [yuhengshen
yuhengshen](https://github.com/yuhengshen)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=yuhengshen "Documentation") | [chenfeng
chenfeng](https://github.com/ProfBramble)
[📖](https://github.com/jkchao/typescript-book-chinese/commits?author=ProfBramble "Documentation") | ## How to contribute 你可以: - 通过 PR 修改错别字,或者错误的格式; - 发 issue 讨论文章中出现的一些不合理地方; - 翻译 TODO 文件夹下的文章,并顺手 Email 我。 希望你在翻译或者 PR 之前,阅读[中文文章排版指北](https://github.com/mzlogin/chinese-copywriting-guidelines)。 ### 公众号 ## 最后 如果你和我一样对 TypeScript 充满兴趣,可以订阅(star)本项目,及时收到有关于此项目的更新。 如果你对文章有任何疑问,欢迎提交 [issues](https://github.com/jkchao/typescript-book-chinese/issues) 和我交流。 如果你认为有些地方翻译不够准确,或者你想补充一些文中没提到但是非常有意思的知识点,欢迎 [PR](https://github.com/jkchao/typescript-book-chinese/pulls)。 ================================================ FILE: commitlint.config.js ================================================ const types = ['build', 'ci', 'docs', 'feat', 'fix', 'perf', 'refactor', 'revert', 'style', 'test', 'chore', 'update']; module.exports = { // https://github.com/conventional-changelog/commitlint/blob/master/%40commitlint/config-angular/README.md extends: ['@commitlint/config-angular'], rules: { 'type-enum': [2, 'always', types] } }; ================================================ FILE: docs/.vuepress/config.js ================================================ module.exports = { base: '/typescript-book-chinese/', title: '深入理解 TypeScript', description: 'TypeScript Deep Dive 中文版', head: [ ['link', { rel: 'icon', href: `/logo.png` }], ['link', { rel: 'manifest', href: '/manifest.json' }], ['meta', { name: 'theme-color', content: '#3eaf7c' }], ['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }], ['meta', { name: 'apple-mobile-web-app-status-bar-style', content: 'black' }], ['meta', { name: 'msapplication-TileColor', content: '#000000' }] ], plugins: [ // '@vuepress/pwa', '@vuepress/back-to-top', [ '@vuepress/google-analytics', { ga: 'UA-106861408-1' // UA-00000000-0 } ] ], // theme: [], themeConfig: { repo: 'jkchao/typescript-book-chinese', docsDir: 'docs', editLinks: true, editLinkText: '编辑此页', activeHeaderLinks: false, sidebarDepth: 3, lastUpdated: '上次更新', // algolia: { // apiKey: 'fd0efd57c48824ceb1bcfa9690dba5b0', // indexName: 'jkchao_typescript' // }, adsConfig: [ { title: '关注公众号', src: '/typescript-book-chinese/qrcode.jpg' }, { title: '与我交流', src: '/typescript-book-chinese/contact.png' }, { title: '购买此书', src: 'https://static.jkchao.cn/2019-11-22/WechatIMG719.png?imageMogr2/auto-orient/thumbnail/80x/blur/1x0/quality/75|imageslim', url: 'https://item.jd.com/12755624.html' } ], nav: [ { text: '原书链接', link: 'https://basarat.gitbooks.io/typescript/content/docs/getting-started.html' }, { text: 'Blog', link: 'https://jkchao.cn' } ], sidebar: [ { title: '写在前面', collapsable: false, children: ['/'] }, { title: 'TypeScript 项目', collapsable: false, children: [ '/project/compilationContext', '/project/declarationspaces', '/project/modules', '/project/namespaces', '/project/dynamicImportExpressions' ] }, { title: 'TypeScript 类型系统', collapsable: false, children: [ '/typings/overview', '/typings/migrating', '/typings/types', '/typings/ambient', '/typings/interfaces', '/typings/enums', '/typings/lib', '/typings/functions', '/typings/callable', '/typings/typeAssertion', '/typings/freshness', '/typings/typeGuard', '/typings/literals', '/typings/readonly', '/typings/generices', '/typings/typeInference', '/typings/typeCompatibility', '/typings/neverType', '/typings/discrominatedUnion', '/typings/indexSignatures', '/typings/movingTypes', '/typings/exceptionsHanding', '/typings/mixins', '/typings/thisType' ] }, { title: 'JSX', collapsable: false, children: ['/jsx/support', '/jsx/reactJSX', '/jsx/nonReactJSX'] }, { title: 'TypeScript 错误提示', collapsable: false, children: ['/error/interpreting', '/error/common'] }, { title: 'TIPs', collapsable: false, children: [ '/tips/stringBasedEmuns', '/tips/nominalTyping', '/tips/statefulFunctions', '/tips/bind', '/tips/curry', '/tips/typeInstantiation', '/tips/lazyObjectLiteralInitialization', '/tips/classAreUseful', '/tips/avoidExportDefault', '/tips/limitPropertySetters', '/tips/createArrays', '/tips/outFileCaution', '/tips/staticConstructors', '/tips/singletonPatern', '/tips/functionParameters', '/tips/truthy', '/tips/buildToggles', '/tips/typesafeEventEmitter', '/tips/metadata', '/tips/covarianceAndContravariance', '/tips/infer' ] }, { title: 'TypeScript 编译原理', collapsable: false, children: [ '/compiler/overview', '/compiler/program', '/compiler/ast', '/compiler/scanner', '/compiler/parser', '/compiler/binder', '/compiler/checker', '/compiler/emitter' ] }, { title: 'TypeScript FAQs', collapsable: false, children: [ './faqs/common-bug-not-bugs', './faqs/common-feature-request', './faqs/type-system-behavior', './faqs/function', './faqs/class', './faqs/generics', './faqs/modules', './faqs/enums', './faqs/type-guards', './faqs/jsx-and-react', './faqs/thing-that-dont-work', './faqs/commandline-behavior', './faqs/tsconfig-behavior' ] }, { title: 'TypeScript 更新', collapsable: false, children: ['/new/typescript-3.9', '/new/typescript-3.8', '/new/typescript-3.7'] } // { // title: 'TypeScript 更新', // collapsable: false, // children: ['/new/typescript-3.7'] // } ] } }; ================================================ FILE: docs/.vuepress/public/manifest.json ================================================ { "name": "TypeScriptBook", "short_name": "TypeScriptBook", "icons": [ { "src": "/icons/android-chrome-36x36.png", "sizes": "36x36", "type": "image/png", "density": 0.75 }, { "src": "/icons/android-chrome-48x48.png", "sizes": "48x48", "type": "image/png", "density": 1 }, { "src": "/icons/android-chrome-72x72.png", "sizes": "72x72", "type": "image/png", "density": 1.5 }, { "src": "/icons/android-chrome-96x96.png", "sizes": "96x96", "type": "image/png", "density": 2 }, { "src": "/icons/android-chrome-144x144.png", "sizes": "144x144", "type": "image/png", "density": 3 }, { "src": "/icons/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png", "density": 4 } ], "start_url": "/typescript-book-chinese/", "display": "standalone", "background_color": "#fff", "theme_color": "#3eaf7c" } ================================================ FILE: docs/.vuepress/theme/components/Ads.vue ================================================ ================================================ FILE: docs/.vuepress/theme/components/AlgoliaSearchBox.vue ================================================ ================================================ FILE: docs/.vuepress/theme/components/DropdownLink.vue ================================================ ================================================ FILE: docs/.vuepress/theme/components/DropdownTransition.vue ================================================ ================================================ FILE: docs/.vuepress/theme/components/Home.vue ================================================ ================================================ FILE: docs/.vuepress/theme/components/NavLink.vue ================================================ ================================================ FILE: docs/.vuepress/theme/components/NavLinks.vue ================================================ ================================================ FILE: docs/.vuepress/theme/components/Navbar.vue ================================================ ================================================ FILE: docs/.vuepress/theme/components/Page.vue ================================================ ================================================ FILE: docs/.vuepress/theme/components/Sidebar.vue ================================================ ================================================ FILE: docs/.vuepress/theme/components/SidebarButton.vue ================================================ ================================================ FILE: docs/.vuepress/theme/components/SidebarGroup.vue ================================================ ================================================ FILE: docs/.vuepress/theme/components/SidebarLink.vue ================================================ ================================================ FILE: docs/.vuepress/theme/components/SidebarLinks.vue ================================================ ================================================ FILE: docs/.vuepress/theme/global-components/Badge.vue ================================================ ================================================ FILE: docs/.vuepress/theme/index.js ================================================ const path = require('path'); // Theme API. module.exports = (options, ctx) => ({ alias() { const { themeConfig, siteConfig } = ctx; // resolve algolia const isAlgoliaSearch = themeConfig.algolia || Object.keys((siteConfig.locales && themeConfig.locales) || {}).some(base => themeConfig.locales[base].algolia); return { '@AlgoliaSearchBox': isAlgoliaSearch ? path.resolve(__dirname, 'components/AlgoliaSearchBox.vue') : path.resolve(__dirname, 'noopModule.js') }; }, plugins: [ '@vuepress/active-header-links', '@vuepress/search', '@vuepress/plugin-nprogress', ['@vuepress/container', { type: 'tip' }], ['@vuepress/container', { type: 'warning' }], ['@vuepress/container', { type: 'danger' }] ] }); ================================================ FILE: docs/.vuepress/theme/layouts/404.vue ================================================ ================================================ FILE: docs/.vuepress/theme/layouts/Layout.vue ================================================ ================================================ FILE: docs/.vuepress/theme/styles/arrow.styl ================================================ @require './config' .arrow display inline-block width 0 height 0 &.up border-left 4px solid transparent border-right 4px solid transparent border-bottom 6px solid $arrowBgColor &.down border-left 4px solid transparent border-right 4px solid transparent border-top 6px solid $arrowBgColor &.right border-top 4px solid transparent border-bottom 4px solid transparent border-left 6px solid $arrowBgColor &.left border-top 4px solid transparent border-bottom 4px solid transparent border-right 6px solid $arrowBgColor ================================================ FILE: docs/.vuepress/theme/styles/code.styl ================================================ .content code color lighten($textColor, 20%) padding 0.25rem 0.5rem margin 0 font-size 0.85em background-color rgba(27,31,35,0.05) border-radius 3px .token &.deleted color #EC5975 &.inserted color $accentColor .content pre, pre[class*="language-"] line-height 1.4 padding 1.25rem 1.5rem margin 0.85rem 0 background-color $codeBgColor border-radius 6px overflow auto code color #fff padding 0 background-color transparent border-radius 0 div[class*="language-"] position relative background-color $codeBgColor border-radius 6px .highlight-lines user-select none padding-top 1.3rem position absolute top 0 left 0 width 100% line-height 1.4 .highlighted background-color rgba(0, 0, 0, 66%) pre, pre[class*="language-"] background transparent position relative z-index 1 &::before position absolute z-index 3 top 0.8em right 1em font-size 0.75rem color rgba(255, 255, 255, 0.4) &:not(.line-numbers-mode) .line-numbers-wrapper display none &.line-numbers-mode .highlight-lines .highlighted position relative &:before content ' ' position absolute z-index 3 left 0 top 0 display block width $lineNumbersWrapperWidth height 100% background-color rgba(0, 0, 0, 66%) pre padding-left $lineNumbersWrapperWidth + 1 rem vertical-align middle .line-numbers-wrapper position absolute top 0 width $lineNumbersWrapperWidth text-align center color rgba(255, 255, 255, 0.3) padding 1.25rem 0 line-height 1.4 br user-select none .line-number position relative z-index 4 user-select none font-size 0.85em &::after content '' position absolute z-index 2 top 0 left 0 width $lineNumbersWrapperWidth height 100% border-radius 6px 0 0 6px border-right 1px solid rgba(0, 0, 0, 66%) background-color $codeBgColor for lang in $codeLang div{'[class~="language-' + lang + '"]'} &:before content ('' + lang) div[class~="language-javascript"] &:before content "js" div[class~="language-typescript"] &:before content "ts" div[class~="language-markup"] &:before content "html" div[class~="language-markdown"] &:before content "md" div[class~="language-json"]:before content "json" div[class~="language-ruby"]:before content "rb" div[class~="language-python"]:before content "py" div[class~="language-bash"]:before content "sh" div[class~="language-php"]:before content "php" ================================================ FILE: docs/.vuepress/theme/styles/custom-blocks.styl ================================================ .custom-block .custom-block-title font-weight 600 margin-bottom -0.4rem &.tip, &.warning, &.danger padding .1rem 1.5rem border-left-width .5rem border-left-style solid margin 1rem 0 &.tip background-color #f3f5f7 border-color #42b983 &.warning background-color rgba(255,229,100,.3) border-color darken(#ffe564, 35%) color darken(#ffe564, 70%) .custom-block-title color darken(#ffe564, 50%) a color $textColor &.danger background-color #ffe6e6 border-color darken(red, 20%) color darken(red, 70%) .custom-block-title color darken(red, 40%) a color $textColor pre.vue-container border-left-width: .5rem; border-left-style: solid; border-color: #42b983; border-radius: 0px; & > code font-size: 14px !important; & > p margin: -5px 0 -20px 0; code background-color: #42b983 !important; padding: 3px 5px; border-radius: 3px; color #000 em color #808080 font-weight light ================================================ FILE: docs/.vuepress/theme/styles/mobile.styl ================================================ @require './config' $mobileSidebarWidth = $sidebarWidth * 0.82 // narrow desktop / iPad @media (max-width: $MQNarrow) .sidebar font-size 15px width $mobileSidebarWidth .page padding-left $mobileSidebarWidth // wide mobile @media (max-width: $MQMobile) .sidebar top 0 padding-top $navbarHeight transform translateX(-100%) transition transform .2s ease .page padding-left 0 .theme-container &.sidebar-open .sidebar transform translateX(0) &.no-navbar .sidebar padding-top: 0 // narrow mobile @media (max-width: $MQMobileNarrow) h1 font-size 1.9rem .content div[class*="language-"] margin 0.85rem -1.5rem border-radius 0 ================================================ FILE: docs/.vuepress/theme/styles/theme.styl ================================================ @require './code' @require './custom-blocks' @require './arrow' @require './wrapper' @require './toc' html, body padding 0 margin 0 background-color #fff body font-family -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif -webkit-font-smoothing antialiased -moz-osx-font-smoothing grayscale font-size 16px color $textColor .page padding-left $sidebarWidth .navbar position fixed z-index 20 top 0 left 0 right 0 height $navbarHeight background-color #fff box-sizing border-box border-bottom 1px solid $borderColor .sidebar-mask position fixed z-index 9 top 0 left 0 width 100vw height 100vh display none .sidebar font-size 16px background-color #fff width $sidebarWidth position fixed z-index 10 margin 0 top $navbarHeight left 0 bottom 0 box-sizing border-box border-right 1px solid $borderColor overflow-y auto .content:not(.custom) @extend $wrapper > *:first-child margin-top $navbarHeight a:hover text-decoration underline p.demo padding 1rem 1.5rem border 1px solid #ddd border-radius 4px img max-width 100% .content.custom padding 0 margin 0 img max-width 100% a font-weight 500 color $accentColor text-decoration none p a code font-weight 400 color $accentColor kbd background #eee border solid 0.15rem #ddd border-bottom solid 0.25rem #ddd border-radius 0.15rem padding 0 0.15em blockquote font-size .9rem color #999 border-left .5rem solid #dfe2e5 margin 0.5rem 0 padding .25rem 0 .25rem 1rem & > p margin 0 ul, ol padding-left 1.2em strong font-weight 600 h1, h2, h3, h4, h5, h6 font-weight 600 line-height 1.25 .content:not(.custom) > & margin-top (0.5rem - $navbarHeight) padding-top ($navbarHeight + 1rem) margin-bottom 0 &:first-child margin-top -1.5rem margin-bottom 1rem + p, + pre, + .custom-block margin-top 2rem &:hover .header-anchor opacity: 1 h1 font-size 2.2rem h2 font-size 1.65rem padding-bottom .3rem border-bottom 1px solid $borderColor h3 font-size 1.35rem a.header-anchor font-size 0.85em float left margin-left -0.87em padding-right 0.23em margin-top 0.125em opacity 0 &:hover text-decoration none code, kbd, .line-number font-family source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace p, ul, ol line-height 1.7 hr border 0 border-top 1px solid $borderColor table border-collapse collapse margin 1rem 0 display: block overflow-x: auto tr border-top 1px solid #dfe2e5 &:nth-child(2n) background-color #f6f8fa th, td border 1px solid #dfe2e5 padding .6em 1em .theme-container &.sidebar-open .sidebar-mask display: block &.no-navbar .content:not(.custom) > h1, h2, h3, h4, h5, h6 margin-top 1.5rem padding-top 0 .sidebar top 0 @media (min-width: ($MQMobile + 1px)) .theme-container.no-sidebar .sidebar display none .page padding-left 0 @require 'mobile.styl' ================================================ FILE: docs/.vuepress/theme/styles/toc.styl ================================================ .table-of-contents .badge vertical-align middle ================================================ FILE: docs/.vuepress/theme/styles/wrapper.styl ================================================ $wrapper max-width $contentWidth // margin 0 2rem padding 2rem 2.5rem @media (max-width: $MQNarrow) padding 2rem @media (max-width: $MQMobileNarrow) padding 1.5rem ================================================ FILE: docs/.vuepress/theme/util/index.js ================================================ export const hashRE = /#.*$/; export const extRE = /\.(md|html)$/; export const endingSlashRE = /\/$/; export const outboundRE = /^(https?:|mailto:|tel:)/; export function normalize(path) { return decodeURI(path) .replace(hashRE, '') .replace(extRE, ''); } export function getHash(path) { const match = path.match(hashRE); if (match) { return match[0]; } } export function isExternal(path) { return outboundRE.test(path); } export function isMailto(path) { return /^mailto:/.test(path); } export function isTel(path) { return /^tel:/.test(path); } export function ensureExt(path) { if (isExternal(path)) { return path; } const hashMatch = path.match(hashRE); const hash = hashMatch ? hashMatch[0] : ''; const normalized = normalize(path); if (endingSlashRE.test(normalized)) { return path; } return normalized + '.html' + hash; } export function isActive(route, path) { const routeHash = route.hash; const linkHash = getHash(path); if (linkHash && routeHash !== linkHash) { return false; } const routePath = normalize(route.path); const pagePath = normalize(path); return routePath === pagePath; } export function resolvePage(pages, rawPath, base) { if (base) { rawPath = resolvePath(rawPath, base); } const path = normalize(rawPath); for (let i = 0; i < pages.length; i++) { if (normalize(pages[i].regularPath) === path) { return Object.assign({}, pages[i], { type: 'page', path: ensureExt(pages[i].path) }); } } console.error(`[vuepress] No matching page found for sidebar item "${rawPath}"`); return {}; } function resolvePath(relative, base, append) { const firstChar = relative.charAt(0); if (firstChar === '/') { return relative; } if (firstChar === '?' || firstChar === '#') { return base + relative; } const stack = base.split('/'); // remove trailing segment if: // - not appending // - appending to trailing slash (last segment is empty) if (!append || !stack[stack.length - 1]) { stack.pop(); } // resolve relative path const segments = relative.replace(/^\//, '').split('/'); for (let i = 0; i < segments.length; i++) { const segment = segments[i]; if (segment === '..') { stack.pop(); } else if (segment !== '.') { stack.push(segment); } } // ensure leading slash if (stack[0] !== '') { stack.unshift(''); } return stack.join('/'); } /** * @param { Page } page * @param { string } regularPath * @param { SiteData } site * @param { string } localePath * @returns { SidebarGroup } */ export function resolveSidebarItems(page, regularPath, site, localePath) { const { pages, themeConfig } = site; const localeConfig = localePath && themeConfig.locales ? themeConfig.locales[localePath] || themeConfig : themeConfig; const pageSidebarConfig = page.frontmatter.sidebar || localeConfig.sidebar || themeConfig.sidebar; if (pageSidebarConfig === 'auto') { return resolveHeaders(page); } const sidebarConfig = localeConfig.sidebar || themeConfig.sidebar; if (!sidebarConfig) { return []; } else { const { base, config } = resolveMatchingConfig(regularPath, sidebarConfig); return config ? config.map(item => resolveItem(item, pages, base)) : []; } } /** * @param { Page } page * @returns { SidebarGroup } */ function resolveHeaders(page) { const headers = groupHeaders(page.headers || []); return [ { type: 'group', collapsable: false, title: page.title, path: null, children: headers.map(h => ({ type: 'auto', title: h.title, basePath: page.path, path: page.path + '#' + h.slug, children: h.children || [] })) } ]; } export function groupHeaders(headers) { // group h3s under h2 headers = headers.map(h => Object.assign({}, h)); let lastH2; headers.forEach(h => { if (h.level === 2) { lastH2 = h; } else if (lastH2) { (lastH2.children || (lastH2.children = [])).push(h); } }); return headers.filter(h => h.level === 2); } export function resolveNavLinkItem(linkItem) { return Object.assign(linkItem, { type: linkItem.items && linkItem.items.length ? 'links' : 'link' }); } /** * @param { Route } route * @param { Array | Array | [link: string]: SidebarConfig } config * @returns { base: string, config: SidebarConfig } */ export function resolveMatchingConfig(regularPath, config) { if (Array.isArray(config)) { return { base: '/', config: config }; } for (const base in config) { if (ensureEndingSlash(regularPath).indexOf(base) === 0) { return { base, config: config[base] }; } } return {}; } function ensureEndingSlash(path) { return /(\.html|\/)$/.test(path) ? path : path + '/'; } function resolveItem(item, pages, base, groupDepth = 1) { if (typeof item === 'string') { return resolvePage(pages, item, base); } else if (Array.isArray(item)) { return Object.assign(resolvePage(pages, item[0], base), { title: item[1] }); } else { if (groupDepth > 3) { console.error('[vuepress] detected a too deep nested sidebar group.'); } const children = item.children || []; if (children.length === 0 && item.path) { return Object.assign(resolvePage(pages, item.path, base), { title: item.title }); } return { type: 'group', path: item.path, title: item.title, sidebarDepth: item.sidebarDepth, children: children.map(child => resolveItem(child, pages, base, groupDepth + 1)), collapsable: item.collapsable !== false }; } } ================================================ FILE: docs/README.md ================================================ # 深入理解 TypeScript 此书是 [《TypeScript Deep Dive》](https://github.com/basarat/typescript-book/) 的中文翻译版,感谢作者 [Basarat](https://github.com/basarat) 的付出。 如果你喜欢纸质书籍,可以通过[京东](https://item.jd.com/12755624.html)或者[当当](http://product.m.dangdang.com/28487648.html?t=1574581821),来购买此书。 你可以通过订阅该[公众号](https://cdn.jkchao.cn/nuxt/img/14958af.jpg),来获取更多有趣的内容。 ## Why ide > 数据来源:[npm 包下载量](https://npm-stat.com/charts.html?package=typescript&from=2016-01-01&to=2018-07-31) 如你所见,TypeScript 发展至今,已经成为大型项目的标配,其提供的静态类型系统,大大增强了代码的可读性以及可维护性;同时,它提供最新和不断发展的 JavaScript 特性,能让我们建立更健壮的组件。 [《TypeScript Deep Dive》](https://github.com/basarat/typescript-book/) 是一本很好的开源书,从基础到深入,很全面的阐述了 TypeScript 的各种魔法,不管你是新手,还是老鸟,它都将适应你。此外,它不同于 TypeScript 官方给出的文档(当然 TypeScript 给出的文档是很好的),在此书中,结合实际应用下的场景用例,你将能更深入的理解 TypeScript。 如今社区已经存在部分翻译,但都似乎已经停止更新。 于是在某天的某个冲动之下,这个 RP 就诞生了。 ## 翻译内容 《TypeScript Deep Dive》 书中包含一部分 JavaScript Future 和一些其他的内容,在这里,我们并不打算翻译它,如果你有兴趣,可以查看原书中 [JavaScript Future](https://basarat.gitbooks.io/typescript/content/docs/future-javascript.html) 的有关章节。 由于 TypeScript 更新频繁,在此书中,我也将加入一些原书中并没有涉及到的知识点,希望和大家相互学习,一起进步。 此外,在不违背原作者本意前提下,为了更直观的表达,部分内容将采用意译,而非直译。 ## How to contribute 你可以: - 通过 PR 修改错别字,或者错误的格式; - 发 issue 讨论文章中出现的一些不合理地方; - 翻译 TODO 文件夹下的文章,并顺手 Email 我。 希望你在翻译或者 PR 之前,阅读[中文文章排版指北](https://github.com/mzlogin/chinese-copywriting-guidelines)。 ## 最后 如果你和我一样对 TypeScript 充满兴趣,可以订阅(star)本项目,及时收到有关于此项目的更新。 如果你对文章有任何疑问,欢迎提交 [issues](https://github.com/jkchao/typescript-book-chinese/issues) 和我交流。 如果你认为有些地方翻译不够准确,或者你想补充一些文中没提到但是非常有意思的知识点,欢迎 [PR](https://github.com/jkchao/typescript-book-chinese/pulls)。 公众号: 支付宝 ================================================ FILE: docs/compiler/ast.md ================================================ # 抽象语法树 ### Node 节点 节点是抽象语法树(AST) 的基本构造块。语法上,通常 `Node` 表示非末端(non-terminals)节点。但是,有些末端节点,如:标识符和字面量也会保留在树中。 AST 节点文档由两个关键部分构成。一是节点的 `SyntaxKind` 枚举,用于标识 AST 中的类型。二是其接口,即实例化 AST 时节点提供的 API。 这里是 `interface Node` 的一些关键成员: - `TextRange` 标识该节点在源文件中的起止位置。 - `parent?: Node` 当前节点(在 AST 中)的父节点 `Node` 还有一些其他的成员,标志(flags)和修饰符(modifiers)等。你可以在源码中搜索 `interface Node` 来查看,而上面提到对节点的遍历是非常重要的。 ### SourceFile - `SyntaxKind.SourceFile` - `interface SourceFile`. 每个 `SourceFile` 都是一棵 AST 的顶级节点,它们包含在 `Program` 中。 ## AST 技巧:访问子节点 有个工具函数 `ts.forEachChild`,可以用来访问 AST 任一节点的所有子节点。 下面是简化的代码片段,用于演示如何工作: ```ts export function forEachChild(node: Node, cbNode: (node: Node) => T, cbNodeArray?: (nodes: Node[]) => T): T { if (!node) { return; } switch (node.kind) { case SyntaxKind.BinaryExpression: return visitNode(cbNode, (node).left) || visitNode(cbNode, (node).operatorToken) || visitNode(cbNode, (node).right); case SyntaxKind.IfStatement: return visitNode(cbNode, (node).expression) || visitNode(cbNode, (node).thenStatement) || visitNode(cbNode, (node).elseStatement); // .... 更多 ``` 该函数主要检查 `node.kind` 并据此判断 node 的接口,然后在其子节点上调用 `cbNode`。但是,要注意该函数不会为*所有*子节点调用 `visitNode`(例如:SyntaxKind.SemicolonToken)。想获得某 AST 节点的*所有*子节点,只要调用该节点的成员函数 `.getChildren`。 如下函数会打印 AST 节点详细信息: ```ts function printAllChildren(node: ts.Node, depth = 0) { console.log(new Array(depth + 1).join('----'), ts.syntaxKindToName(node.kind), node.pos, node.end); depth++; node.getChildren().forEach(c => printAllChildren(c, depth)); } ``` 我们进一步讨论解析器时会看到该函数的使用示例。 ## AST 技巧:SyntaxKind 枚举 `SyntaxKind` 被定义为一个常量枚举,如下所示: ```ts export const enum SyntaxKind { Unknown, EndOfFileToken, SingleLineCommentTrivia, // ... 更多 ``` 这是个[常量枚举](../typings/enums.md#常量枚举),方便*内联*(例如:`ts.SyntaxKind.EndOfFileToken` 会变为 `1`),这样在使用 AST 时就不会有处理引用的额外开销。但编译时需要使用 --preserveConstEnums 编译标志,以便枚举*在运行时仍可用*。JavaScript 中你也可以根据需要使用 `ts.SyntaxKind.EndOfFileToken`。另外,可以用以下函数,将枚举成员转化为可读的字符串: ```ts export function syntaxKindToName(kind: ts.SyntaxKind) { return (ts).SyntaxKind[kind]; } ``` ## AST 杂项 杂项(Trivia)是指源文本中对正常理解代码不太重要的部分,例如:空白,注释,冲突标记。(为了保持轻量)杂项*不会存储*在 AST 中。但是可以*视需要*使用一些 `ts.*` API 来获取。 展示这些 API 前,你需要理解以下内容: ### 杂项的所有权 通常: - token 拥有它后面 _同一行_ 到下一个 token 之前的所有杂项 - 该行之后的注释都与下个的 token 相关 对于文件中的前导(leading)和结束(ending)注释: - 源文件中的第一个 token 拥有所有开始的杂项 - 而文件最后的一些列杂项则附加到文件结束符上,该 token 长度为 0 ### 杂项 API 注释在多数基本使用中,都是让人关注的杂项。节点的注释可以通过以下函数获取: | 函数 | 描述 | | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `ts.getLeadingCommentRanges` | 给定源文本及其位置,返回给定位置后第一个换行符到 token 本身之间的注释范围(可能需要结合 `ts.Node.getFullStart` 使用)。 | | `ts.getTrailingCommentRanges` | 给定源文本及其位置,返回给定位置后第一个换行符之前的注释范围(可能需要结合 `ts.Node.getEnd` 使用)。 | 假设下面是某个源文件的一部分: ```ts debugger;/*hello*/ //bye /*hi*/ function ``` 对 `function` 而言,`getLeadingCommentRanges` 仅返回最后的两个注释 `//bye` 和 `/*hi*/`。 另外,而在 `debugger` 语句结束位置调用 `getTrailingCommentRanges` 会得到注释 `/*hello*/`。 ### Token Start 和 Full Start 位置 节点有所谓的 "token start" 和 "full start" 位置。 - Token Start:比较自然的版本,即文件中一个 token 的文本开始的位置。 - Full Start:是指扫描器从上一个重要 token 开始扫描的位置。 AST 节点有 `getStart` 和 `getFullStart` API 用于获取以上两种位置,还是这个例子: ```ts debugger;/*hello*/ //bye /*hi*/ function ``` 对 `function` 而言,token start 即 `function` 的位置,而 _full_ start 是 `/*hello*/` 的位置。要注意,full start 甚至会包含前一节点拥有的杂项。 ================================================ FILE: docs/compiler/binder.md ================================================ # 绑定器 大多数的 JavaScript 转译器(transpiler)都比 TypeScript 简单,因为它们几乎没提供代码分析的方法。典型的 JavaScript 转换器只有以下流程: ``` 源码 ~~扫描器~~> Tokens ~~解析器~~> AST ~~发射器~~> JavaScript ``` 上述架构确实对于简化 TypeScript 生成 JavaScript 的理解有帮助,但缺失了一个关键功能,即 TypeScript 的*语义*系统。为了协助(检查器执行)类型检查,绑定器将源码的各部分连接成一个相关的类型系统,供检查器使用。绑定器的主要职责是创建*符号*(Symbols)。 ### 符号 符号将 AST 中的声明节点与其它声明连接到相同的实体上。符号是语义系统的基本构造块。符号的构造器定义在 `core.ts`(绑定器实际上通过 `objectAllocator.getSymbolConstructor` 来获取构造器)。下面是符号构造器: ```ts function Symbol(flags: SymbolFlags, name: string) { this.flags = flags; this.name = name; this.declarations = undefined; } ``` `SymbolFlags` 符号标志是个标志枚举,用于识别额外的符号类别(例如:变量作用域标志 `FunctionScopedVariable` 或 `BlockScopedVariable` 等) ### 检查器对绑定器的使用 实际上,绑定器被检查器在内部调用,而检查器又被程序调用。简化的调用栈如下所示: ``` program.getTypeChecker -> ts.createTypeChecker(检查器中)-> initializeTypeChecker(检查器中) -> for each SourceFile `ts.bindSourceFile`(绑定器中) // followed by for each SourceFile `ts.mergeSymbolTable`(检查器中) ``` SourceFile 是绑定器的工作单元,`binder.ts` 由 `checker.ts` 驱动。 ## 绑定器函数 `bindSourceFile` 和 `mergeSymbolTable` 是两个关键的绑定器函数,我们来看下: ### `bindSourceFile` 该函数主要是检查 `file.locals` 是否定义,如果没有则交给(本地函数) `bind` 来处理。 注意:`locals` 定义在节点上,其类型为 `SymbolTable`。`SourceFile` 也是一个节点(事实上是 AST 中的根节点)。 提示:TypeScript 编译器大量使用本地函数。本地函数很可能使用来自父函数的变量(通过闭包捕获)。例如 `bind` 是 `bindSourceFile` 中的一个本地函数,它或它调用的函数会设置 `symbolCount` 和 `classifiableNames` 等状态,然后将其存在返回的 `SourceFile` 中 ### `bind` bind 能处理任一节点(不只是 `SourceFile`),它做的第一件事是分配 `node.parent`(如果 `parent` 变量已设置,绑定器在 `bindChildren` 函数的处理中仍会再次设置), 然后交给 `bindWorker` 做很多*重*活。最后调用 `bindChildren`(该函数简单地将绑定器的状态(如:`parent`)存入函数本地变量中,接着在每个子节点上调用 `bind`,然后再将状态转存回绑定器中)。现在我们看下 `bindWorker`,一个更有趣的函数。 ### `bindWorker` 该函数依据 `node.kind`(`SyntaxKind`类型)进行切换,并将工作委托给合适的 `bindXXX` 函数(也定义在`binder.ts`中)。例如:如果该节点是 `SourceFile` 则(最终且仅当节点是外部文件模块时)调用 `bindAnonymousDeclaration` ### `bindXXX` 函数 `bindXXX` 系函数有一些通用的模式和工具函数。其中最常用的一个是 `createSymbol` 函数,全部代码展示如下: ```ts function createSymbol(flags: SymbolFlags, name: string): Symbol { symbolCount++; return new Symbol(flags, name); } ``` 如您所见,它简单地更新 `symbolCount`(一个 `bindSourceFile` 的本地变量),并使用指定的参数创建符号。 ## 绑定器声明 ### 符号与声明 节点和符号间的链接由几个函数执行。其中一个用于绑定 `SourceFile` 节点到源文件符号(外部模块的情况下)的函数是 `addDeclarationToSymbol` 注意:外部模块源文件的符号设置方式是 `flags : SymbolFlags.ValueModule` 和 `name: '"' + removeFileExtension(file.fileName) + '"'`. ```ts function addDeclarationToSymbol(symbol: Symbol, node: Declaration, symbolFlags: SymbolFlags) { symbol.flags |= symbolFlags; // 创建 AST 节点到 symbol 的连接 node.symbol = symbol; if (!symbol.declarations) { symbol.declarations = []; } // 将该节点添加为该符号的一个声明 symbol.declarations.push(node); if (symbolFlags & SymbolFlags.HasExports && !symbol.exports) { symbol.exports = {}; } if (symbolFlags & SymbolFlags.HasMembers && !symbol.members) { symbol.members = {}; } if (symbolFlags & SymbolFlags.Value && !symbol.valueDeclaration) { symbol.valueDeclaration = node; } } ``` 上述代码主要执行的操作如下: - 创建一个从 AST 节点到符号的链接(`node.symbol`) - 将节点添加为该符号的*一个*声明 ### 声明 声明就是一个有可选的名字的节点。下面是 `types.ts` 中的定义: ```ts interface Declaration extends Node { _declarationBrand: any; name?: DeclarationName; } ``` ## 绑定器容器 AST 的节点可以被当作容器。这决定了节点及相关符号的 `SymbolTables` 的类别。容器是个抽象概念(没有相关的数据结构)。该概念由一些东西决定,`ContainerFlags` 枚举是其中之一。函数 `getContainerFlags`(位于 `binder.ts`) 驱动此标志,如下所示: ```ts function getContainerFlags(node: Node): ContainerFlags { switch (node.kind) { case SyntaxKind.ClassExpression: case SyntaxKind.ClassDeclaration: case SyntaxKind.InterfaceDeclaration: case SyntaxKind.EnumDeclaration: case SyntaxKind.TypeLiteral: case SyntaxKind.ObjectLiteralExpression: return ContainerFlags.IsContainer; case SyntaxKind.CallSignature: case SyntaxKind.ConstructSignature: case SyntaxKind.IndexSignature: case SyntaxKind.MethodDeclaration: case SyntaxKind.MethodSignature: case SyntaxKind.FunctionDeclaration: case SyntaxKind.Constructor: case SyntaxKind.GetAccessor: case SyntaxKind.SetAccessor: case SyntaxKind.FunctionType: case SyntaxKind.ConstructorType: case SyntaxKind.FunctionExpression: case SyntaxKind.ArrowFunction: case SyntaxKind.ModuleDeclaration: case SyntaxKind.SourceFile: case SyntaxKind.TypeAliasDeclaration: return ContainerFlags.IsContainerWithLocals; case SyntaxKind.CatchClause: case SyntaxKind.ForStatement: case SyntaxKind.ForInStatement: case SyntaxKind.ForOfStatement: case SyntaxKind.CaseBlock: return ContainerFlags.IsBlockScopedContainer; case SyntaxKind.Block: // 不要将函数内部的块直接当做块作用域的容器。 // 本块中的本地变量应当置于函数中,否则下例中的 'x' 不会重新声明为一个块作用域的本地变量: // // function foo() { // var x; // let x; // } // // 如果将 'var x' 留在函数中,而将 'let x' 放到本块中(函数外),就不会有冲突了。 // // 如果不在这里创建一个新的块作用域容器,'var x' 和 'let x' 都会进入函数容器本地中,这样就会有碰撞冲突。 return isFunctionLike(node.parent) ? ContainerFlags.None : ContainerFlags.IsBlockScopedContainer; } return ContainerFlags.None; } ``` 该函数*只*在绑定器函数 `bindChildren` 中调用,会根据 `getContainerFlags` 的运行结果将节点设为 `container` 和(或) `blockScopedContainer`。函数 `bindChildren` 如下所示: ```ts // 所有容器节点都以声明顺序保存在一个链表中。 // 类型检查器中的 getLocalNameOfContainer 函数会使用该链表对容器使用的本地名称的唯一性做验证。 function bindChildren(node: Node) { // 在递归到子节点之前,我们先要保存父节点,容器和块容器。处理完弹出的子节点后,再将这些值存回原处。 let saveParent = parent; let saveContainer = container; let savedBlockScopeContainer = blockScopeContainer; // 现在要将这个节点设为父节点,我们要递归它的子节点。 parent = node; // 根据节点的类型,需要对当前容器或块容器进行调整。 如果当前节点是个容器,则自动将其视为当前的块容器。 // 由于我们知道容器可能包含本地变量,因此提前初始化 .locals 字段。 // 这样做是因为很可能需要将一些子(节点)置入 .locals 中(例如:函数参数或变量声明)。 // // 但是,我们不会主动为块容器创建 .locals,因为通常块容器中不会有块作用域变量。 // 我们不想为遇到的每个块都分配一个对象,大多数情况没有必要。 // // 最后,如果是个块容器,我们就清理该容器中可能存在的 .locals 对象。这种情况常在增量编译场景中发生。 // 由于我们可以重用上次编译的节点,而该节点可能已经创建了 locals 对象。 // 因此必须清理,以免意外地从上次的编译中移动了过时的数据。 let containerFlags = getContainerFlags(node); if (containerFlags & ContainerFlags.IsContainer) { container = blockScopeContainer = node; if (containerFlags & ContainerFlags.HasLocals) { container.locals = {}; } addToContainerChain(container); } else if (containerFlags & ContainerFlags.IsBlockScopedContainer) { blockScopeContainer = node; blockScopeContainer.locals = undefined; } forEachChild(node, bind); container = saveContainer; parent = saveParent; blockScopeContainer = savedBlockScopeContainer; } ``` 您可能还记得绑定器函数中的这部分:`bindChildren` 由 `bind` 函数调用。我们得到这样的递归绑定:`bind` 调用 `bindChildren`,而 `bindChildren` 又为其每个子节点调用 `bind` ## 绑定器符号表 符号表(SymbolTable)是以一个简单的 HashMap 实现的,下面是其接口(`types.ts`): ```ts interface SymbolTable { [index: string]: Symbol; } ``` 符号表通过绑定进行初始化,这里是编译器使用的一些符号表: 节点上: ```ts locals?: SymbolTable; // 节点相关的本地变量 ``` 符号上: ```ts members?: SymbolTable; // 类,接口或字面量实例成员 exports?: SymbolTable; // 模块导出 ``` 请注意:`bindChildren` 基于 `ContainerFlags` 初始化 `locals`(为 `{}`) ### 符号表填充 符号表使用符号来填充,主要是通过调用 `declareSymbol` 来进行,如下所示的是该函数的全部代码: ```ts /** * 为指定的节点声明一个符号并加入 symbols。标识名冲突时报告错误。 * @param symbolTable - 要将节点加入进的符号表 * @param parent - 指定节点的父节点的声明 * @param node - 要添加到符号表的(节点)声明 * @param includes - SymbolFlags,指定节点额外的声明类型(例如:export, ambient 等) * @param excludes - 不能在符号表中声明的标志,用于报告禁止的声明 */ function declareSymbol( symbolTable: SymbolTable, parent: Symbol, node: Declaration, includes: SymbolFlags, excludes: SymbolFlags ): Symbol { Debug.assert(!hasDynamicName(node)); // 默认导出的函数节点或类节点的符号总是"default" let name = node.flags & NodeFlags.Default && parent ? 'default' : getDeclarationName(node); let symbol: Symbol; if (name !== undefined) { // 检查符号表中是否已有同名的符号。若没有,创建此名称的新符号并加入表中。 // 注意,我们尚未给新符号指定任何标志。这可以确保不会和传入的 excludes 标志起冲突。 // // 如果已存在的一个符号,查看是否与要创建的新符号冲突。 // 例如:同一符号表中,'var' 符号和 'class' 符号会冲突。 // 如果有冲突,报告该问题给该符号的每个声明,然后为该声明创建一个新符号 // // 如果我们创建的新符号既没在符号表中重名也没和现有符号冲突,就将该节点添加为新符号的唯一声明。 // // 否则,就要(将新符号)合并进兼容的现有符号中(例如同一容器中有多个同名的 'var' 时)。这种情况下要把该节点添加到符号的声明列表中。 symbol = hasProperty(symbolTable, name) ? symbolTable[name] : (symbolTable[name] = createSymbol(SymbolFlags.None, name)); if (name && includes & SymbolFlags.Classifiable) { classifiableNames[name] = name; } if (symbol.flags & excludes) { if (node.name) { node.name.parent = node; } // 报告每个重复声明的错误位置 // 报告之前遇到的声明错误 let message = symbol.flags & SymbolFlags.BlockScopedVariable ? Diagnostics.Cannot_redeclare_block_scoped_variable_0 : Diagnostics.Duplicate_identifier_0; forEach(symbol.declarations, declaration => { file.bindDiagnostics.push( createDiagnosticForNode(declaration.name || declaration, message, getDisplayName(declaration)) ); }); file.bindDiagnostics.push(createDiagnosticForNode(node.name || node, message, getDisplayName(node))); symbol = createSymbol(SymbolFlags.None, name); } } else { symbol = createSymbol(SymbolFlags.None, '__missing'); } addDeclarationToSymbol(symbol, node, includes); symbol.parent = parent; return symbol; } ``` 填充哪个符号表,由此函数的第一个参数决定。例如:添加声明到类型为 `SyntaxKind.ClassDeclaration` 或 `SyntaxKind.ClassExpression` 的*容器*时,将会调用下面的函数 `declareClassMember`: ```ts function declareClassMember(node: Declaration, symbolFlags: SymbolFlags, symbolExcludes: SymbolFlags) { return node.flags & NodeFlags.Static ? declareSymbol(container.symbol.exports, container.symbol, node, symbolFlags, symbolExcludes) : declareSymbol(container.symbol.members, container.symbol, node, symbolFlags, symbolExcludes); } ``` ## 绑定器错误报告 绑定错误被添加到源文件的 `bindDiagnostics` 列表中 一个绑定时错误检测的例子是在严格模式下使用 `eval` 或 `arguments` 作为变量名。下面展示了相关的全部代码(多个位置都会调用`checkStrictModeEvalOrArguments`,调用栈发自 `bindWorker`,该函数对不同节点的 `SyntaxKind` 调用不同的检查函数): ```ts function checkStrictModeEvalOrArguments(contextNode: Node, name: Node) { if (name && name.kind === SyntaxKind.Identifier) { let identifier = name; if (isEvalOrArgumentsIdentifier(identifier)) { // 首先检查名字是否在类声明或者类表达式中,如果是则给出明确消息,否则报告一般性错误 let span = getErrorSpanForNode(file, name); file.bindDiagnostics.push( createFileDiagnostic( file, span.start, span.length, getStrictModeEvalOrArgumentsMessage(contextNode), identifier.text ) ); } } } function isEvalOrArgumentsIdentifier(node: Node): boolean { return ( node.kind === SyntaxKind.Identifier && ((node).text === 'eval' || (node).text === 'arguments') ); } function getStrictModeEvalOrArgumentsMessage(node: Node) { // 向用户提供特定消息,有助他们理解为何会处于严格模式。 if (getContainingClass(node)) { return Diagnostics.Invalid_use_of_0_Class_definitions_are_automatically_in_strict_mode; } if (file.externalModuleIndicator) { return Diagnostics.Invalid_use_of_0_Modules_are_automatically_in_strict_mode; } return Diagnostics.Invalid_use_of_0_in_strict_mode; } ``` ================================================ FILE: docs/compiler/checker.md ================================================ # 检查器 如前所述,*检查器*使得 TypeScript 更独特,比*其它 JavaScript 转译器*更强大。检查器位于 `checker.ts` 中,当前有 23k 行以上的代码(编译器中最大的部分) ### 程序对检查器的使用 检查器是由程序初始化,下面是调用栈示意(绑定器一节也展示过): ``` program.getTypeChecker -> ts.createTypeChecker(检查器中)-> initializeTypeChecker(检查器中) -> for each SourceFile `ts.bindSourceFile`(绑定器中) // 接着 for each SourceFile `ts.mergeSymbolTable`(检查器中) ``` ### 与发射器的联系 真正的类型检查会在调用 `getDiagnostics` 时才发生。该函数被调用时(比如由 `Program.emit` 请求),检查器返回一个 `EmitResolver`(由程序调用检查器的 `getEmitResolver` 函数得到),`EmitResolver` 是 `createTypeChecker` 的一个本地函数的集合。介绍发射器时还会再次提到。 下面是该过程直到 `checkSourceFile` 的调用栈(`checkSourceFile` 是 `createTypeChecker` 的一个本地函数): ``` program.emit -> emitWorker (program local) -> createTypeChecker.getEmitResolver -> // 第一次调用下面的几个 createTypeChecker 的本地函数 call getDiagnostics -> getDiagnosticsWorker -> checkSourceFile // 接着 return resolver // 通过对本地函数 createResolver() 的调用,resolver 已在 createTypeChecker 中初始化。 ``` ## 全局命名空间合并 `initializeTypeChecker` 中存在以下代码: ```ts // 初始化全局符号表(SymbolTable)。 forEach(host.getSourceFiles(), file => { if (!isExternalModule(file)) { mergeSymbolTable(globals, file.locals); } }); ``` 基本上是将所有的 `global` 符号合并到 `let globals: SymbolTable = {}` 符号表中(位于 `createTypeChecker` 中)。 `mergeSymbolTable` 主要调用 `mergeSymbol` 函数。 ## 检查器错误报告 检查器使用本地的 `error` 函数报告错误,如下所示: ```ts function error(location: Node, message: DiagnosticMessage, arg0?: any, arg1?: any, arg2?: any): void { let diagnostic = location ? createDiagnosticForNode(location, message, arg0, arg1, arg2) : createCompilerDiagnostic(message, arg0, arg1, arg2); diagnostics.add(diagnostic); } ``` ================================================ FILE: docs/compiler/emitter.md ================================================ # 发射器 TypeScript 编译器提供了两个发射器: - `emitter.ts`:可能是你最感兴趣的发射器,它是 TS -> JavaScript 的发射器 - `declarationEmitter.ts`:这个发射器用于为 _TypeScript 源文件(`.ts`)_ 创建*声明文件(`.d.ts`)* 本节我们介绍 `emitter.ts` ### Promgram 对发射器的使用 Program 提供了一个 `emit` 函数。该函数主要将功能委托给 `emitter.ts`中的 `emitFiles` 函数。下面是调用栈: ``` Program.emit -> `emitWorker` (在 program.ts 中的 createProgram) -> `emitFiles` (emitter.ts 中的函数) ``` `emitWorker`(通过 `emitFiles` 参数)给发射器提供一个 `EmitResolver`。 `EmitResolver` 由程序的 TypeChecker 提供,基本上它是一个来自 `createChecker` 的本地函数的子集。 ## 发射器函数 ### `emitFiles` 定义在 `emitter.ts` 中,下面是该函数的签名: ```ts // targetSourceFile 当用户想发射项目中的某个文件时指定,保存时编译(compileOnSave)功能使用此参数 export function emitFiles(resolver: EmitResolver, host: EmitHost, targetSourceFile?: SourceFile): EmitResult { ``` `EmitHost` 是 `CompilerHost` 的简化版(运行时,很多用例实际上都是 `CompilerHost`) `emitFiles` 中的最有趣的调用栈如下所示: ``` emitFiles -> emitFile(jsFilePath, targetSourceFile) -> emitJavaScript(jsFilePath, targetSourceFile); ``` ### `emitJavaScript` 该函数有良好的注释,我们下面给出它: ```ts function emitJavaScript(jsFilePath: string, root?: SourceFile) { let writer = createTextWriter(newLine); let write = writer.write; let writeTextOfNode = writer.writeTextOfNode; let writeLine = writer.writeLine; let increaseIndent = writer.increaseIndent; let decreaseIndent = writer.decreaseIndent; let currentSourceFile: SourceFile; // 导出器函数的名称,如果文件是个系统外部模块的话 // System.register([...], function () {...}) // System 模块中的导出像这样: // export var x; ... x = 1 // => // var x;... exporter("x", x = 1) let exportFunctionForFile: string; let generatedNameSet: Map = {}; let nodeToGeneratedName: string[] = []; let computedPropertyNamesToGeneratedNames: string[]; let extendsEmitted = false; let decorateEmitted = false; let paramEmitted = false; let awaiterEmitted = false; let tempFlags = 0; let tempVariables: Identifier[]; let tempParameters: Identifier[]; let externalImports: (ImportDeclaration | ImportEqualsDeclaration | ExportDeclaration)[]; let exportSpecifiers: Map; let exportEquals: ExportAssignment; let hasExportStars: boolean; /** 将发射输出写入磁盘 */ let writeEmittedFiles = writeJavaScriptFile; let detachedCommentsInfo: { nodePos: number; detachedCommentEndPos: number }[]; let writeComment = writeCommentRange; /** 发射一个节点 */ let emit = emitNodeWithoutSourceMap; /** 在发射节点前调用 */ let emitStart = function(node: Node) {}; /** 发射结点完成后调用 */ let emitEnd = function(node: Node) {}; /** 从 startPos 位置开始,为指定的 token 发射文本。默认写入的文本由 tokenKind 提供, * 但是如果提供了可选的 emitFn 回调,将使用该回调来代替默认方式发射文本。 * @param tokenKind 要搜索并发射的 token 的类别 * @param startPos 源码中搜索 token 的起始位置 * @param emitFn 如果给出,会被调用来进行文本的发射。 */ let emitToken = emitTokenText; /** 该函数由于节点的缘故,在被发射的代码中的函数或类中,会在启用词法作用域前被调用 * @param scopeDeclaration 启动词法作用域的节点 * @param scopeName 可选的作用域的名称,默认从节点声明中推导 */ let scopeEmitStart = function(scopeDeclaration: Node, scopeName?: string) {}; /** 出了作用域后调用 */ let scopeEmitEnd = function() {}; /** 会被编码的 Sourcemap 数据 */ let sourceMapData: SourceMapData; if (compilerOptions.sourceMap || compilerOptions.inlineSourceMap) { initializeEmitterWithSourceMaps(); } if (root) { // 不要直接调用 emit,那样不会设置 currentSourceFile emitSourceFile(root); } else { forEach(host.getSourceFiles(), sourceFile => { if (!isExternalModuleOrDeclarationFile(sourceFile)) { emitSourceFile(sourceFile); } }); } writeLine(); writeEmittedFiles(writer.getText(), /*writeByteOrderMark*/ compilerOptions.emitBOM); return; /// 一批本地函数 } ``` 它主要设置了一批本地变量和函数(这些函数构成 `emitter.ts` 的*大部分*内容),接着交给本地函数 `emitSourceFile` 发射文本。`emitSourceFile` 函数设置 `currentSourceFile` 然后交给本地函数 `emit` 去处理。 ```ts function emitSourceFile(sourceFile: SourceFile): void { currentSourceFile = sourceFile; exportFunctionForFile = undefined; emit(sourceFile); } ``` `emit` 函数处理 _注释_ 和 _实际 JavaScript_ 的发射。_实际 JavaScript_ 的发射是 emitJavaScriptWorker 函数的工作。 ### `emitJavaScriptWorker` 完整的函数: ```ts function emitJavaScriptWorker(node: Node) { // 检查节点是否可以忽略 ScriptTarget 发射 switch (node.kind) { case SyntaxKind.Identifier: return emitIdentifier(node); case SyntaxKind.Parameter: return emitParameter(node); case SyntaxKind.MethodDeclaration: case SyntaxKind.MethodSignature: return emitMethod(node); case SyntaxKind.GetAccessor: case SyntaxKind.SetAccessor: return emitAccessor(node); case SyntaxKind.ThisKeyword: return emitThis(node); case SyntaxKind.SuperKeyword: return emitSuper(node); case SyntaxKind.NullKeyword: return write('null'); case SyntaxKind.TrueKeyword: return write('true'); case SyntaxKind.FalseKeyword: return write('false'); case SyntaxKind.NumericLiteral: case SyntaxKind.StringLiteral: case SyntaxKind.RegularExpressionLiteral: case SyntaxKind.NoSubstitutionTemplateLiteral: case SyntaxKind.TemplateHead: case SyntaxKind.TemplateMiddle: case SyntaxKind.TemplateTail: return emitLiteral(node); case SyntaxKind.TemplateExpression: return emitTemplateExpression(node); case SyntaxKind.TemplateSpan: return emitTemplateSpan(node); case SyntaxKind.JsxElement: case SyntaxKind.JsxSelfClosingElement: return emitJsxElement(node); case SyntaxKind.JsxText: return emitJsxText(node); case SyntaxKind.JsxExpression: return emitJsxExpression(node); case SyntaxKind.QualifiedName: return emitQualifiedName(node); case SyntaxKind.ObjectBindingPattern: return emitObjectBindingPattern(node); case SyntaxKind.ArrayBindingPattern: return emitArrayBindingPattern(node); case SyntaxKind.BindingElement: return emitBindingElement(node); case SyntaxKind.ArrayLiteralExpression: return emitArrayLiteral(node); case SyntaxKind.ObjectLiteralExpression: return emitObjectLiteral(node); case SyntaxKind.PropertyAssignment: return emitPropertyAssignment(node); case SyntaxKind.ShorthandPropertyAssignment: return emitShorthandPropertyAssignment(node); case SyntaxKind.ComputedPropertyName: return emitComputedPropertyName(node); case SyntaxKind.PropertyAccessExpression: return emitPropertyAccess(node); case SyntaxKind.ElementAccessExpression: return emitIndexedAccess(node); case SyntaxKind.CallExpression: return emitCallExpression(node); case SyntaxKind.NewExpression: return emitNewExpression(node); case SyntaxKind.TaggedTemplateExpression: return emitTaggedTemplateExpression(node); case SyntaxKind.TypeAssertionExpression: return emit((node).expression); case SyntaxKind.AsExpression: return emit((node).expression); case SyntaxKind.ParenthesizedExpression: return emitParenExpression(node); case SyntaxKind.FunctionDeclaration: case SyntaxKind.FunctionExpression: case SyntaxKind.ArrowFunction: return emitFunctionDeclaration(node); case SyntaxKind.DeleteExpression: return emitDeleteExpression(node); case SyntaxKind.TypeOfExpression: return emitTypeOfExpression(node); case SyntaxKind.VoidExpression: return emitVoidExpression(node); case SyntaxKind.AwaitExpression: return emitAwaitExpression(node); case SyntaxKind.PrefixUnaryExpression: return emitPrefixUnaryExpression(node); case SyntaxKind.PostfixUnaryExpression: return emitPostfixUnaryExpression(node); case SyntaxKind.BinaryExpression: return emitBinaryExpression(node); case SyntaxKind.ConditionalExpression: return emitConditionalExpression(node); case SyntaxKind.SpreadElementExpression: return emitSpreadElementExpression(node); case SyntaxKind.YieldExpression: return emitYieldExpression(node); case SyntaxKind.OmittedExpression: return; case SyntaxKind.Block: case SyntaxKind.ModuleBlock: return emitBlock(node); case SyntaxKind.VariableStatement: return emitVariableStatement(node); case SyntaxKind.EmptyStatement: return write(';'); case SyntaxKind.ExpressionStatement: return emitExpressionStatement(node); case SyntaxKind.IfStatement: return emitIfStatement(node); case SyntaxKind.DoStatement: return emitDoStatement(node); case SyntaxKind.WhileStatement: return emitWhileStatement(node); case SyntaxKind.ForStatement: return emitForStatement(node); case SyntaxKind.ForOfStatement: case SyntaxKind.ForInStatement: return emitForInOrForOfStatement(node); case SyntaxKind.ContinueStatement: case SyntaxKind.BreakStatement: return emitBreakOrContinueStatement(node); case SyntaxKind.ReturnStatement: return emitReturnStatement(node); case SyntaxKind.WithStatement: return emitWithStatement(node); case SyntaxKind.SwitchStatement: return emitSwitchStatement(node); case SyntaxKind.CaseClause: case SyntaxKind.DefaultClause: return emitCaseOrDefaultClause(node); case SyntaxKind.LabeledStatement: return emitLabelledStatement(node); case SyntaxKind.ThrowStatement: return emitThrowStatement(node); case SyntaxKind.TryStatement: return emitTryStatement(node); case SyntaxKind.CatchClause: return emitCatchClause(node); case SyntaxKind.DebuggerStatement: return emitDebuggerStatement(node); case SyntaxKind.VariableDeclaration: return emitVariableDeclaration(node); case SyntaxKind.ClassExpression: return emitClassExpression(node); case SyntaxKind.ClassDeclaration: return emitClassDeclaration(node); case SyntaxKind.InterfaceDeclaration: return emitInterfaceDeclaration(node); case SyntaxKind.EnumDeclaration: return emitEnumDeclaration(node); case SyntaxKind.EnumMember: return emitEnumMember(node); case SyntaxKind.ModuleDeclaration: return emitModuleDeclaration(node); case SyntaxKind.ImportDeclaration: return emitImportDeclaration(node); case SyntaxKind.ImportEqualsDeclaration: return emitImportEqualsDeclaration(node); case SyntaxKind.ExportDeclaration: return emitExportDeclaration(node); case SyntaxKind.ExportAssignment: return emitExportAssignment(node); case SyntaxKind.SourceFile: return emitSourceFileNode(node); } } ``` 通过简单地调用相应的 `emitXXX` 函数来完成递归,例如 `emitFunctionDeclaration` ```ts function emitFunctionDeclaration(node: FunctionLikeDeclaration) { if (nodeIsMissing(node.body)) { return emitOnlyPinnedOrTripleSlashComments(node); } if (node.kind !== SyntaxKind.MethodDeclaration && node.kind !== SyntaxKind.MethodSignature) { // 会把注释当做方法声明的一部分去发射。 emitLeadingComments(node); } // 目标为 es6 之前时,使用 function 关键字来发射类函数(functions-like)声明,包括箭头函数 // 目标为 es6 时,可以发射原生的 ES6 箭头函数,并使用宽箭头代替 function 关键字. if (!shouldEmitAsArrowFunction(node)) { if (isES6ExportedDeclaration(node)) { write('export '); if (node.flags & NodeFlags.Default) { write('default '); } } write('function'); if (languageVersion >= ScriptTarget.ES6 && node.asteriskToken) { write('*'); } write(' '); } if (shouldEmitFunctionName(node)) { emitDeclarationName(node); } emitSignatureAndBody(node); if ( languageVersion < ScriptTarget.ES6 && node.kind === SyntaxKind.FunctionDeclaration && node.parent === currentSourceFile && node.name ) { emitExportMemberAssignments((node).name); } if (node.kind !== SyntaxKind.MethodDeclaration && node.kind !== SyntaxKind.MethodSignature) { emitTrailingComments(node); } } ``` ## 发射器源映射(SourceMaps) 如前所述 `emitter.ts` 中的大部分代码是函数 `emitJavaScript`(我们之前展示过该函数的初始化例程)。 它主要是设置一批本地变量并交给 `emitSourceFile` 处理。下面我们再看一遍这个函数,这次我们重点关注 `SourceMap` 的部分: ```ts function emitJavaScript(jsFilePath: string, root?: SourceFile) { // 无关代码 ........... 已移除 let writeComment = writeCommentRange; /** 将发射的输出写到磁盘上 */ let writeEmittedFiles = writeJavaScriptFile; /** 发射一个节点 */ let emit = emitNodeWithoutSourceMap; /** 节点发射前调用 */ let emitStart = function (node: Node) { }; /** 节点发射完成后调用 */ let emitEnd = function (node: Node) { }; /** 从 startPos 位置开始,为指定的 token 发射文本。默认写入的文本由 tokenKind 提供, * 但是如果提供了可选的 emitFn 回调,将使用该回调来代替默认方式发射文本。 * @param tokenKind 要搜索并发射的 token 的类别 * @param startPos 源码中搜索 token 的起始位置 * @param emitFn 如果给出,会被调用来进行文本的发射。*/ let emitToken = emitTokenText; /** 该函数因为节点,会在发射的代码中于函数或类中启用词法作用域前调用 * @param scopeDeclaration 启动词法作用域的节点 * @param scopeName 可选的作用域的名称,而不是从节点声明中推导 */ let scopeEmitStart = function(scopeDeclaration: Node, scopeName?: string) { }; /** 出了作用域后调用 */ let scopeEmitEnd = function() { }; /** 会被编码的 Sourcemap 数据 */ let sourceMapData: SourceMapData; if (compilerOptions.sourceMap || compilerOptions.inlineSourceMap) { initializeEmitterWithSourceMaps(); } if (root) { // 不要直接调用 emit,那样不会设置 currentSourceFile emitSourceFile(root); } else { forEach(host.getSourceFiles(), sourceFile => { if (!isExternalModuleOrDeclarationFile(sourceFile)) { emitSourceFile(sourceFile); } }); } writeLine(); writeEmittedFiles(writer.getText(), /*writeByteOrderMark*/ compilerOptions.emitBOM); return; ``` 重要的函数调用: `initializeEmitterWithSourceMaps`,该函数是 `emitJavaScript` 的本地函数,它覆盖了部分已定义的本地函数。 覆盖的函数可以在 `initalizeEmitterWithSourceMap` 的底部找到: ```ts // `initializeEmitterWithSourceMaps` 函数的最后部分 writeEmittedFiles = writeJavaScriptAndSourceMapFile; emit = emitNodeWithSourceMap; emitStart = recordEmitNodeStartSpan; emitEnd = recordEmitNodeEndSpan; emitToken = writeTextWithSpanRecord; scopeEmitStart = recordScopeNameOfNode; scopeEmitEnd = recordScopeNameEnd; writeComment = writeCommentRangeWithMap; ``` 就是说大部分的发射器代码不关心 `SourceMap`,它们以相同的方式使用这些(带或不带 SourceMap 的)本地函数。 ================================================ FILE: docs/compiler/overview.md ================================================ # 概览 TypeScript 编译器源文件位于 [`src/compiler`](https://github.com/Microsoft/TypeScript/tree/master/src/compiler) 目录下 > 译注:Typescript Deep Dive 使用的源码应为 2016 年以前的源码。学习时请对照现有的源码 它分为以下几个关键部分: - Scanner 扫描器(`scanner.ts`) - Parser 解析器(`parser.ts`) - Binder 绑定器(`binder.ts`) - Checker 检查器(`checker.ts`) - Emitter 发射器(`emitter.ts`) 每个部分在源文件中均有独立文件,本章稍后会对这些部分做解释。 ### BYOTS 我们有个名为 [Bring Your Own TypeScript (BYOTS)](https://github.com/basarat/byots) 的项目,通过暴露内部接口让编译器 API 使用起来更简单。你可以在全局范围上暴露你 TypeScript 应用的本地变量。 ### 语法和语义 *语法*正确并不意味着*语义*上也正确。下面的 TypeScript 代码,语法合法,但是语义却不正确 ```ts var foo: number = 'not a number'; ``` `语义` 从自然语言角度意味着有意义,理解这个概念对你很有用。 ### 处理概览 以下演示简单说明 TypeScript 编译器如何将上述几个关键部分组合在一起: ```code SourceCode(源码) ~~ 扫描器 ~~> Token 流 ``` ```code Token 流 ~~ 解析器 ~~> AST(抽象语法树) ``` ```code AST ~~ 绑定器 ~~> Symbols(符号) ``` 符号(`Symbol`)是 TypeScript *语义*系统的主要构造块。如上所示,符号是绑定的结果。符号将 AST 中的声明节点与相同实体的其他声明相连。 符号和 AST 是检查器用来验证源代码*语义*的 ```code AST + 符号 ~~ 检查器 ~~> 类型验证 ``` 最后,需要输出 JavaScript 时: ```code AST + 检查器 ~~ 发射器 ~~> JavaScript 代码 ``` TypeScript 编译器中还有一些其他文件,为我们接下来介绍的很多关键部分提供实用工具。 ## 文件:Utilities `core.ts` :TypeScript 编译器使用的核心工具集,重要的有: - `let objectAllocator: ObjectAllocator` 是一个定义为全局单例的变量。提供以下定义: - `getNodeConstructor`(节点会在解析器 / AST 中介绍) - `getSymbolConstructor`(符号会在绑定器中介绍) - `getTypeConstructor`(类型会在检查器中介绍) - `getSignatureConstructor`(签名是索引,调用和构造签名) ## 文件:关键数据结构 `types.ts` 包含整个编译器中使用的关键数据结构和接口,这里列出一些关键部分: - `SyntaxKind` AST 节点类型通过 `SyntaxKind` 枚举进行识别 - `TypeChecker` 类型检查器提供此接口 - `CompilerHost` 用于程序(`Program`)和系统之间的交互 - `Node` AST 节点 ## 文件:系统 `system.ts`,TypeScript 编译器与操作系统的所有交互均通过 `System` 接口进行。接口及其实现(`WScript` 和 `Node`) 均定义在 `system.ts` 中。你可以将其视为*操作环境(OE, Operating Environment)*。 现在对主要文件有一个整体了解了,我们继续介绍程序([`Program`](./program.md))的概念 ================================================ FILE: docs/compiler/parser.md ================================================ # 解析器 TypeScript 解析器代码均位于 `parser.ts` 中。在内部,由解析器控制扫描器将源码转化为 AST。其期望结果如下: ``` 源码 ~~ 扫描器 ~~> Token 流 ~~ 解析器 ~~> AST ``` 解析器实现原理是单例模式(其原因类似扫描器,如果能重新初始化就不重新构建)。实际实现成 `namespace Parser`,包含解析器的各种*状态*变量和单例扫描器(`const scanner`)。该扫描器由解析器函数管理。 ### 程序对解析器的使用 解析器由程序间接驱动(通过之前提到过的 `CompilerHost`)。基本上,简化的调用栈如下所示: ``` 程序 -> CompilerHost.getSourceFile -> (全局函数 parser.ts).createSourceFile -> Parser.parseSourceFile ``` `parseSourceFile` 不仅准备好解析器的状态,还调用 `initializeState` 准备好扫描器的状态。然后使用 `parseSourceFileWorker` 继续解析源代码。 ### 使用示例 深入解析器的内部之前,这里有个使用 TypeScript 解析器的示例,(使用 `ts.createSourceFile`)获取一个源文件的 AST 并打印它。 `code/compiler/parser/runParser.ts` ```ts import * as ts from 'ntypescript'; function printAllChildren(node: ts.Node, depth = 0) { console.log(new Array(depth + 1).join('----'), ts.formatSyntaxKind(node.kind), node.pos, node.end); depth++; node.getChildren().forEach(c => printAllChildren(c, depth)); } var sourceCode = ` var foo = 123; `.trim(); var sourceFile = ts.createSourceFile('foo.ts', sourceCode, ts.ScriptTarget.ES5, true); printAllChildren(sourceFile); ``` 该段代码会打印以下内容: ```ts SourceFile 0 14 ---- SyntaxList 0 14 -------- VariableStatement 0 14 ------------ VariableDeclarationList 0 13 ---------------- VarKeyword 0 3 ---------------- SyntaxList 3 13 -------------------- VariableDeclaration 3 13 ------------------------ Identifier 3 7 ------------------------ FirstAssignment 7 9 ------------------------ FirstLiteralToken 9 13 ------------ SemicolonToken 13 14 ---- EndOfFileToken 14 14 ``` 如果把头向左倾,这个看起来像棵(右侧)树 ## 解析器函数 如前所述,`parseSourceFile` 设置初始状态并将工作交给 `parseSourceFileWorker` 函数。 ### `parseSourceFileWorker` 该函数先创建一个 `SourceFile` AST 节点,然后从 `parseStatements` 函数开始解析源代码。一旦返回结果,就用额外信息(例如 `nodeCount`, `identifierCount`等) 完善 `SourceFile` 节点。 ### `parseStatements` 是最重要的 `parseXXX` 系函数之一(概念接下来介绍)。它根据扫描器返回的当前 token 来切换(调用相应的 `parseXXX` 函数),例如:如果当前 token 是一个 `SemicolonToken`(分号标记),就会调用 `paserEmptyStatement` 为空语句创建一个 AST 节点。 ### 节点创建 解析器有一系列 `parseXXX` 函数用来创建相应类型为`XXX`的节点,通常在相应类型的节点出现时被(其他解析器函数)调用。该过程的典型示例是解析空语句(例如 `;;;;;;`)时要用的 `parseEmptyStatement()` 函数。下面是其全部代码: ```ts function parseEmptyStatement(): Statement { let node = createNode(SyntaxKind.EmptyStatement); parseExpected(SyntaxKind.SemicolonToken); return finishNode(node); } ``` 它展示了 3 个关键函数 `createNode`, `parseExpected` 和 `finishNode`. #### `createNode` 解析器函数 `function createNode(kind: SyntaxKind, pos?: number): Node` 负责创建节点,设置传入的 `SyntaxKind`(语法类别),和初始位置(默认使用当前扫描器状态提供的位置信息)。 #### `parseExpected` 解析器的 `parseExpected` 函数 `function parseExpected(kind: SyntaxKind, diagnosticMessage?: DiagnosticMessage): boolean` 会检查解析器状态中的当前 token 是否与指定的 `SyntaxKind` 匹配。如果不匹配,则会向传入的 `diagnosticMessage`(诊断消息)报告,未传入则创建某种通用形式 `xxx expected`。该函数内部用 `parseErrorAtPosition` 函数(使用扫描位置)提供良好的错误报告。 #### `finishNode` 解析器的 `finishNode` 函数 `function finishNode(node: T, end?: number): T` 设置节点的 `end` 位置,并添加一些有用的信息,例如上下文标志(`parserContextFlags`)以及解析该节点前出现的错误(如果有错的话,就不能在增量解析中重用此 AST 节点)。 ================================================ FILE: docs/compiler/program.md ================================================ # 程序 程序定义在 `program.ts` 中。[编译上下文](../project/compilationContext.md)在 TypeScript 编译器中被视为一个 `Program`,它包含 `SourceFile` 和编译选项 ## `CompilerHost` 的使用 CompilerHost 是与操作环境(OE, Operating Enviornment)进行交互的机制: `Program` _-使用->_ `CompilerHost` _-使用->_ `System` 用 `CompilerHost` 作中间层的原因是可以让接口对 `Program` 的需求进行细粒度的调整,而无需考虑操作环境的需求。(例如:`Program` 无需关心 `System` 的 `fileExists` 函数) 对`System`而言还有其他的使用者(比如测试) ## SourceFile 程序有个 API,用于获取 SourceFile:`getSourceFiles(): SourceFile[];`。得到的每个元素均是一棵抽象语法树的根节点(称做 `SourceFile`) ================================================ FILE: docs/compiler/scanner.md ================================================ # 扫描器 TypeScript 扫描器的源码均位于 `scanner.ts`。在内部,由解析器*控制*扫描器将源码转化为抽象语法树(AST)。期望结果如下: ``` SourceCode ~~ 扫描器 ~~> Token 流 ~~ 解析器 ~~> AST ``` ## 解析器对扫描器的使用 为避免重复创建扫描器造成的开销,`parser.ts` 中创建了一个扫描器的*单例*。解析器根据需要使用 `initializeState` 函数*准备*该扫描器。 下面是解析器中的实际代码的简化版,你可以运行它演示以上概念 `code/compiler/scanner/runScanner.ts` ```ts import * as ts from 'ntypescript'; // 单例扫描器 const scanner = ts.createScanner(ts.ScriptTarget.Latest, /* 忽略杂项 */ true); // 此函数与初始化使用的 `initializeState` 函数相似 function initializeState(text: string) { scanner.setText(text); scanner.setOnError((message: ts.DiagnosticMessage, length: number) => { console.error(message); }); scanner.setScriptTarget(ts.ScriptTarget.ES5); scanner.setLanguageVariant(ts.LanguageVariant.Standard); } // 使用示例 initializeState( ` var foo = 123; `.trim() ); // 开始扫描 var token = scanner.scan(); while (token != ts.SyntaxKind.EndOfFileToken) { console.log(ts.formatSyntaxKind(token)); token = scanner.scan(); } ``` 该段代码输出以下内容: ``` VarKeyword Identifier FirstAssignment FirstLiteralToken SemicolonToken ``` ## 扫描器状态 调用 `scan` 后,扫描器更新其局部状态(扫描位置,当前 token 详情等)。扫描器提供了一组工具函数获取当前扫描器状态。下例中,我们创建一个扫描器并用它识别 token 以及 token 在代码中的位置。 `code/compiler/scanner/runScannerWithPosition.ts` ```ts // 使用示例 initializeState( ` var foo = 123; `.trim() ); // 开始扫描 var token = scanner.scan(); while (token != ts.SyntaxKind.EndOfFileToken) { let currentToken = ts.formatSyntaxKind(token); let tokenStart = scanner.getStartPos(); token = scanner.scan(); let tokenEnd = scanner.getStartPos(); console.log(currentToken, tokenStart, tokenEnd); } ``` 该代码输出以下内容: ``` VarKeyword 0 3 Identifier 3 7 FirstAssignment 7 9 FirstLiteralToken 9 13 SemicolonToken 13 14 ``` ## 独立扫描器 即便 TypeScript 解析器有单例扫描器,你仍可以使用 `createScanner` 创建独立的扫描器,然后可以用 `setText`/`setTextPos` 随意扫描文件的不同位置。 ================================================ FILE: docs/error/common.md ================================================ # 常见的 Error 在此章节中,我们学习在实际应用中将会遇到的常见错误代码。 ## TS2304 例子: > `Cannot find name ga`, `Cannot find name $`, `Cannot find module jquery` 你可能在使用第三方的库(如:google analytics),但是你并没有 `declare` 的声明。在没有声明它们之前,TypeScript 试图避免错误和使用变量。因此在使用一些额外的库时,你需要明确的声明使用的任何变量([如何修复它](../typings/ambient.md))。 ## TS2307 例子: > `Cannot find module 'underscore'` 你可能把第三方的库作为模块([移步模块](../project/modules.md))来使用,并且没有一个与之对应的环境声明文件([更多声明文件信息](../typings/ambient.md))。 ## TS1148 例子: > `Cannot compile modules unless the '--module' flag provided` 请查看[模块](../project/modules.md)章节 ## 捕获不能有类型注解的简短变量 例子: ```ts try { something(); } catch (e) { // 捕获不能有类型注解的简短变量 // ... } ``` TypeScript 正在保护你免受 JavaScript 代码的侵害,取而代之,使用类型保护: ```ts try { something(); } catch (e) { // 捕获不能有类型注解的简短变量 if (e instanceof Error) { // do... } } ``` ## 接口 `ElementClass` 不能同时扩展类型别名 `Component` 和 `Component` 当在编译上下文中同时含有两个 `react.d.ts`(`@types/react/index.d.ts`)会发生这种情况。 修复: - 删除 `node_modules` 和任何 `package-lock`(或者 `yarn lock`),然后再一次 `npm install`; - 如果这不能工作,查找无效的模块(你所使用的所用用到了 `react.d.ts` 模块应该作为 `peerDependency` 而不是作为 `dependency` 使用)并且把这个报告给相关模块。 ================================================ FILE: docs/error/interpreting.md ================================================ # 解读 Errors TypeScript 是一种专注于帮助开发人员的编程语言,当错误出现时,它会提供尽可能提供非常有用的错误信息。这对于那些信任使用者的编译器来说,可能会导致轻微的信息量过载,而不会那么实用。 让我们来看一个在 IDE 中的例子: ```ts type SomethingComplex = { foo: number; bar: string; }; function takeSomethingComplex(arg: SomethingComplex) {} function getBar(): string { return 'some bar'; } // 一个可能会出现的错误使用 const fail = { foo: 123, bar: getBar }; takeSomethingComplex(fail); // 在这里 TypeScript 会报错 ``` 这个简单的例子,演示了一个常见的程序设计错误,它调用函数失败(`bar: getBar` 应该是 `bar: getBar()`)。幸运的是,一旦不符合类型要求,TypeScript 将会捕捉到这个错误。 ## 错误分类 TypeScript 错误信息分为两类:简洁和详细。 ### 简洁 简洁的错误信息是为了提供一个编译器描述的错误号以及一些相关的信息,一个简洁的错误信息类似于如下所示: ```ts TS2345: Argument of type '{ foo: number; bar: () => string; }' is not assignable to parameter of type 'SomethingComplex'. ``` 然而,它没有提供更深层次的信息,如为什么这个错误会发生。这就是详细错误所需要的原因。 ## 详细 详细的错误信息类似于如下所示: ```ts [ts] Argument of type '{ foo: number; bar: () => string; }' is not assignable to parameter of type 'SomethingComplex'. Types of property 'bar' are incompatible. Type '() => string' is not assignable to type 'string'. ``` 详细的错误信息是为了指导使用者知道为什么一些错误(在这个例子里是类型不兼容)会发生。第一行与简洁的错误信息相同,后跟一些详细的信息。你应该阅读这些详细信息,因为对于开发者的一些疑问,它都给出了问答: ```ts ERROR: Argument of type '{ foo: number; bar: () => string; }' is not assignable to parameter of type 'SomethingComplex'. WHY? CAUSE ERROR: Types of property 'bar' are incompatible. WHY? CAUSE ERROR: Type '() => string' is not assignable to type 'string'. ``` 所以,最根本的原因是: - 在属性 `bar` - 函数 `() => string` 它应该是一个字符串。 这能够帮助开发者修复 bar 属性的 bug(它们忘记了调用这个函数)。 ## 在 IDE 中怎么提示 IDE 通常会在详细的错误提示之后显示简洁版本,如下所示: ide - 你通常可能只会阅读「为什么」的详细信息; - 当你想寻找相同的错误时(使用 `TSXXX` 错误编号,或者部分错误信息),使用简洁的版本。 ================================================ FILE: docs/faqs/class.md ================================================ # 类 ## 为什么这些空类的行为很奇怪? > 我写下这段代码,并期望它抛出错误 ```ts class Empty { /* empty */ } var e2: Empty = window; ``` 请参阅此问题「[为什么所有的内容都能赋值给空的接口](./type-system-behavior.html#为什么所有的类型,都能赋值给一个空的接口?)」。值得重新思考一下这个答案的建议:一般来说,你永远不应该声明一个没有属性的类。即使对于子类也是如此: ```ts class Base { important: number; properties: number; } class Alpha extends Base {} class Bravo extends Base {} ``` `Alpha` 和 `Bravo` 的结构相同,都是继承自 `Base`,这会产生许多令人惊讶的效果,所以别这么做。如果你想让 `Alpha` 与 `Bravo` 不相同,为它们各自提供一个属性。 ## 什么是名义上的类 这两段代码该如何解释: ```ts class Alpha { x: number; } class Bravo { x: number; } class Charlie { private x: number; } class Delta { private x: number; } let a = new Alpha(), b = new Bravo(), c = new Charlie(), d = new Delta(); a = b; // OK c = d; // Error ``` 在 TypeScript 中,类进行结构上的比较,有一个例外是对于 `private` 与 `protected` 的成员。当一个成员是 `private` 或者 `protected` 时,它们必须来自同一个声明,才能被视为与另一个 `private` 或者 `protected` 的成员相同。 ## 为什么在我的实例方法中,`this` 成了一个「孤儿」? > 我写下如下代码 ```ts class MyClass { x = 10; someCallback() { console.log(this.x); // Prints 'undefined', not 10 this.someMethod(); // Throws error "this.method is not a function" } someMethod() {} } let obj = new MyClass(); window.setTimeout(obj.someCallback, 10); ``` 可能会提出一些相似的问题: - 为什么在我的回调函数中类的属性没有定义? - 为什么在我的回调函数中,`this` 指向 `window`? - 为什么在我的回调函数中,`this` 指向 `undefined`? - 为什么我会得到 `this.someMethod is not a function` 的错误? - 为什么我会得到 `Cannot read property 'someMethod' of undefined` 的错误? 在 JavaScript 中,`this` 值由以下确定: 1. 该函数是调用 `.bind` 的结果吗?如果是这样,`this` 由传递给 `bind` 的第一个参数确定 2. 该函数是通过属性表达式 `expr.method() ?` 直接调用吗?如果是这样,`this` 指向 `expr` 3. 否则,`this` 是 `undefined`(在严格模式中),或者是 `window` (非严格模式中)。 在上一个例子中,影响结果的是这行代码: ```ts window.setTimeout(obj.someCallback, 10); ``` 在这里,我们提供了 `obj.someCallback` 到 `setTimeout` 的函数引用,然后该函数并不是作为 `bind` 的结果调用,也不是直接作为一个方法调用。因此在 `someCallback` 里的 `this` 指向 `window`(或者在严格模式下的 `undefied`)。 这里概述了一些解决办法:http://stackoverflow.com/a/20627988/1704166 ## 当 `Bar` 是一个 `class` 时,`Bar` 和 `typeof Bar` 有什么区别? > 我写下这段代码,但是我不理解我为什么会得到错误: ```ts class MyClass { someMethod() {} } var x: MyClass; // Cannot assign 'typeof MyClass' to MyClass? Huh? x = MyClass; ``` 在 JavaScript 中,类仅仅是个函数,这点很重要。我们将类对象本身 -- `MyClass` 的值,作为是构造函数。当一个构造函数被 `new` 调用时,我们得到一个对象,它是该类的实例。 因此,当我们定义一个类时,实际上,我们定义了两个不同的类型。 第一个是由类的名字推导而来,在这个例子中是 `MyClass`。这个是类实例的类型,它定义了类的实例具有的属性和方法,它是一个通过调用类的构造函数来返回的类型。 第二个类型是一个匿名的类型,它是构造函数具有的类型。它包含一个返回类实例的构造函数签名(可以使用 `new` 调用),同时,它也包含类中可能含有的 `static` 属性和方法。它也通常被称为「静态方面」,因为它包含那些静态成员(以及作为类的构造函数)。我们可以用 `typeof` 来引用此类型。 当在类型位置使用 `typeof` 操作符时,描述了表达式的类型。因此 `typeof MyClass` 是指 `MyClass` 的类型。 ## 为什么我的子类属性初始值设定会覆盖基类构造函数中设置的值? 有关此问题,和其他初始化顺序问题,请参阅 [#1617](https://github.com/Microsoft/TypeScript/issues/1617)。 ## 声明类和接口有什么区别? 参阅: http://stackoverflow.com/a/14348084/1704166 ## 接口继承类,意味着什么? > 这段代码是什么意思? ```ts class Foo { /* ... */ } interface Bar extends Foo {} ``` 这创建了一个名叫 `Bar` 的类型,它与 `Foo` 的实例具有相同的成员。当 `Foo` 具有私有成员时,`Bar` 内的相同属性,必须由一个继承自 `Foo` 的类实现。总的来说,这种模式是应当避免的,尤其是在 `Foo` 有私有成员时。 ## 为什么我会得到错误:`TypeError: [base class name] is not defined in __extends`? > 我写下一段代码, ```ts /** file1.ts **/ class Alpha { /* ... */ } /** file2.ts **/ class Bravo extends Alpha { /* ... */ } ``` 在运行时,有如下错误发生在 `_extends` 中: ```ts Uncaught TypeError: Alpha is not defined ``` 最常见的原因是在你的 HTML 中包含有 file2.ts 的 `script`,但是并没有包含 `file1.ts` 的 `script`。因此你需要在引用 `file2.ts` 之前引用 `file1.ts`。 ## 为什么我会得到 `TypeError: Cannot read property 'prototype' of undefined" in __extends` 的错误? > 我写下如下代码: ```ts /** file1.ts **/ class Alpha { /* ... */ } /** file2.ts **/ class Bravo extends Alpha { /* ... */ } ``` 在运行时,有如下错误发生在 `_extends` 中: ```ts Uncaught TypeError: Cannot read property 'prototype' of undefined ``` 出现这种情况,原因可能有一些。 首先,在单个文件中,你在基类之前定义了派生类,那么你应该重新排序文件,以便在派生类之前声明基类。 如果你使用了 `--out` 的编译选项,编译器可能会对你希望文件的顺序感到困惑。请参阅常见问题简答中「如果控制文件排序」部分 如果您没有使用 `--out`,HTML 文件中的 `script` 引用文件的顺序可能出现错误。重新排序 `script` 对文件的引用,以便在定义派生类的文件之前包含定义基类的文件。 最后,如果你使用某种类型的第三方包,该包可能会错误地排序了文件。请参阅该工具的文档以了解如何在结果输出中正确排序输入文件。 ## 为什么不扩展 `Error`、`Array`、`Map` 内置函数? 在 ES2015 中,返回一个对象的构造函数将 `this` 的值隐式替换为 `super(...)` 的任何调用者。这对于构造函数代码捕获 `super(...)` 的任何潜在返回值并将其替换为 `this` 是必要的。 这样导致的结果是:`Error`、`Array` 等子类将不再按预期工作。这是由于 `Error`、`Array` 等的构造函数使用 ECMAScript6 中的 `new.target` 来调整原型链。但是,在 ECMAScript 5 中调用构造函数时,无法确保 `new.target` 的值。在其他一些低水平的编译器通常都有相同的限制。 ### 例如: 如下作为一个子类: ```ts class FooError extends Error { constructor(m: string) { super(m); } sayHello() { return 'hello ' + this.message; } } ``` 你可能会发现: - 通过这些子类的构造函数返回的对象中,方法可能是 `undefined`。因此,当调用 `sayHello` 时,会抛出一个错误。 - `instanceof` 将会在子类的实例和自身实例中被中断。因此 `new FooError() instanceof FooError` 将返回 `false`。 ### 推荐 作为一个推荐方式,你可以在 `super(...)` 被调用之后手动调整原型。 ```ts class FooError extends Error { constructor(m: string) { super(m); // Set the prototype explicitly. Object.setPrototypeOf(this, FooError.prototype); } sayHello() { return 'hello ' + this.message; } } ``` 然而,任何 `FooError` 的子类将不得不手动设置原型。在运行时,对于那些不支持 `Object.setPrototypeOf` 属性的,你可能用要 `__proto__` 来替代他。 不幸的是,[IE 10 及其一下不兼容这些方法](https://docs.microsoft.com/zh-cn/microsoft-edge/dev-guide/whats-new/javascript-version-information)。你可以手动将原型中的方法复制到实例本身,(例如:`FooError.prototype` 复制到 `this` 上),但是对于原型链本身是无法修复的。 ================================================ FILE: docs/faqs/commandline-behavior.md ================================================ # 命令行的行为 ## 如何控制输出文件中的排序(-- out)? 输出文件的排序遵循预处理后输入文件的顺序。 编译器执行预处理,主要是为了解决所有的三斜线指令和模块导入。在这个过程中,额外的文件将会被将入到编译过程中。 这个过程开始于一个给定的根文件,这些是在命令行或者是 `tsconfig.json` 文件中 files 指定文件名,这些根文件按照指定的顺序进行预处理。在一个文件添加到这个列表之前,将处理所有的三斜线引用和模块导入语法,并包括它们的目标。三斜线引用和导入语法按照它们在文件中出现的顺序,以深度优先的方式解析。 请参考有关[三斜线指令](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html)的更多信息,和[模块](https://www.typescriptlang.org/docs/handbook/module-resolution.html)导入语法的信息。 ## `Exported variable [name] has or is using private name [name]` 是什么错误? 当你使用 `--declarartion` 编译选项的时候,可能会出现这个错误,因为编译器试图生成与你定义模块完全匹配的声明文件: 假设你有这样一段代码: ```ts /// MyFile.ts class Test { // ... other members .... constructor(public parent: Test) {} } export let t = new Test('some thing'); ``` 为了生成声明文件,编译器必须为 `t` 写一个类型: ```ts /// MyFile.d.ts, auto-generated export let t: ___fill in the blank___; ``` 成员 `t` 有类型 `Test`,但是类型 `Test` 并不是可见的,因为它没有导出,因此我们不能写 `t: Test`。 在这个非常简单的例子里,我们可以用一个对象字面量重写 `Test's` 的形状。但是对于绝大多数情况,这并不能正常工作。如代码里所写,Test 的形状是自引用的,不能重写为匿名函数。如果 `Test` 有任何私有或受保护的成员,这同样也不能正常工作。因此,与其让你通过编写一个真实的类来获得 65% 的成功而后开始抛出错误,我们仅仅是在一开始的时候就抛出错误(你以后会发现)并为你省去不必要的麻烦。 为了避免这些错误: - 导出相关类型中使用的声明 - 当编写声明的时候,显示的为编译器指定类型注解 ## 为什么添加 `--outDir` 属性后,当在添加一个新文件时,会把所有的输出删除 `--outDir` 指定输出的「根」目录。编译器需要此属性,用来将资源映射输出到根目录。如果 `--rootDir` 没有被指定,编辑器将会自己计算出一个。它根据常见的路径计算,它是所有输入文件的最长公共前缀。显然,当在较短路径前缀中添加新文件时,`--rootDir` 将会被修改。 为了确保添加一个新文件时,输出不会被修改,你应该在命令行中或 `tsconfig.json` 指定一个 `--rootDir`。 ================================================ FILE: docs/faqs/comments.md ================================================ # 评论 ================================================ FILE: docs/faqs/common-bug-not-bugs.md ================================================ # 一些常见的「bug」并不是 bug > 注:此章节的所有文章都来自 [TypeScript FAQs](https://github.com/Microsoft/TypeScript/wiki/FAQ) 这有一些看起来像 Bug 的行为,但实际上,它们并不是。 - 两个空的类,可以彼此代替 - 查看相关的 [FAQ](./class.html#为什么这些空类的行为很奇怪?) - 我可以在一个返回值为 void 的函数中使用一个返回值不为 `void` 的函数 - 查看相关的 [FAQ](./type-system-behavior.html#为什么一个返回值不是-void-的函数,可以赋值给一个返回值为-void-的函数?) - 查看此 [ISSUES](https://github.com/Microsoft/TypeScript/issues/4544) - 我可以使用一个更短的参数列表,而不是一个期望的长参数列表 - 查看相关 [FAQ](./type-system-behavior.html#为什么有更少参数的函数能够赋值给更多参数的函数?) - 相关 ISSUES:[#370](https://github.com/Microsoft/TypeScript/issues/370)、[#9300](https://github.com/Microsoft/TypeScript/issues/9300)、[#9765](https://github.com/Microsoft/TypeScript/issues/9765)、[#9825](https://github.com/Microsoft/TypeScript/issues/9825)、[#13043](https://github.com/Microsoft/TypeScript/issues/13043)、[#16871](https://github.com/Microsoft/TypeScript/issues/16871)、[#13529](https://github.com/Microsoft/TypeScript/issues/13529)、[#13977](https://github.com/Microsoft/TypeScript/issues/13977)、[#17868](https://github.com/Microsoft/TypeScript/issues/17868)、[#20274](https://github.com/Microsoft/TypeScript/issues/20274)、[#20541](https://github.com/Microsoft/TypeScript/issues/20541)、[#21868](https://github.com/Microsoft/TypeScript/issues/21868)。 - 类的 `private` 成员,在运行时实际上是可见的 - 查看相关 FAQ,以及一些修复的建议 - 相关 ISSUES:[#564](https://github.com/Microsoft/TypeScript/issues/564)、[#1537](https://github.com/Microsoft/TypeScript/issues/1537)、[#2967](https://github.com/Microsoft/TypeScript/issues/2967)、[#3151](https://github.com/Microsoft/TypeScript/issues/3151)、[#6748](https://github.com/Microsoft/TypeScript/issues/6748)、[#8847](https://github.com/Microsoft/TypeScript/issues/8847)、[#9733](https://github.com/Microsoft/TypeScript/issues/9733)、[#11033](https://github.com/Microsoft/TypeScript/issues/11033)。 ================================================ FILE: docs/faqs/common-feature-request.md ================================================ # 一些常见的 Feature 需求 这有一些常见的 Feature 列表以及相应的 ISSUES,在提新的 ISSUES 之前,如果有相同的 Feature,请在相应的评论区留下评论 - 安全的导航操作符 [#16](https://github.com/Microsoft/TypeScript/issues/16) - 最小化 [#8](https://github.com/Microsoft/TypeScript/issues/8) - 局部的 classes [#563](https://github.com/Microsoft/TypeScript/issues/563) - 与 `this` 相关 [#531](https://github.com/Microsoft/TypeScript/issues/531) - 函数成员 `call/bind/apply` 的强类型 [#212](https://github.com/Microsoft/TypeScript/issues/531) - 运行时的函数重载 [#3422](https://github.com/Microsoft/TypeScript/issues/531) ================================================ FILE: docs/faqs/decorators.md ================================================ # Decorators ================================================ FILE: docs/faqs/enums.md ================================================ # 枚举 ## `enum` 和 `const enum` 之间的区别是什么? TODO:`enum` / `const enum` 有如下区别,请看:[https://www.typescriptlang.org/docs/handbook/enums.html#enums](https://www.typescriptlang.org/docs/handbook/enums.html#enums) ================================================ FILE: docs/faqs/function.md ================================================ # 函数 ## 为什么我不能在解构函数 `function f({ x: number }) { /* ... */ }` 中使用 `x`? > 我写下这单代码,但是得到了一个错误 ```ts function f({ x: number }) { // Error, x is not defined? console.log(x); } ``` 对于那些习惯于查看 TypeScript 类型字面量的人来说,解构语法是有悖常理的。语法 `f({ x: number })` 声明了属性名从 `x` 转换为 `number` 名的解构。 让我们从发出的代码来收到启发: ```ts function f(_a) { // Not really what we were going for var number = _a.x; } ``` 为了能让这段代码正确运行,你需要写下: ```ts function f({ x }: { x: number }) { // OK console.log(x); } ``` 如果你想为所有属性提供一个初始变量,最合适的写法是: ```ts function f({ x = 0 }) { // x: number console.log(x); } ``` ================================================ FILE: docs/faqs/generics.md ================================================ # 泛型 ## 通过接口 `A`,为什么 `A` 可赋值给 `A`? > 我写下这段代码,让它抛出一个错误。 ```typescript interface Something { name: string; } let x: Something; let y: Something; // Expected error: Can't convert Something to Something! x = y; ``` `TypeScript` 使用了一种结构类型的系统。当判断 `Something` 和 `Something` 兼容性的时候,我们会检查每一个成员的每一个属性,如果类型的每个成员都是兼容的,那么这个类型也是兼容的。因为 `Something` 没有在任何成员中使用 `T`,所以 `T` 是什么类型并不重要。 通常,你绝不应该有未使用类型的参数。该类型会有无法预料的兼容性(如上所示),同时在函数调用中也无法获取正确的泛型类型接口。 ## 为什么类型接口不能在这个接口上运行: `interface Foo { }`? > 我写了一些这样的代码 ```typescript interface Named { name: string; } class MyNamed implements Named { name: 'mine'; } function findByName(x: Named): T { // TODO: Implement return undefined; } var x: MyNamed; var y = findByName(x); // expected y: string, got y: {} ``` `TypeScript` 使用了一种结构类型的系统。这种结构性也适用于泛型类型接口。当在函数调用中推断 `T` 的类型时,我们试图在 `x` 参数上找到 `T` 类型的成员,从而判断 `T` 应该是什么。因为没有使用 `T` 的成员,所以没有什么可推断的,于是我们返回 `{}`。 请注意,如果你使用 `T`,你就会得到正确的结果: ```typescript interface Named { name: string; value: T; // <-- added } class MyNamed implements Named { name: 'mine'; value: T; // <-- added } function findByName(x: Named): T { // TODO: Implement return undefined; } var x: MyNamed; var y = findByName(x); // got y: string; ``` 记住:你绝不应该有未使用类型的参数!请看前一个问题,了解为什么这样不好。 ## 为什么不要在泛型函数中写 `typeof T`、`new T`, 或者 `instanceof T`? > 我写了一些这样的代码 ```typescript function doSomething(x: T) { // Can't find name T? let xType = typeof T; let y = new xType(); // Same here? if(someVar instanceof typeof T) { } // How do I instantiate? let z = new T(); } ``` 泛型在编译期间被删除,这意味着在 `doSomething` 运行时没有值为 `T` 。这里人们试图表达的正常模式是将类的构造函数用于工厂或运行时类型检查。。在这两种情况下,使用构造签名并将其作为参数提供是正确的: ```typescript function create(ctor: { new(): T }) { return new ctor(); } var c = create(MyClass); // c: MyClass function isReallyInstanceOf(ctor: { new(...args: any[]): T }, obj: T) { return obj instanceof ctor; } ``` ================================================ FILE: docs/faqs/glossary-and-terms.md ================================================ # 术语表 ================================================ FILE: docs/faqs/jsx-and-react.md ================================================ # JSX 和 React ## 我写了声明 `declare var MyComponent: React.Component`,为什么我不能写 `` > 我写下了如下代码,为什么会抛出错误? ```ts class Display extends React.Component { render() { ... } } let SomeThing: Display = /* ... */; // Error here, isn't this OK? let jsx = ; ``` 这可能是把类的实例与静态类混淆了。当 React 实例化一个组件时,它在调用构造函数。因此当 TypeScript 看到一个 JSX 标签 `` 时,它在验证构造函数 `TagName` 的结果是否可以产生有效组件。 但是这个声明 `let someThing: Display` 只是表明了 `someThing` 是类的实例,并不是类的构造函数。实际上,他会在运行时抛出错误: ```ts let SomeThing = new Display(); let jsx = ; // Not gonna work ``` 最简单的修复方式是使用 `typeof` 操作符: ```ts let SomeThing: typeof Display = /* ... */; ``` ================================================ FILE: docs/faqs/modules.md ================================================ # 模块 ## 为什么我导入的模块在编译后被删除了? > 我写了一些这样的代码 ```typescript import someModule = require('./myMod'); let x: someModule.SomeType = /* something */; ``` > 有这样的输出 ```typescript // Expected to see "var someModule = require('./myMod');" here! var x = /* something */; ``` `TypeScript` 假定导入的模块没有副作用,所以它移除了不用于任何表达式的模块导入。 使用 `import "mod"` 语法来强制加载模块 ```typescript import './myMod'; // For side effects ``` 你也可以简单调用模块,这是最常见的解决办法。 ```typescript import someModule = require('./myMod'); someModule; // Used for side effects ``` ## 为什么不跨模块文件合并命名空间? TODO:本小节内容请查看:[https://stackoverflow.com/questions/30357634/how-do-i-use-namespaces-with-typescript-external-modules](https://stackoverflow.com/questions/30357634/how-do-i-use-namespaces-with-typescript-external-modules) 或者 [https://www.typescriptlang.org/docs/handbook/release-notes/typescript-1-5.html#namespace-keyword](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-1-5.html#namespace-keyword) ================================================ FILE: docs/faqs/thing-that-dont-work.md ================================================ # 一些不能按预期工作的代码 ## 你应该像这样发出一些类,以便于他们拥有真正的私有成员 > 如果我写下一些以下代码 ```ts class Foo { private x = 0; increment(): number { this.x++; return x; } } ``` > 你应该发出这样的代码,以便 `x` 是真正的私有成员: ```ts var Foo = (function() { var x = 0; function Foo() {} Foo.prototype.increment = function() { x++; return x; }; return Foo; })(); ``` 这些代码不会工作,它创建了一个所有类共享的单个私有字段: ```ts var a = new Foo(); a.increment(); // Prints 1 a.increment(); // Prints 2 var b = new Foo(); // Should not affect a a.increment(); // Prints 1 ``` ## 你应该发出这样的类,这样它们就不会在回调函数中丢失 `this` > 如果我写下这样的代码 ```ts class MyClass { method() {} } ``` > 你应该发出这样的代码,以便我不会在回调函数中丢失 `this` ```ts var MyClass = (function() { function MyClass() { this.method = function() {}; } return MyClass; })(); ``` 这里有两个问题: 首先,建议改变的行为与 ECMAScript 规范不一致。在这方面没有任何异议 -- TypeScript 必须与 JavaScript 具有相同的运行时行为。 其次,这个运行时类的特点非常令人惊讶。它为每个实例的每个方法创建一个闭包,而不是为每个方法创建一个闭包,这在初始化时,内存、以及垃圾回收上的性能都非常糟糕。 ================================================ FILE: docs/faqs/tsconfig-behavior.md ================================================ # `tsconfig.json` 的行为 ## 为什么把一个文件放入「exclude」选项中,它仍然会被编译器选中? `tsconfig.json` 将会把一个文件夹转换为「项目」,如果不指定任何 `exclude` 或者 `files`,则包含在 `tsconfig.json` 中的所有文件夹中的所有文件都会被包含在编译中。 如果你想忽略一些文件,使用 `exclude`。如果希望指定所有文件,而不是让编译器查找它们,请使用 `files`。 这些行为,`tsconfig.json` 将会自动确认。但是这有一个不同的问题,即是解析模块。模块解析:编译器将尝试去理解 `ns` 在模块语法中表示什么,即 `import * as ns from 'mod'`。为了理解它,编译器需要定义一个模块,它可能是包含你自己代码的 .ts 文件,或者是导入的一个 .d.ts 文件。如果一个文件被找到,则无论它是否在 `excludes` 中,它都将会被编译。 因此,如果你想从编译中排除一个文件,你需要排除所有具有 `import` 或者 `` 指令的文件。 使用 `tsc --listFiles` 来列出在编译时包含了哪些文件,`tsc --traceResolution` 来看看它们为什么会被包含在编译中。 ## 我怎么指定一个 `include`? 现在无法在 `tsconfig.json` 的 `include` 选项外指定所需要包含的文件。你可以通过以下任意一种方式获得相同的结果:1 使用 `files` 列表,2 在目录中添加 `///` 指令。 ## 当我使用 JavaScript 文件时,为什么我会得到 `error TS5055: Cannot write file 'xxx.js' because it would overwrite input file` 错误? 对于 TypeScript 文件来说,在默认情况下,编译器将在同一目录中生成与 JavaScript 相同文件名的文件。因为 TypeScript 文件与编译后的文件总是拥有不同的后缀,这么做是安全的。然而,如果你设置 `allowJs` 编译选项为 `true` 和没有设置任何的编译输出属性(`outFile` 和 `outDir`),编译器将会尝试使用相同的规则来编译文件,这将导致发出的 JavaScript 文件与源文件具有相同的文件名。为了避免意外覆盖源文件,编译器将会发出此警告,并跳过编写输出文件。 有多种方法可以解决此问题,但所有这些方法都涉及配置编译器选项,因此建议你在项目根目录中的 tsconfig.json 文件来启用此功能。如果你不想编译 JavaScript 文件,你只需要将 `allowJs` 选项设置为 `false`;如果你确实想要包含和编译这些 JavaScript 文件,你应该设置 `outDir` 或者 `outFile` 选项,定向到其他位置,这样他们就不会与源文件冲突。如果你仅仅是想包含这些 JavaScript 文件,但是不需要编译,设置 `noEmit` 选项为 `true` 可以跳过编译检查。 ================================================ FILE: docs/faqs/type-guards.md ================================================ # 类型守卫 ## 为什么 `x instanceof Foo` 不能将 `x` 的类型缩小至 `Foo`? 这取决于 `x` 是什么?如果 `x` 的类型不与 `Foo` 兼容,那么缩小 `x` 的类型就毫无意义,所以我们不会这么做。 当你发现 `x` 具有任何类型时,我们对此推荐的做法是: ```ts function doIt(x) { if (x instanceof Object) { // Assume 'x' is a well-known object which // we know how to handle specifically } // Treat 'x' as a primitive } ``` 你将在 TypeScript 中看到这些代码(它们可能早于联合类型被发现),或者是一些从 JavaScript 移植到 TypeScript 的代码,如果我们把 `x` 缩小至 `Object`,那么你将只能做更少的事情。使用任何不在 `Object` 中的属性都将导致错误。这不仅适用于 `Object`,对于具有已定义属性集的任何其他类型都是如此。 ================================================ FILE: docs/faqs/type-system-behavior.md ================================================ # 类型系统的行为 ## 什么是结构化类型? TypeScript 使用**结构化类型**,这个系统并不同于你可能使用过的一些其他流行语言(如:Java、C# 等)的类型系统。 结构化类型系统背后的思想是如果他们的成员类型是兼容的,则他们是兼容的。例如:在 C# 或者 Java 中,有两个名为 `MyPoint` 和 `YourPoint` 的类,它们都具有公共 `int` 类型的属性 `x` 和 `y`,这两个类是不可互换的。但在结构化的类型系统中(TypeScript),这些类型具有不同名称的事实并不重要,因为它们具有相同类型的成员,所以它们是相同的(可以互换的)。 这也适用于子类型关系。例如:在 C++ 中,如果 `Animal` 是 `Dog` 的父类,你只能使用 `Dog` 来替代 `Animal`。在 TypeScript 中,并不是如此,具有至少与 `Animal` 相同数量的成员(适当的类型)的 `Dog`,才是 `Animal` 的子类型,而不管是否是继承关系。 这对于习惯于使用名义类型语言的程序员来说,会产生一些令人惊讶的结果。在这个 FAQs 中的许多问题,都可以追溯到结构化类型及其含义。一旦你掌握了它的基础知识,就很容易理解了。 ## 什么是类型删除? TypeScript 移除了类型断言、接口、类型别名和一些其他编译期间的类型结构。 输入: ```ts var x: SomeInterface; ``` 输出: ```ts var x; ``` 这意味着,在运行时,没有信息表明变量 `x` 的类型是 `SomeInterface`。 对于习惯使用反射或其他元数据系统的程序员来说,缺少的运行时类型信息可能是令人惊讶的。FAQs 中的许多问题都可以归结为「因为类型被删除」。 ## 为什么没有 setter 时的 getter,没有被认为是只读? > 我写下一段代码,并且期望它会抛出错误 ```ts class Foo { get bar() { return 42; } } let x = new Foo(); // Expected error here x.bar = 10; ``` 这在 TypeScript 2.0 + 中会抛出错误。具体请看 [#12](https://github.com/Microsoft/TypeScript/issues/12) ## 为什么函数参数是双向协变? > 我写下一段代码,并且期望它会抛出错误 ```ts function trainDog(d: Dog) { ... } function cloneAnimal(source: Animal, done: (result: Animal) => void): void { ... } let c = new Cat(); // Runtime error here occurs because we end up invoking 'trainDog' with a 'Cat' cloneAnimal(c, trainDog); ``` 这是由于类型系统中缺乏显示协变/逆变注解而导致的不健全。由于它们的缺失,当被问及到 `(x: Dog) => void` 是否能够赋值给 `(x: Animal) => void` 时,TypeScript 必须更加的宽容处理。 为了理解为什么是这样,我们来思考两个问题:`Dog[]` 是 `Animal[]` 的子类型吗?在 TypeScript 中 `Dog[]` 是否应该是 `Animal[]` 的子类型? 第二个问题很容易分析,如果它的答案是 `no` 了? ```ts function checkIfAnimalsAreAwake(arr: Animal[]) { ... } let myPets: Dog[] = [spot, fido]; // Error? Can't substitute Dog[] for Animal[] ? checkIfAnimalsAreAwake(myPets); ``` 这将是非常烦人。在 `checkIfAnimalsAreAwake` 没有修改 arr 的情况下,这段代码 100% 是正确的。没有充足的理由来认为 `Dog[]` 不能被赋值给 `Animal[]` - 在这里很明显,一组 `Dog` 是一组 `Animal`。 回到第一个问题,类型系统什么时候会决定 `Dog[]` 是 `Animal[]` 的子类型。它将会进行以下计算(写到这里,编译器好像没有进行任何优化): - `Dog[]` 可以被赋值给 `Animal[]` 类型吗? - `Dog[]` 的每个成员都可以被赋值给 `Animal[]` 吗? - `Dog[].push` 可以赋值给 `Animal[].push` 吗? - 类型 `(x: Dog) => number` 可以赋值给 `(x: Animal) => number` 吗? - `(x: Dog) => number` 的第一个参数的类型,可以赋值给 `(x: Animal) => number` 的第一个参数吗? - `Dog` 可以赋值给 `Animal` 吗? - 是的 如你所看到的一样,类型系统在检查类型是否可以赋值时,它会提问「`(x: Dog) => number` 的类型能赋值给 `(x: Animal) => number` 吗?」,这与类型系统要求原始类型所需的问题相同。如果 TypeScript 强制函数参数进行逆变(`Animal` 可以赋值给 `Dog`),这可能会导致 `Dog[]` 并不能赋值给 `Animal[]`。 总的来说,在 TypeScript 的类型系统里,一个接收更多特定类型参数的函数是否能够赋值给一个较少特定类型参数的函数的问题,它的答案有一个先决条件 - 有更多特定类型的数组能够赋值到一个较少特定类型的数组吗?在大多数情况下,如果后者不是这情情况,则认为是不被允许的。所以我们必须对函数参数类型的特定情况进行正确的权衡。 ## 为什么有更少参数的函数能够赋值给具有更多参数的函数? > 我写下这段代码,并期望它抛出错误 ```ts function handler(arg: string) { // .... } function doSomething(callback: (arg1: string, arg2: number) => void) { callback('hello', 42); } // Expected error because 'doSomething' wants a callback of // 2 parameters, but 'handler' only accepts 1 doSomething(handler); ``` 这是预期和期望的行为。首先,参考在顶部 FAQ 中的 「substitutability」(译者注:「substitutability」会在接下来的术语专题中解释) - `handler` 是回调函数中的有效参数,因为它可以安全的忽略额外的参数。 其次,让我们来探讨下另外一个用例: ```ts let items = [1, 2, 3]; items.forEach(arg => console.log(arg)); ``` 这也可以看成一个「期望的错误」。在运行时,`forEach` 使用三个参数调用指定的回调函数,但是在大多数情况下,回调函数仅仅使用一个或者两个参数。这是一种非常常见的 JavaScript 模式,必须明确声明所有未使用的参数是很麻烦的。 > 但是 `forEach` 仅仅是标记它的参数作为可选项,例如:`forEach(callback: (element?: T, index?: number, array?: T[]))` 这并不是可选回调函数的含义。始终从函数调用者的角度去读取函数签名,如果 `forEach` 声明回调函数是可选的,这意味着 `forEach` 可能会使用 0 参数来调用回调函数。 一个可选的回调函数参数的含义是: ```ts // Invoke the provided function with 0 or 1 argument function maybeCallWithArg(callback: (x?: number) => void) { if (Math.random() > 0.5) { callback(); } else { callback(42); } } ``` `forEach` 总是为其回调函数提供所有的三个参数。你不必检查 `index` 参数是否为 undefined - 它始终存在,并不是可选的。 目前在 TypeScript 没有存在一种方法可以指示回调函数的参数必须存在。注意,这种强制执行,并不会修复一个错误。换句话说,我们假设每一个回调函数中必须至少有一个参数,你可能会写下以下代码: ```ts [1, 2, 3].forEach(() => console.log('just counting')); // ~~ Error, not enough arguments? ``` 我们可以通过添加一个参数来修复它,但是它可能不是很正确 ```ts [1, 2, 3].forEach(x => console.log('just counting')); // OK, but doesn't do anything different at all ``` ## 为什么一个返回值不是 `void` 的函数,可以赋值给一个返回值为 `void` 的函数? > 我写下这段代码,并期望它抛出错误 ```ts function doSomething(): number { return 42; } function callMeMaybe(callback: () => void) { callback(); } // Expected an error because 'doSomething' returns number, but 'callMeMaybe' // expects void-returning function callMeMaybe(doSomething); ``` 这是预期和期望的行为。首先,参考在顶部的 FAQ 中的 「substitutability」- 相比于 `callMeMaybe`, `doSomething` 返回「更多」的信息,`callMeMaybe` 是一个有效的替代品。 其次,让我们来探讨下另外一个用例: ```ts let items = [1, 2]; callMeMaybe(() => items.push(3)); ``` 这也可以看成是一个「期望的错误」。 `Array#push` 会返回一个数字(数组的新长度),但是它使用在一个返回值为 `void` 的函数上,它是一个安全的替代品。 另外一种思考这个问题的方式是:一个返回值类型为 `void` 的函数,它会说:“无论你的返回值是否存在,我都不会检查它”。 ## 为什么所有的类型,都能赋值给一个空的接口? > 我写下这段代码,并期望它抛出错误 ```ts interface Thing { /* nothing here */ } function doSomething(a: Thing) { // mysterious implementation here } // Expected some or all of these to be errors doSomething(window); doSomething(42); doSomething('huh?'); ``` 没有成员的类型,能够被任何类型替代。在这个例子中,`window`、`42`、`huh` 都能取代 `Thing`。 通常来说,你永远不应该声明没有属性的 `interface`。 ## 我可以用名义上的类型别名吗? > 我写下这段代码,并期望它抛出错误 ```ts type SomeUrl = string; type FirstName = string; let x: SomeUrl = 'http://www.typescriptlang.org/'; let y: FirstName = 'Bob'; x = y; // Expected error ``` 类型别名只是一个简单的别名,它们无法区分自己所表示的类型。 这有一个涉及到使用交叉类型的解决办法: ```ts // Strings here are arbitrary, but must be distinct type SomeUrl = string & { 'this is a url': {} }; type FirstName = string & { 'person name': {} }; // Add type assertions let x = ''; let y = 'bob'; x = y; // Error // OK let xs: string = x; let ys: string = y; xs = ys; ``` 你需要在创建值的任何位置添加类型断言,但是它仍然可以使用 `string` 别名,并且会失去类型的安全性。 ## 如何防止两种类型在结构上兼容? > 我写下这段代码,并期望它抛出错误 ```ts interface ScreenCoordinate { x: number; y: number; } interface PrintCoordinate { x: number; y: number; } function sendToPrinter(pt: PrintCoordinate) { // ... } function getCursorPos(): ScreenCoordinate { // Not a real implementation return { x: 0, y: 0 }; } // This should be an error sendToPrinter(getCursorPos()); ``` 如果你真的希望两种类型不兼容,有一种方式添加一个 「brand」 成员: ```ts interface ScreenCoordinate { _screenCoordBrand: any; x: number; y: number; } interface PrintCoordinate { _printCoordBrand: any; x: number; y: number; } // Error sendToPrinter(getCursorPos()); ```  请注意,这将需要在创建「brand」的地方使用类型断言: ```ts function getCursorPos(): ScreenCoordinate { // Not a real implementation return { x: 0, y: 0 }; } ``` 另外你也可以查看此 [#202](https://github.com/Microsoft/TypeScript/issues/202) 来获取更多有关于此问题的信息; ## 如果对象实现了某个接口,我怎么在运行时检查? > 我写下了像下面的一段代码 ```ts interface SomeInterface { name: string; length: number; } interface SomeOtherInterface { questions: string[]; } function f(x: SomeInterface | SomeOtherInterface) { // Can't use instanceof on interface, help? if (x instanceof SomeInterface) { // ... } } ``` 在编译时期, TypeScript 的类型被删除。这意味着没有用于执行运行时类型检查的内置机制。这完全取决与你如何鉴别对象。一个比较广泛的用法是检查某个对象里的属性。你可以使用用户定义的类型保护来实现它: ```ts function isSomeInterface(x: any): x is SomeInterface { return typeof x.name === 'string' && typeof x.length === 'number'; function f(x: SomeInterface|SomeOtherInterface) { if (isSomeInterface(x)) { console.log(x.name); // Cool! } } ``` ## 为什么错误的转化不会引起运行时的错误? > 我写下一些代码: ```ts let x: any = true; let y = x; // Expected: runtime error (can't convert boolean to string) ``` 或者是这样: ```ts let a: any = 'hmm'; let b = a as HTMLElement; // expected b === null ``` TypeScript 拥有类型断言,但这并不是一个「casts」:` x` 仅仅是用来告诉 TypeScript:「TypeScript,请将 `x` 的类型认为是 `T`」,而不是执行类型安全的运行时转换。因为类型被删除,没有直接等价于 C# 的 `expr as` 或者是 `(type)expr` 的语法。 ## 为什么我没有为 `(number) => string` 或者 `(T) => T` 进行类型检查? > 我写下这段代码,并期望它抛出错误 ```ts let myFunc: (number) => string = n => 'The number in hex is ' + n.toString(16); // Expected error because boolean is not number console.log(myFunc(true)); ``` 在函数的类型中,参数名字是必须的。上面那段代码描述了一个参数名是 `number`,类型是 `any` 的函数。换句话说,这个声明: ```ts let myFunc: (number) => string; ``` 它等价于这个: ```ts let myFunc: (number: any) => string; ``` 你应该写成: ```ts let myFunc: (myArgName: number) => string; ``` 为了避免这些问题,你需要开启 `noImplicitAny` 选项,当检测到有任何参数的类型为 `any` 时,它将会发出一个警告。 ## 为什么我会得到 `Supplied parameters do not match any signature` 的错误? 函数实现签名,它并不是重载的一部分: ```ts function createLog(message: string): number; function createLog(source: string, message?: string): number { return 0; } createLog('message'); // OK createLog('source', 'message'); // ERROR: Supplied parameters do not match any signature ``` 当至少具有一个函数重载的签名时,只有重载是可见的。最后一个声明签名(也可以被称为签名的实现)对签名的形状并没有贡献,因此,要获得所需的行为,你需要添加额外的重载: ```ts function createLog(message: string): number; function createLog(source: string, message: string): number; function createLog(source: string, message?: string): number { return 0; } ``` 由于 JavaScript 没有函数重载,我们不得不这么做,因此你需要在你的函数中进行参数检查。 例如,你可以要求你的使用者使用匹配的参数对来调用,并正确实现它,而不允许混合参数类型。 ```ts function compare(a: string, b: string): void; function compare(a: number, b: number): void; function compare(a: string | number, b: string | number): void { // Just an implementation and not visible to callers } compare(1, 2); // OK compare('s', 'l'); // OK compare(1, 'l'); // Error. ``` ================================================ FILE: docs/jsx/nonReactJSX.md ================================================ # 非 React JSX TypeScript 让你能够以类型安全的方式,在 React 中使用 JSX 之外的其他东西。下面列出了一些可自定义的点,但请注意,这只适用于高级 UI 框架的作者。 - 你可以使用 `"jsx":"preserve"` 选项来禁用 React 的样式触发。这意味着,JSX 将按原样被触发,然后你可以使用自定义转化器来转化 JSX 部分。 - 使用 `JSX` 全局模块: - 你可以通过定制 `JSX.IntrinsicElements` 的接口成员来控制哪些 HTML 标签是可用的,以及如何对其进行类型检查; - 当你在组件中使用时: - 你可以通过自定义默认的 `interface ElementClass extends React.Component { }` 声明文件来控制哪个 `class` 必须由组件继承; - 你可以通过自定义 `declare module JSX { interface ElementAttributesProperty { props: {} } }` 声明文件来控制使用的哪个属性(property)来检查特性(attribute)(默认是 `props`)。 ## jsxFactory 通过 `--jsxFactory ` 与 `--jsx react`,能让你不同于默认 `React` 的方式使用 JSX 工厂函数。 这个新的工厂函数名字习惯被称之为 `createElement` 函数。 ### 例子 ```jsx import { jsxFactory } from 'jsxFactory'; const div =
Hello JSX!
; ``` 使用编译: ```ts tsc --jsx react --reactNamespace jsxFactory --m commonJS ``` 编译结果: ```js 'use strict'; var jsxFactory_1 = require('jsxFactory'); var div = jsxFactory_1.jsxFactory.createElement('div', null, 'Hello JSX!'); ``` ### jsx 编译提示 你甚至可以使用`jsxPragma` 为每个文件指定不同的 `jsxFactory`: ```tsx /** @jsx jsxFactory */ import { jsxFactory } from 'jsxFactory'; var div =
Hello JSX!
; ``` 在 jsx 编译提示中,配合 `--jsx react` 命令,这个文件将会被触发使用工厂函数: ```js 'use strict'; var jsxFactory_1 = require('jsxFactory'); var div = jsxFactory_1.jsxFactory.createElement('div', null, 'Hello JSX!'); ``` ================================================ FILE: docs/jsx/reactJSX.md ================================================ # React JSX > [在 React 中使用 TypeScript 的教学视频](https://egghead.io/courses/use-typescript-to-develop-react-applications) ## 建立 在 [TypeScript in the browser](https://basarat.gitbooks.io/typescript/content/docs/quick/browser.html) 章节中,我们已经学会开始开发 React 的应用了,以下是一些重点: - 使用文件后缀 `.tsx`(替代 `.ts`); - 在你的 `tsconfig.json` 配置文件的 `compilerOptions` 里设置选项 `"jsx": "react"`; - 在你的项目里为 `JSX` 和 `React` 安装声明文件:`npm i -D @types/react @types/react-dom`; - 导入 `react` 到你的 `.tsx` 文件(`import * as React from 'react'`)。 ## HTML 标签 vs 组件 React 不但能渲染 HTML 标签(strings)也能渲染 React 组件(classes)。JavaScript 触发这些的原理是不同的(`React.createElement('div')` vs `React.createElement(MyComponent)`), 确定使用哪一种方式取决于首字母的大小写,`foo` 被认为是 HTML 标签,`Foo` 被认为是一个组件。 ## 类型检查 ### HTML 标签 一个 HTML 标签 `foo` 被标记为 `JSX.IntrinsicElements.foo` 类型。在我们已经安装的文件 `react-jsx.d.ts` 中定义了所有主要标签的类型,如下是一部分示例: ```ts declare namespace JSX { interface IntrinsicElements { a: React.HTMLAttributes; abbr: React.HTMLAttributes; div: React.HTMLAttributes; span: React.HTMLAttributes; // 其他 } } ``` ### 函数式组件 你可以使用 `React.FunctionComponent` 接口定义函数组件: ```tsx type Props = { foo: string; }; const MyComponent: React.FunctionComponent = props => { return {props.foo}; }; ; ``` ### 类组件 根据组件的 `props` 属性对组件进行类型检查。这是以 JSX 如何转换作为蓝本,例如:属性成为 `props` 的组成部分。 `react.d.ts` 文件定义了 `React.Component`,你应该使用自己所需的 `Props` 和 `State` 声明扩展它: ```tsx type Props = { foo: string; }; class MyComponent extends React.Component { render() { return {this.props.foo}; } } ; ``` ### React JSX Tip: 接收组件的实例 react 类型声明文件提供了 `React.ReactElement`,它可以让你通过传入 ``,来注解类组件的实例化结果。 ```ts class MyAwesomeComponent extends React.Component { render() { return
Hello
; } } const foo: React.ReactElement = ; // Okay const bar: React.ReactElement = ; // Error! ``` ::: tip 当然,你可以将它用作函数参数的注解,甚至可以是 React 组件的 prop 成员。 ::: ### React JSX Tip: 接受一个可以在 Props 起作用,并使用 JSX 渲染的组件 类型 `React.Component` 是 `React.ComponentClass

` 与 `React.StatelessComponent

` 的组合,所以你可以接受一些可以用作 Props 类型和使用 JSX 渲染的组件。 ```ts const X: React.Component = foo; // from somewhere // Render X with some props: ; ``` ### React JSX Tip: 可渲染的接口 React 可以渲染一些像 `JSX` 或者是 `string` 的内容,这些被合并到类型 `React.ReactNode` 中,因此,当你接收可渲染的内容时,你可以使用它: ```tsx type Props = { header: React.ReactNode; body: React.ReactNode; }; class MyComponent extends React.Component { render() { return (

{this.props.header} {this.props.body}
); } } Header} body={body} /> ``` ### React JSX tip: 接收组件的接口 React 声明文件提供 `React.ReactElement` 的接口,可以让你注解一个类组件实例化的返回值``,如: ```tsx class MyAwesomeComponent extends React.Component { render() { return
Hello
; } } const foo: React.ReactElement = ; // Okay const bar: React.ReactElement = ; // Error! ``` ::: tip 你也可以将其用做函数参数的注解,或者是 React 组件的 prop 注解。 ::: ### React JSX tip: 接收可以作为 props 的组件,并且使用 JSX 渲染它 类型 `React.Component` 合并了 `React.ComponentClass

` 和 `React.StatelessComponent

`,因此,你可以接收一些使用 `Prop` 类型的组件,并使用 JSX 渲染它: ```tsx const X: React.Component = foo // 来自其他地方 // 渲染 X ``` ### React JSX tip: 泛型组件 它完全能按我们预期工作,如: ```tsx // 一个泛型组件 type SelectProps = { items: T[] }; class Select extends React.Component, any> {} // 使用 const Form = () => items={['a', 'b']} />; ``` ### 泛型函数 一些像下面这样的正常工作: ```ts function foo(x: T): T { return x; } ``` 然而不能使用箭头泛型函数: ```ts const foo = (x: T) => T; // Error: T 标签没有关闭 ``` **解决办法**:在泛型参数里使用 `extends` 来提示编译器,这是个泛型: ```ts const foo = (x: T) => x; ``` ### React Tip: 强类型的 Refs 基本上你在初始化一个变量时,使用 ref 和 null 的联合类型,并且在回调函数中初始化他: ```ts class Example extends React.Component { example() { // ... something } render() { return

Foo
; } } class Use { exampleRef: Example | null = null; render() { return (this.exampleRef = exampleRef)} />; } } ``` 使用原生元素时也一样: ```ts class FocusingInput extends React.Component<{ value: string; onChange: (value: string) => any }, {}> { input: HTMLInputElement | null = null; render() { return ( (this.input = input)} value={this.props.value} onChange={e => { this.props.onChange(e.target.value); }} /> ); } focus() { if (this.input != null) { this.input.focus(); } } } ``` ### 类型断言 如我们之前[提到](../typings/typeAssertion.md#as-foo-与-foo)的,可以使用 `as Foo` 语法进行类型断言。 ## 默认 Props - 在有状态组件中使用默认的 Props:你可以通过 `null` 操作符(这不是一个理想的方式,但是这是我能想到的最简单的最小代码解决方案)告诉 TypeScript 一个属性将会被外部提供(React)。 ```tsx class Hello extends React.Component<{ /** * @default 'TypeScript' */ compiler?: string; framework: string; }> { static defaultProps = { compiler: 'TypeScript' }; render() { const compiler = this.props.compiler!; return (
{compiler}
{this.props.framework}
); } } ReactDOM.render( , // TypeScript React document.getElementById('root') ); ``` - 在 SFC 中使用默认的 Props:推荐使用简单的 JavaScript 参数,因为同样适用于 TypeScript 类型系统: ```tsx const Hello: React.SFC<{ /** * @default 'TypeScript' */ compiler?: string; framework: string; }> = ({ compiler = 'TypeScript', // Default prop framework }) => { return (
{compiler}
{framework}
); }; ReactDOM.render( , // TypeScript React document.getElementById('root') ); ``` ================================================ FILE: docs/jsx/support.md ================================================ # 支持 JSX TypeScript 支持 JSX 转换和代码分析,如果你还不了解 JSX,[官网](https://facebook.github.io/jsx/)上有关于它的摘要: > JSX is an XML-like syntax extension to ECMAScript without any defined semantics. It's NOT intended to be implemented by engines or browsers. It's NOT a proposal to incorporate JSX into the ECMAScript spec itself. It's intended to be used by various preprocessors (transpilers) to transform these tokens into standard ECMAScript. JSX 背后的动机是允许用户在 JavaScript 中书写类似于 HTML 的视图,因此你可以: - 使用相同代码,既能检查你的 JavaScript,同时能检查你的 HTML 视图层部分。 - 让视图层了解运行时的上下文(加强传统 MVC 中的控制器与视图连接)。 - 复用 JavaScript 设计模式维护 HTML 部分,例如:用 `Array.prototype.map.`、`?:`、`switch` 等,代替创建新的可替代品。 这能够减少错误的可能性,并且能增加用户界面的可维护性。目前 JSX 的主要消费者来自 [facebook 推出的 ReactJS](http://facebook.github.io/react/),接下来我们结合它来讨论 JSX 用法。 ================================================ FILE: docs/new/typescript-3.7.md ================================================ # TypeScript 3.7 ## 可选链(Optional Chaining) 在我们的 issue 追踪器中,可选链在 [issue #16](https://github.com/microsoft/TypeScript/issues/16) 中,自那以后,有超过 23000 条 issues 被记录在 issue 中。 可选链的核心是允许我们写下如果碰到 `null` 或者 `undefined`,TypeScript 能立即停止运行的代码。可选链耀眼的部分是使用 `?.` 运算符来访问一个可选属性的运算符。 下面代码: ```ts let x = foo?.bar.baz(); ``` 告诉我们,当 `foo` 被定义了,`foo.bar.baz()` 将会执行完成;但是当 `foo` 是 `null` 或者 `undefined` 时,TypeScript 会立即停止运行,并且仅仅是返回 `undefined`。 也就是说,上文的代码等效于如下代码: ```ts let x = foo === null || foo === undefined ? undefined : foo.bar.baz(); ``` 注意,如果 `bar` 是 `null` 或者 `undefined`,在访问 `bar` 时,我们的代码仍然会抛出一个错误。 与此相似,如果 `baz` 是 `null` 或者 `undefined`,在调用时,它也会抛出一个错误。`?.` 只会检查它左边的值是 `undefined` 还是 `null` - 并不会检查后面的任何属性。 你可能已经发现你可以使用 `?.` 来替代很多使用 `&&` 执行空检查的代码: ```ts // Before if (foo && foo.bar && foo.bar.baz) { // ... } // After-ish if (foo?.bar?.baz) { // ... } ``` 注意:`?.` 与 `&&` 运算符行为略有不同,因为 `&&` 专用于 "falsy" 的值(如:空字符串、`0`、`NaN`、和 `false`),但是 `?.` 是一个仅作用于结构上的操作符,`?.` 在验证有效数据如 `0` 或者空字符串时,它并没有使用短路验证的方式。 可选链还包含另外两个运算符,首先是可选元素的访问,它的行为类似于可选属性的访问,但是它允许我们访问非标志符属性(例如:任意的字符串、数字和 symbols): ```ts /** * Get the first element of the array if we have an array. * Otherwise return undefined. */ function tryGetFirstElement(arr?: T[]) { return arr?.[0]; // equivalent to // return (arr === null || arr === undefined) ? // undefined : // arr[0]; } ``` 另外一个是可选调用,它能让我们有条件的调用表达式: ```ts async function makeRequest(url: string, log?: (msg: string) => void) { log?.(`Request started at ${new Date().toISOString()}`); // roughly equivalent to // if (log != null) { // log(`Request started at ${new Date().toISOString()}`); // } const result = (await fetch(url)).json(); log?.(`Request finished at at ${new Date().toISOString()}`); return result; } ``` 可选链的「短路运算」行为被局限在属性的访问、调用以及元素的访问 --- 它不会沿伸到后续的表达式中,也就是说: ```ts let result = foo?.bar / someComputation() ``` 可选链不会阻止除法运算或者 `someComputation()` 调用,它等价于: ```ts let temp = foo === null || foo === undefined ? undefined : foo.bar; let result = temp / someComputation(); ``` 它可能会导致除法运算的结果是 `undefined`,这就是为什么在 `strictNullChecks` 选项下,会抛出一个错误: ```ts function barPercentage(foo?: { bar: number }) { return foo?.bar / 100; // ~~~~~~~~ // Error: Object is possibly undefined. } ``` 更多的信息,你可以阅读 [proposal](https://github.com/tc39/proposal-optional-chaining/) 以及该原始的 [PR](https://github.com/microsoft/TypeScript/pull/33294) ## Nullish Coalescing nullish coalescing 运算符是另一个即将推出的 ECMAScript 功能,它与 Optional chaining 一同被推出,并且我们团队一直参与了 TC39 的有关讨论。 你可以这么想它的功能 - `??` 运算符 - 当处理 `null` 或者 `undefined` 时,它可以作为一种「倒退」到默认值的方式,当我们写下如下代码: ```ts let x = foo ?? bar(); ``` 这种方式来表示当 `foo` 值存在时,`foo` 会被使用,但是当它是 `null` 或 `undefined`,它会计算 `bar()`。 它等价于如下代码: ```ts let x = foo !== null && foo !== undefined ? foo : bar(); ``` 当尝试使用一个默认值时,`??` 运算符可以被 `||` 替代。例如,在下面的代码片段中,当尝试获取最后一次储存在 localStorage 中的 `volume` 时(如果它存在);但是当使用 `||`,这会有个 bug: ```ts function initializeAudio() { let volume = localStorage.volume || 0.5; // ... } ``` 当 `localStore.volume` 的值是 `0` 时,在这段代码里,将会把 `volume` 的值设置为 `0.5`。`??` 运算符能避免一些从 `0`、`NaN` 以及`''` 这些被认为是 `falsy` 值的意外行为。 ## 断言函数 有一类特定的函数,在非预期结果出现时会抛出一个错误。这类函数就叫做断言函数。例如,Node.js 有一个专用的断言函数叫 `assert`。 ```js assert(someValue === 42); ``` 在这个示例中,如果 `someValue` 不等于 `42`,那么 `assert` 就会抛出一个 `AssertionError`。 JavaScript 中的断言经常用于确保传入的是正确的类型。 比如, ```js function multiply(x, y) { assert(typeof x === 'number'); assert(typeof y === 'number'); return x * y; } ``` 不幸的是,在 TypeScript 中,这些检查可能从来不会被正确的编写。对于松散类型代码,意味着 TypeScript 检查较少,而对于稍微规范一些的写法,一般要求使用者添加类型断言。 ```ts function yell(str) { assert(typeof str === 'string'); return str.toUppercase(); // Oops! We misspelled 'toUpperCase'. // Would be great if TypeScript still caught this! } ``` 这里有可供选择的替代写法,可以让 TypeScript 分析出问题,不过并不方便。 ```ts function yell(str) { if (typeof str !== 'string') { throw new TypeError('str should have been a string.'); } // Error caught! return str.toUppercase(); } ``` TypeScript 的目标是以最小的改动为现存的 JavaScript 结构添加类型声明。因此,TypeScript 3.7 引入了一个「断言签名(assertion signatures)」的新概念,用来模拟这些断言函数。 第一种断言签名,模拟 Node 中的 `assert` 函数的功能。它确保在断言的范围内,断言条件必须为这个真。 ```ts function assert(condition: any, msg?: string): asserts condition { if (!condition) { throw new AssertionError(msg) } } ``` `asserts condition` 的意思是,如果 `assert` 函数有返回,传入 `condition` 的参数必须为真,因为如果不是这样,它肯定会抛出一个错误。这意味着,在剩下的作用域中(if 条件后)`condition` 必须为 `truthy`。 举一个例子,用这个断言函数意味着我们可以实现捕获之前的 `yell` 示例的错误。 ```ts function yell(str) { assert(typeof str === "string"); return str.toUppercase(); // ~~~~~~~~~~~ // error: Property 'toUppercase' does not exist on type 'string'. // Did you mean 'toUpperCase'? } function assert(condition: any, msg?: string): asserts condition { if (!condition) { throw new AssertionError(msg) } } ``` 另外一种断言签名不是用来校验一个条件,而是告诉 TypeScript 某个变量或属性有不同的类型。 ```ts function assertIsString(val: any): asserts val is string { if (typeof val !== "string") { throw new AssertionError("Not a string!"); } } ``` 这里 `asserts val is string` 确保在 `assertIsString` 在被调用之后, 任何传入的变量将被认为是一个 `string`. ```ts function yell(str: any) { assertIsString(str); // Now TypeScript knows that 'str' is a 'string'. return str.toUppercase(); // ~~~~~~~~~~~ // error: Property 'toUppercase' does not exist on type 'string'. // Did you mean 'toUpperCase'? } ``` 这里的断言签名非常类似于类型谓词(predicate)签名: ```ts function isString(val: any): val is string { return typeof val === 'string'; } function yell(str: any) { if (isString(str)) { return str.toUppercase(); } throw 'Oops!'; } ``` 就像类型谓词签名一样,这些断言签名非常强大的。我们可以用它们实现一些非常复杂的想法和设计。 ```ts function assertIsDefined(val: T): asserts val is NonNullable { if (val === undefined || val === null) { throw new AssertionError( `Expected 'val' to be defined, but received ${val}` ); } } ``` 想阅读更多断言签名相关内容, [签出原始的 pull request](https://github.com/microsoft/TypeScript/pull/32695). ## 更好的支持返回函数的 `never` 为了能使断言签名工作,其中的一个工作是,TypeScript 需要对调用时的函数,以及位置信息编码更多的信息。这给了我们扩展另一类函数的机会:返回 `never` 的函数。 返回为 `never` 的函数,即是永远没有返回的函数。它表明抛出了异常、由于错误发生暂停、或者程序退出的情况。例如:[`process.exit(...)` 中的 `@types/node`](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/5299d372a220584e75a031c13b3d555607af13f8/types/node/globals.d.ts#l874) 指定返回为 `never`。 为了确保函数永远不会返回潜在的 `undefined`,或者从所有的代码路径中有效返回,TypeScript 需要一些句法(syntactic)信号 - 可以是函数末尾的 `return` 或者 `thorw`。因此使用者就会发现自己正在返回它们的故障函数: ```ts function dispatch(x: string | number): SomeType { if (typeof x === 'string') { return doThingWithString(x); } else if (typeof x === 'number') { return doThingWithNumber(x); } return process.exit(1); } ``` 现在,当函数被调用时,TypeScript 能识别出它们会影响的流程并说明原因。 ```ts function dispatch(x: string | number): SomeType { if (typeof x === 'string') { return doThingWithString(x); } else if (typeof x === 'number') { return doThingWithNumber(x); } process.exit(1); } ``` 与断言函数一样,具体详情,你可以查看此 [PR](https://github.com/microsoft/TypeScript/pull/32695) ## 递归的类型别名 类型别名在如何“递归”引用方面一直存在局限性,原因在于对类型别名的任何使用都必须能够用这个来代替自己。在某些情况下,这是不可能的,因此编译器,会拒绝某些别名的递归: ```ts type Foo = Foo; ``` 这是一个合理的限制,因为 `Foo` 的任何使用都可以被 `Foo` 替代,`Foo` 的任何使用都可以被 `Foo` 替代(无限循环),到最后,没有类型可以替代 `Foo` 这与其他语言处理类型别名时是一致的。但是在用户如何利用该功能方面,它确实造成了一些令人疑惑的情景,例如,在 TypeScript 3.6 及其以下版本时,下面的代码会抛出错误: ```ts type ValueOrArray = T | Array>; // ~~~~~~~~~~~~ // error: Type alias 'ValueOrArray' circularly references itself. ``` 这很令人疑惑,因为从技术上讲,这没有错误。使用者也可以通过引入接口的方式来实现上述中相同作用的代码: ```ts type ValueOrArray = T | ArrayOfValueOrArray; interface ArrayOfValueOrArray extends Array> {} ``` 因为接口(和其他对象类型)引入了一个中间类型,并且不需要立马构建它们的完整结构,因此 TypeScript 在使用这种结构时没有问题。但是对于使用者来说,引入一个中间类型来说,并不是很直观。原则上,直接使用 `Array` 的 `ValueOrArray` 的原始版本确实没有任何问题。假如编译器有一点“偷懒”,并且只在必要时才计算 `Array` 的类型参数,则 TypeScript 可以正确表示出这些。 这正是 TypeScript 3.7 所引入的内容,在最顶级的类型别名中,TypeScript 将会推迟解析类型参数,已允许这种情况的发生。 这意味着,以下代码能成立: ```ts type Json = string | number | boolean | null | JsonObject | JsonArray; interface JsonObject { [property: string]: Json; } interface JsonArray extends Array {} ``` 在没有中间 interface 时,能重写为以下形式: ```ts type Json = string | number | boolean | null | { [property: string]: Json } | Json[]; ``` 这种新的行为,可以让我们在元组中递归使用类型别名,下面的代码,在以前会抛出错误,但是现在是有效的: ```ts type VirtualNode = string | [string, { [key: string]: any }, ...VirtualNode[]]; const myNode: VirtualNode = [ 'div', { id: 'parent' }, ['div', { id: 'first-child' }, "I'm the first child"], ['div', { id: 'second-child' }, "I'm the second child"] ]; ``` 更多信息,你可以查看 [PR](https://github.com/microsoft/TypeScript/pull/33050)。 ## `--declaration` and `--allowJs` `--declaration` 选项可以让我们从 TypeScript 源文件(如 `.ts`、`.tsx` 文件)中生成 `.d.ts`(声明文件)。这些 `.d.ts` 文件为什么很重要,有以下原因: 首先,它们允许 TypeScript 在不需要重新检查源代码的情况下,能对其他项目进行类型检查。其次,它们允许 TypeScript 与没有使用 TypeScript 构建的 JavaScript 库之间更好的协作。最后,一个被经常忽略小细节:当使用由 TypeScript 驱动的编辑器来获得一些更好功能(比如自动完成)时,TypeScript 和 JavaScript 用户都可以从这些文件中受益。 不幸的是,`--declaration` 与 `--allowJs` 并不能一起使用,`--declaration` 选项会混合 TypeScript 和 JavaScript 输入文件。这是一个令人沮丧的限制,因为它意味着用户在迁移代码库时,不能使用 `--declaration` 选项,即使是使用了 JSDoc 形式的注释。TypeScript 3.7 改变了此行为,它能让两个选项同时使用。 此功能带来最有影响力的结果,可能并不容易被发现:在 TypeScript 3.7 中,用户在 JavaScript 库中写的 JSDoc 注释,能帮助 TypeScript 用户。 它之所以能实现,是因为:当使用 `allowJs` 时,TypeScript 会尽可能的分析代码来了解常见的 JavaScript 模式。然而,一些在 JavaScript 能表示的模式,并不一定能在 TypeScript 等效表示出来。当 `declaration` 选项打开时,TypeScript 会找出将 JSDoc 注释和 CommonJS 输出到 `.d.ts` 文件中的类型声明的最佳方式。 请看如下例子: ```js const assert = require('assert'); module.exports.blurImage = blurImage; /** * Produces a blurred image from an input buffer. * * @param input {Uint8Array} * @param width {number} * @param height {number} */ function blurImage(input, width, height) { const numPixels = width * height * 4; assert(input.length === numPixels); const result = new Uint8Array(numPixels); // TODO return result; } ``` 将会产生如下的 `.d.ts` ```ts /** * Produces a blurred image from an input buffer. * * @param input {Uint8Array} * @param width {number} * @param height {number} */ export function blurImage(input: Uint8Array, width: number, height: number): Uint8Array; ``` 使用 `@param` 注解也有益处: ```js /** * @callback Job * @returns {void} */ /** Queues work */ export class Worker { constructor(maxDepth = 10) { this.started = false; this.depthLimit = maxDepth; /** * NOTE: queued jobs may add more items to queue * @type {Job[]} */ this.queue = []; } /** * Adds a work item to the queue * @param {Job} work */ push(work) { if (this.queue.length + 1 > this.depthLimit) throw new Error('Queue full!'); this.queue.push(work); } /** * Starts the queue if it has not yet started */ start() { if (this.started) return false; this.started = true; while (this.queue.length) { /** @type {Job} */ (this.queue.shift())(); } return true; } } ``` 将会生成如下 `.d.ts` ```ts /** * @callback Job * @returns {void} */ /** Queues work */ export class Worker { constructor(maxDepth?: number); started: boolean; depthLimit: number; /** * NOTE: queued jobs may add more items to queue * @type {Job[]} */ queue: Job[]; /** * Adds a work item to the queue * @param {Job} work */ push(work: Job): void; /** * Starts the queue if it has not yet started */ start(): boolean; } export type Job = () => void; ``` 注意,如果你想一起使用这些选项,TypeScript 不需要含有对应的 `.js` 文件,如果只是需要 TypeScript 生成简单的 `.d.ts` 文件,使用 `--emitDeclarationOnly` 选项即可。 更多详情,请参考 [PR](https://github.com/microsoft/TypeScript/pull/32372) For more details, you can [check out the original pull request](https://github.com/microsoft/TypeScript/pull/32372). ## `useDefineForClassFields` 标记与 `declare` 属性修饰符 当在 TypeScript 中实现 class 中的公有字段时,我们尽可能的实现了以下代码功能: ```ts class C { foo = 100; bar: string; } ``` 它等同于构造函数内的相似语句: ```ts class C { constructor() { this.foo = 100; } } ``` 不幸的是,虽然这是 TC39 早期提案的发展方向,但是极有可能对于公共 class 字段有不同的标准化方向。因此,原始的代码可能会被编译成如下代码: ```ts class C { constructor() { Object.defineProperty(this, 'foo', { enumerable: true, configurable: true, writable: true, value: 100 }); Object.defineProperty(this, 'bar', { enumerable: true, configurable: true, writable: true, value: void 0 }); } } ``` 当然 TypeScript 3.7 版本在默认情况下编译出的代码不会有任何改变(与以前版本相同),我们一直在逐步更改,来帮助用户减少可能发生的破坏性更改。我们提供来一个新的标志 `useDefineForClassFields` 来使用此种模式,并带有一些新的逻辑检查。 最大的两个改变如下: - 使用 `Object.defineProperty` 来初始化声明。 - 初始化的值都是 undefined,即使是它们没有被初始化。 对于使用继承的代码来说,这可能会造成很多问题。首先,来自于基类的 set 访问器,不再会被触发 -- 它可能会被完全重写。 ```ts class Base { set data(value: string) { console.log('data changed to ' + value); } } class Derived extends Base { // No longer triggers a 'console.log' // when using 'useDefineForClassFields'. data = 10; } ``` 其次,在基类中使用一个类字段来专门指定一个属性,它也不会正常工作。 ```ts interface Animal { animalStuff: any; } interface Dog extends Animal { dogStuff: any; } class AnimalHouse { resident: Animal; constructor(animal: Animal) { this.resident = animal; } } class DogHouse extends AnimalHouse { // Initializes 'resident' to 'undefined' // after the call to 'super()' when // using 'useDefineForClassFields'! resident: Dog; constructor(dog: Dog) { super(dog); } } ``` 这两个问题,可以归结为在没有初始化的情况下,混合属性与访问器时,将会有重复声明出现。 为了检查访问器是否有上述问题,TypeScript 3.7 将会在 .d.ts 中编译出 `get`/`set`,于是 TypeScript 能够检查出是否有重写访问器的问题。 ```ts class Base { set data(value: string) { console.log('data changed to ' + value); } } class Derived extends Base { constructor() { data = 10; } } ``` 为了解决第二个问题,你可以添加一个显示的初始化值,或者添加一个 `declare` 修饰符来表明该属性不会被编译出任何值。 ```ts interface Animal { animalStuff: any; } interface Dog extends Animal { dogStuff: any; } class AnimalHouse { resident: Animal; constructor(animal: Animal) { this.resident = animal; } } class DogHouse extends AnimalHouse { declare resident: Dog; // ^^^^^^^^ // 'resident' now has a 'declare' modifier, // and won't produce any output code. constructor(dog: Dog) { super(dog); } } ``` 现在,只有当编译目标是 ES5 及其以上时,`useDefineForClassFields` 编译选项才可用,因为 ES3 并没有 `Object.defineProperty`。要实现类似的问题检查,你可以创建一个编译目标为 ES5 并且使用 `--noEmit` 来避免完全构建的单独项目。 更多的信息,你可以查看此 [PR](https://github.com/microsoft/TypeScript/pull/33509)。 ## 编辑有项目引用的项目,无需构建 TypeScript 的项目引用功能给我们提供了一个方便的方式来拆分代码库,从而能让我们能实现更快地编译。 遗憾的是,当我们编辑一个依赖未被构建(或者构建结果已过期)的项目时,会得到不好的编辑体验。 在 TypeScript 3.7 中,当打开一个有依赖的项目时,TypeScript 将会自动地使用原始 `.ts`/`.tsx` 文件来代替。 这意味着在有依赖引用的项目中,代码的修改会马上同步和生效,从而编辑体验会有所提升, 你可以打开编译器选项 `disableSourceOfProjectReferenceRedirect` 来禁用这个引用的功能,因为在超大型项目中这个功能可能会影响性能。 你可以 [阅读它的 pull request,获取更多相关信息](https://github.com/microsoft/TypeScript/pull/32028). ## 没有调用的函数检查 一个常见且危险的错误是忘记调用一个函数,特别是当函数具有零参数或用暗示它可能是属性而不是函数的方式名称时。 ```ts interface User { isAdministrator(): boolean; notify(): void; doNotDisturb?(): boolean; } // later... // Broken code, do not use! function doAdminThing(user: User) { // oops! if (user.isAdministrator) { sudo(); editTheConfiguration(); } else { throw new AccessDeniedError('User is not an admin'); } } ``` 在这里,我们忘记调用 `isAdministrator`,该代码错误的允许非管理员用户编辑配置。 在 TypeScript 3.7 中,它就会抛出错误: ```ts function doAdminThing(user: User) { if (user.isAdministrator) { // ~~~~~~~~~~~~~~~~~~~~ // error! This condition will always return true since the function is always defined. // Did you mean to call it instead? ``` 这个检查是一个破坏性的更改,因此这个错误仅仅是发生在 `if` 条件语句中,并且如果 `strictNullChecks` 关闭或者在 if 块中,函数有被调用,不会发出此错误。 ```ts interface User { isAdministrator(): boolean; notify(): void; doNotDisturb?(): boolean; } function issueNotification(user: User) { if (user.doNotDisturb) { // OK, property is optional } if (user.notify) { // OK, called the function user.notify(); } } ``` 如果你想在没有调用函数的前提下,对它进行测试。你可以修改它的声明,让它可能是 `undefined/null`,或者使用 `!!` 来写下一些 `if(!!user.isAdministrator)` 来表明这种行为是故意的。 ================================================ FILE: docs/new/typescript-3.8.md ================================================ # TypeScript 3.8 [TypeScript 3.8](http://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html) 将会带来了许多特性,其中包含一些新的或即将到来的 ECMAScript 特性、仅仅导入/导出声明语法等。 ## 仅仅导入/导出声明 为了能让我们导入类型,TypeScript 重用了 JavaScript 导入语法。例如在下面的这个例子中,我们确保 JavaScript 的值 `doThing` 以及 TypeScript 类型 `Options` 一同被导入 ```ts // ./foo.ts interface Options { // ... } export function doThing(options: Options) { // ... } // ./bar.ts import { doThing, Options } from './foo.js'; function doThingBetter(options: Options) { // do something twice as good doThing(options); doThing(options); } ``` 这很方便的,因为在大多数的情况下,我们不必担心导入了什么 —— 仅仅是我们想导入的内容。 遗憾的是,这仅是因为一个被称之为「导入省略」的功能而起作用。当 TypeScript 输出一个 JavaScript 文件时,TypeScript 会识别出 `Options` 仅仅是当作了一个类型来使用,它将会删除 `Options` ```ts // ./foo.js export function doThing(options: Options) { // ... } // ./bar.js import { doThing } from './foo.js'; function doThingBetter(options: Options) { // do something twice as good doThing(options); doThing(options); } ``` 在通常情况下,这种行为都是比较好的。但是它会导致一些其他问题。 首先,在一些场景下,TypeScript 会混淆导出的究竟是一个类型还是一个值。比如在下面的例子中, `MyThing` 究竟是一个值还是一个类型? ```ts import { MyThing } from './some-module.js'; export { MyThing }; ``` 如果单从这个文件来看,我们无从得知答案。如果 `Mything` 仅仅是一个类型,Babel 和 TypeScript 使用的 `transpileModule` API 编译出的代码将无法正确工作,并且 TypeScript 的 `isolatedModules` 编译选项将会提示我们,这种写法将会抛出错误。问题的关键在于,没有一种方式能识别它仅仅是个类型,以及是否应该删除它,因此「导入省略」并不够好。 同时,这也存在另外一个问题,TypeScript 导入省略将会去除只包含用于类型声明的导入语句。对于含有副作用的模块,这造成了明显的不同行为。于是,使用者将会不得不添加一条额外的声明语句,来确保有副作用。 ```ts // This statement will get erased because of import elision. import { SomeTypeFoo, SomeOtherTypeBar } from './module-with-side-effects'; // This statement always sticks around. import './module-with-side-effects'; ``` 一个我们看到的具体例子是出现在 Angularjs(1.x)中, `services` 需要在全局在注册(它是一个副作用),但是导入的 `services` 仅仅用于类型声明中。 ```ts // ./service.ts export class Service { // ... } register('globalServiceId', Service); // ./consumer.ts import { Service } from './service.js'; inject('globalServiceId', function(service: Service) { // do stuff with Service }); ``` 结果 `./service.js` 中的代码不会被执行,导致在运行时会被中断。 为了避免这类行为,我们意识到在什么该被导入/删除方面,需要给使用者提供更细粒度的控制。 在 TypeScript 3.8 版本中,我们添加了一个仅仅导入/导出声明语法来作为解决方式。 ```ts import type { SomeThing } from "./some-module.js"; export type { SomeThing }; ``` `import type` 仅仅导入被用于类型注解或声明的声明语句,它总是会被完全删除,因此在运行时将不会留下任何代码。与此相似,`export type` 仅仅提供一个用于类型的导出,在 TypeScript 输出文件中,它也将会被删除。 值得注意的是,类在运行时具有值,在设计时具有类型。它的使用与上下文有关。当使用 `import type` 导入一个类时,你不能做类似于从它继承的操作。 ```ts import type { Component } from "react"; interface ButtonProps { // ... } class Button extends Component { // ~~~~~~~~~ // error! 'Component' only refers to a type, but is being used as a value here. // ... } ``` 如果在之前你使用过 Flow,它们的语法是相似的。一个不同的地方是我们添加了一个新的限制条件,来避免可能混淆的代码。 ```ts // Is only 'Foo' a type? Or every declaration in the import? // We just give an error because it's not clear. import type Foo, { Bar, Baz } from "some-module"; // ~~~~~~~~~~~~~~~~~~~~~~ // error! A type-only import can specify a default import or named bindings, but not both. ``` 与 `import type` 相关联,我们提供来一个新的编译选项:`importsNotUsedAsValues`,通过它可以来控制没被使用的导入语句将会被如何处理,它的名字是暂定的,但是它提供来三个不同的选项。 - `remove`,这是现在的行为 —— 丢弃这些导入语句。这仍然是默认行为,没有破坏性的更改 - `preserve`,它将会保留所有的语句,即使是从来没有被使用。它可以保留副作用 - `error`,它将会保留所有的导入(与 `preserve` 选项相同)语句,但是当一个值的导入仅仅用于类型时将会抛出错误。如果你想确保没有意外导入任何值,这会是有用的,但是对于副作用,你仍然需要添加额外的导入语法。 对于该特性的更多信息,参考该 [PR](https://github.com/microsoft/TypeScript/pull/35200)。 ## ECMAScript 私有字段 TypeScript 3.8 支持在 ECMAScript 中处于 [stage-3](https://github.com/tc39/proposal-class-fields/) 中的私有字段。 ```ts class Person { #name: string constructor(name: string) { this.#name = name; } greet() { console.log(`Hello, my name is ${this.#name}!`); } } let jeremy = new Person("Jeremy Bearimy"); jeremy.#name // ~~~~~ // Property '#name' is not accessible outside class 'Person' // because it has a private identifier. ``` 不同于正常属性(甚至是使用 `private` 修饰符声明的属性),私有字段有一些需要记住的规则: - 私有字段使用 `#` 字符作为开始,通常,我们也把这些称为私有名称。 - 每个私有字段的名字,在被包含的类中,都是唯一的 - 在 TypeScript 中,像 `public` 和 `private` 修饰符不能用于私有字段 - 私有字段不能在所包含的类之外访问 —— 即使是对于 JavaScript 使用者来说也是如此。通常,我们把这种称为「hard privacy」。 除了「hard privacy」,私有字段的另外一个优点是我们先前提到的唯一性。 正常的属性容易被子类所改写 ```ts class C { foo = 10; cHelper() { return this.foo; } } class D extends C { foo = 20; dHelper() { return this.foo; } } let instance = new D(); // 'this.foo' refers to the same property on each instance. console.log(instance.cHelper()); // prints '20' console.log(instance.dHelper()); // prints '20' ``` 使用私有字段时,你完全不必对此担心,因为每个私有字段,在所包含的类中,都是唯一的 ```ts class C { #foo = 10; cHelper() { return this.#foo; } } class D extends C { #foo = 20; dHelper() { return this.#foo; } } let instance = new D(); // 'this.#foo' refers to a different field within each class. console.log(instance.cHelper()); // prints '10' console.log(instance.dHelper()); // prints '20' ``` 另外有一个值得注意的地方,访问一个有其他类型的私有字段,都将导致 `TypeError`。 ```ts class Square { #sideLength: number; constructor(sideLength: number) { this.#sideLength = sideLength; } equals(other: any) { return this.#sideLength === other.#sideLength; } } const a = new Square(100); const b = { sideLength: 100 }; // Boom! // TypeError: attempted to get private field on non-instance // This fails because 'b' is not an instance of 'Square'. console.log(a.equals(b)); ``` 对于类属性来说,JavaScript 总是允许使用者访问没被声明的属性,而 TypeScript 需要使用者在访问之前先定义声明。使用私有字段时,无论时 `.js` 文件还是 `.ts`,都需要先声明。 ```ts class C { /** @type {number} */ #foo; constructor(foo: number) { // This works. this.#foo = foo; } } ``` 更多信息,请查看此 [PR](https://github.com/Microsoft/TypeScript/pull/30829)。 ### 该使用哪个? 我们已经收到很多关于「我该使用 `private` 关键字,还是使用 ECMAScript 提供的私有字段 `#` 了?」这类的问题。 像所有其他好的问题一样,答案总是令人遗憾的:它取决你。 在属性方面,TypeScript `private` 修饰符在编译后将会被删除 —— 因此,尽管有数据存在,但是在输出的 JavaScript 代码中没有关于该属性声明的任何编码。在运行时,它的行为就像一个普通的属性。当你使用 `private` 关键字时,私有属性的有关行为只会出现在编译阶段/设计阶段,而对于 JavaScript 消费者来说,则是完全无感知的。 ```ts class C { private foo = 10; } // This is an error at compile time, // but when TypeScript outputs .js files, // it'll run fine and print '10'. console.log(new C().foo); // prints '10' // ~~~ // error! Property 'foo' is private and only accessible within class 'C'. // TypeScript allows this at compile-time // as a "work-around" to avoid the error. console.log(new C()['foo']); // prints '10' ``` 另一方面,ECMAScript 私有属性无法在类之外访问。 ```ts class C { #foo = 10; } console.log(new C().#foo); // SyntaxError // ~~~~ // TypeScript reports an error *and* // this won't work at runtime! console.log(new C()["#foo"]); // prints undefined // ~~~~~~~~~~~~~~~ // TypeScript reports an error under 'noImplicitAny', // and this prints 'undefined'. ``` 「hard privacy」对于确保没有人能使用你的任何内部变量是有用的,如果你是一个库的作者,移除或者重命名一个私有字段不会造成任何重大变化。 正如上文所述,使用 ECMAScript 的私有字段,创建子类会更容易,因为它们是**真**私有。当使用 ECMAScript 私有字段时,子类无需担心字段名字的冲突。当使用 TypeScript `private` 属性声明时,使用者仍然需要小心不要覆盖父类中的相同字段。 最后,还有一些你需要考虑的事情,比如你打算让你的代码在哪运行?当前,TypeScript 只有在编译目标为 ECMAScript 2015(ES6)及其以上时,才能支持该私有字段。因为我们在底层使用 `WeakMaps` 实现这种方法 —— `WeakMaps` 并不能以一种不会导致内存泄漏的方式 polyfill。对比而言,TypeScript 的 `private` 声明属性能在所有的编译目标下正常工作 —— 甚至是 ECMAScript 3。 ## `export * as ns` 语法 以下方式很常见 ```ts import * as utilities from './utilities.js'; export { utilities }; ``` 在 ECMAScript 2020 中,添加了一种新的语法来支持该模式: ```TS export * as utilities from "./utilities.js"; ``` 这是一次 JavaScript 代码质量的改进,TypeScript 3.8 实现了此语法。 当你的编译目标早于 `es2020` 时,TypeScript 将会按照第一个代码片段输出内容。 ## `Top-Level await` 大多数使用 JavaScript 提供 I/O(如 http 请求)的现代环境都是异步的,并且很多现代 API 都返回 `Promise`。尽管它在使操作无阻塞方面有诸多优点,但是它确实在一些如读取文件或外部内容时,会让人厌烦。 ```ts fetch('...') .then(response => response.text()) .then(greeting => { console.log(greeting); }); ``` 为了避免使用 `Promise` 中 `.then` 的链式操作符,JavaScript 使用者通常会引入 `async` 函数以使用 `await`,在定义该函数之后,立即调用该函数。 ```ts async function main() { const response = await fetch('...'); const greeting = await response.text(); console.log(greeting); } main().catch(e => console.error(e)); ``` 为了避免引入 `async` 函数,我们可以使用一个简便的语法,它在即将到来的 ECMAScript feature 中被称为 `top-level await`。 在当前的 JavaScript 中(以及其他具有相似功能的大多数其他语言),`await` 仅仅只能用于 `async` 函数内部。然而,使用 `top-level await` 时,我们可以在一个模块的顶层使用 `await`。 ```ts const response = await fetch('...'); const greeting = await response.text(); console.log(greeting); // Make sure we're a module export {}; ``` 这里有一个细节:`top-level await` 仅仅只能在一个模块的顶层工作 —— 仅当 TypeScript 发现文件代码中含有 `export` 或者 `import` 时,才会认为该文件是一个模块。在一些基础的实践中,你可能需要写下 `export {}` 作为样板,来确保这种行为。 `top-level await` 并不会在你可能期望的所有环境下工作。现在,只有在编译目标选项是 `es2017` 及其以上,`top-level await` 才能被使用,并且 `module` 选项必须为 `esnext` 或者 `system`。 更多相关信息,请查看该 [PR](https://github.com/microsoft/TypeScript/pull/35813)。 ## JSDoc 属性修饰符 TypeScript 3.8 通过打开 `allowJs` 选项,能支持 JavaScript 文件,并且当使用 `checkJs` 选项或者在你的 `.js` 文件顶部中添加 `// @ts-check` 注释时,TypeScript 能对这些 `.js` 文件进行类型检查。 由于 JavaScript 文件没有专用的语法来进行类型检查,因此 TypeScript 选择利用 JSDoc。TypeScript 3.8 能理解一些新的 JSDoc 属性标签。 首先是所有的访问修饰符:`@public`、`@private`、`@protected`。这些标签的工作方式与 TypeScript 中 `public`、`private`、`protected` 相同。 ```js // @ts-check class Foo { constructor() { /** @private */ this.stuff = 100; } printStuff() { console.log(this.stuff); } } new Foo().stuff; // ~~~~~ // error! Property 'stuff' is private and only accessible within class 'Foo'. ``` - `@public` 是默认的,可以省略,它代表了一个属性可以从任何地方访问它 - `@private` 表示一个属性只能在包含的类中访问 - `@protected` 表示该属性只能在所包含的类及子类中访问,但不能在类的实例中访问 下一步,我们计划添加 `@readonly` 修饰符,来确保一个属性只能在初始化时被修改: ```ts // @ts-check class Foo { constructor() { /** @readonly */ this.stuff = 100; } writeToStuff() { this.stuff = 200; // ~~~~~ // Cannot assign to 'stuff' because it is a read-only property. } } new Foo().stuff++; // ~~~~~ // Cannot assign to 'stuff' because it is a read-only property. ``` ## watchOptions 一直以来,TypeScript 致力于在 `--watch` 模式下和编辑器中提供可靠的文件监听功能。尽管在大部分情况下,它都能很好的工作,但是在 Node.js 中,文件监控非常困难,这主要体现在我们的代码逻辑中。在 Node.js 中内置的 API 中,要么占用大量的 CPU 资源,要么不准确([fs.watchFile](https://nodejs.org/api/fs.html#fs_fs_watchfile_filename_options_listener)),甚至它们在各个平台的行为不一致([fs.watch](https://nodejs.org/api/fs.html#fs_fs_watch_filename_options_listener))。除此之外,我们几乎不可能确定哪个 API 会更好的工作,因为它们不仅依赖于平台,还取决于文件所在的文件系统。 这一直是个难题,因为 TypeScript 需要在更多平台上运行,而不仅仅是 Node.js。并且需要考虑到避免依赖模块完全独立。这尤其适用于对 Node.js 原生模块有依赖的模块。 由于每个项目在不同的策略下都可能更好的工作,TypeScript 3.8 在 `tsconfig.json` 和 `jsconfig.json` 中添加了一个新的 `watchOptions` 字段,它可以让使用者告诉编译器/语言服务,应该使用哪种监听策略来跟踪文件或目录。 ```ts { // Some typical compiler options "compilerOptions": { "target": "es2020", "moduleResolution": "node", // ... }, // NEW: Options for file/directory watching "watchOptions": { // Use native file system events for files and directories "watchFile": "useFsEvents", "watchDirectory": "useFsEvents", // Poll files for updates more frequently // when they're updated a lot. "fallbackPolling": "dynamicPriority" } } ``` `watchOptions` 包含四种新的选项 - `watchFile`:监听单个文件的策略,它可以有以下值 - `fixedPollingInterval`,以固定的时间间隔,检查文件的更改 - `priorityPollingInterval`,以固定的时间间隔,检查文件的更改,但是使用「启发法」(heuristics)检查某些类型的文件的频率比其他文件低。(heuristics 概念可参考 [wiki](https://zh.wikipedia.org/wiki/%E5%90%AF%E5%8F%91%E6%B3%95)) - `dynamicPriorityPolling`,使用动态队列,在该队列中,较少检查不经常修改的文件 - `useFsEvents`(默认),尝试使用操作系统/文件系统原生事件来监听文件更改 - `useFsEventsOnParentDirectory`,尝试使用操作系统/文件系统原生事件来监听文件、目录的更改,这样可以使用较小的文件监听程序,但是准确性可能较低 - `watchDirectory`,在缺少递归文件监听功能的系统中,使用哪种策略监听整个目录树,它可以有以下值 - `fixedPollingInterval`,以固定的时间间隔,检查目录树的更改 - `dynamicPriorityPolling`,使用动态队列,在该队列中,较少检查不经常修改的目录 - `useFsEvents`(默认),尝试使用操作系统/文件系统原生事件来监听目录更改 - `fallbackPolling`,当使用文件系统的事件,该选项用来指定使用特定策略,它可以有以下值 - `fixedPollingInterval`,同上 - `priorityPollingInterval`,同上 - `dynamicPriorityPolling`,同上 - `synchronousWatchDirectory`,在目录上禁用延迟监听功能。在可能一次发生大量文件(如 node_modules)更改时,它非常有用,但是你可能需要一些不太常见的设置时,禁用它。 ## “快速和宽松”的增量检查 TypeScript 3.8 带来了一种新的变异选项 —— `assumeChangesOnlyAffectDirectDependencies`。当该选项开启时,TypeScript 将不会重新检查/构建可能受影响的文件,仅仅重新检查/构建已更改的文件和直接导入它们的文件。 例如:`fileD.ts` 导入 `fileC.ts`,`fileC.ts` 导入 `fileB.ts`,`fileB.ts` 导入 `fileA.ts` 文件。 ```ts fileA.ts < -fileB.ts < -fileC.ts < -fileD.ts; ``` 在 `--watch` 模式下,改变 `fileA.ts` 文件通常意味着 TypeScript 需要至少重新检查 `fileB.ts`、`fileC.ts` 和 `fileD.ts`,当使用 `assumeChangesOnlyAffectDirectDependencies` 时,`fileA.ts` 改变,意味着只需要检查 `fileA.ts` 和 `fileB.ts` 即可。 在类似与 VSCode 的代码库中,使用该编译选项时,某些文件的构建时间从大约 14s 减小到 1s。然而我们并不推荐所有的代码库中都使用该编译选项,你可能对拥有庞大代码库时,延迟提示所有错误更感兴趣(例如一个专用的配置文件 `tsconfig.fullbuild.json` 或者是 CI 中)。 ================================================ FILE: docs/new/typescript-3.9.md ================================================ # TypeScript 3.9 [TypeScript3.9 —— 前端之颠](https://wemp.app/posts/7b99df08-8245-4dbe-8514-c10919779e6a) ================================================ FILE: docs/project/compilationContext.md ================================================ # 编译上下文 编译上下文算是一个比较花哨的术语,可以用它来给文件分组,告诉 TypeScript 哪些文件是有效的,哪些是无效的。除了有效文件所携带信息外,编译上下文还包含有正在被使用的编译选项的信息。定义这种逻辑分组,一个比较好的方式是使用 `tsconfig.json` 文件。 ## tsconfig.json ### 基础 开始使用 `tsconfig.json` 是一件比较容易的事,你仅仅需要写下: ```json {} ``` 例如,在项目的根目录下创建一个空 JSON 文件。通过这种方式,TypeScript 将 会把此目录和子目录下的所有 .ts 文件作为编译上下文的一部分,它还会包含一部分默认的编译选项。 ### 编译选项 你可以通过 `compilerOptions` 来定制你的编译选项: ```js { "compilerOptions": { /* 基本选项 */ "target": "es5", // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT' "module": "commonjs", // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015' "lib": [], // 指定要包含在编译中的库文件 "allowJs": true, // 允许编译 javascript 文件 "checkJs": true, // 报告 javascript 文件中的错误 "jsx": "preserve", // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react' "declaration": true, // 生成相应的 '.d.ts' 文件 "sourceMap": true, // 生成相应的 '.map' 文件 "outFile": "./", // 将输出文件合并为一个文件 "outDir": "./", // 指定输出目录 "rootDir": "./", // 用来控制输出目录结构 --outDir. "removeComments": true, // 删除编译后的所有的注释 "noEmit": true, // 不生成输出文件 "importHelpers": true, // 从 tslib 导入辅助工具函数 "isolatedModules": true, // 将每个文件作为单独的模块 (与 'ts.transpileModule' 类似). /* 严格的类型检查选项 */ "strict": true, // 启用所有严格类型检查选项 "noImplicitAny": true, // 在表达式和声明上有隐含的 any类型时报错 "strictNullChecks": true, // 启用严格的 null 检查 "noImplicitThis": true, // 当 this 表达式值为 any 类型的时候,生成一个错误 "alwaysStrict": true, // 以严格模式检查每个模块,并在每个文件里加入 'use strict' /* 额外的检查 */ "noUnusedLocals": true, // 有未使用的变量时,抛出错误 "noUnusedParameters": true, // 有未使用的参数时,抛出错误 "noImplicitReturns": true, // 并不是所有函数里的代码都有返回值时,抛出错误 "noFallthroughCasesInSwitch": true, // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿) /* 模块解析选项 */ "moduleResolution": "node", // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6) "baseUrl": "./", // 用于解析非相对模块名称的基目录 "paths": {}, // 模块名到基于 baseUrl 的路径映射的列表 "rootDirs": [], // 根文件夹列表,其组合内容表示项目运行时的结构内容 "typeRoots": [], // 包含类型声明的文件列表 "types": [], // 需要包含的类型声明文件名列表 "allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入。 /* Source Map Options */ "sourceRoot": "./", // 指定调试器应该找到 TypeScript 文件而不是源文件的位置 "mapRoot": "./", // 指定调试器应该找到映射文件而不是生成文件的位置 "inlineSourceMap": true, // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件 "inlineSources": true, // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性 /* 其他选项 */ "experimentalDecorators": true, // 启用装饰器 "emitDecoratorMetadata": true // 为装饰器提供元数据的支持 } } ``` 关于这些(或者更多)编译选项,稍后将会讨论。 ### TypeScript 编译 好的 IDE 支持对 TypeScript 的即时编译。但是,如果你想在使用 `tsconfig.json` 时从命令行手动运行 TypeScript 编译器,你可以通过以下方式: - 运行 tsc,它会在当前目录或者是父级目录寻找 `tsconfig.json` 文件。 - 运行 `tsc -p ./path-to-project-directory` 。当然,这个路径可以是绝对路径,也可以是相对于当前目录的相对路径。 你甚至可以使用 `tsc -w` 来启用 TypeScript 编译器的观测模式,在检测到文件改动之后,它将重新编译。 ## 指定文件 你也可以显式指定需要编译的文件: ```js { "files": [ "./some/file.ts" ] } ``` 你还可以使用 `include` 和 `exclude` 选项来指定需要包含的文件和排除的文件: ```js { "include": [ "./folder" ], "exclude": [ "./folder/**/*.spec.ts", "./folder/someSubFolder" ] } ``` :::tip 注意 使用 `globs`:`**/*` (一个示例用法:`some/folder/**/*`)意味着匹配所有的文件夹和所有文件(扩展名为 `.ts/.tsx`,当开启了 `allowJs: true` 选项时,扩展名可以是 `.js/.jsx`)。 ::: ================================================ FILE: docs/project/declarationspaces.md ================================================ # 声明空间 在 TypeScript 里存在两种声明空间:类型声明空间与变量声明空间。下文将分别讨论这两个概念。 ## 类型声明空间 类型声明空间包含用来当做类型注解的内容,例如下面的类型声明: ```ts class Foo {} interface Bar {} type Bas = {}; ``` 你可以将 `Foo`, `Bar`, `Bas` 作为类型注解使用,示例如下: ```ts let foo: Foo; let bar: Bar; let bas: Bas; ``` 注意,尽管你定义了 `interface Bar`,却并不能够把它作为一个变量来使用,因为它没有定义在变量声明空间中。 ```ts interface Bar {} const bar = Bar; // Error: "cannot find name 'Bar'" ``` 出现错误提示: `cannot find name 'Bar'` 的原因是名称 `Bar` 并未定义在变量声明空间。这将带领我们进入下一个主题 -- 变量声明空间。 ## 变量声明空间 变量声明空间包含可用作变量的内容,在上文中 `Class Foo` 提供了一个类型 `Foo` 到类型声明空间,此外它同样提供了一个变量 `Foo` 到变量声明空间,如下所示: ```ts class Foo {} const someVar = Foo; const someOtherVar = 123; ``` 这很棒,尤其是当你想把一个类来当做变量传递时。 ::: warning 我们并不能把一些如 `interface` 定义的内容当作变量使用。 ::: 与此相似,一些用 `var` 声明的变量,也只能在变量声明空间使用,不能用作类型注解。 ```js const foo = 123; let bar: foo; // ERROR: "cannot find name 'foo'" ``` 提示 `ERROR: "cannot find name 'foo'"` 原因是,名称 foo 没有定义在类型声明空间里。 ================================================ FILE: docs/project/dynamicImportExpressions.md ================================================ # 动态导入表达式 动态导入表达式是 ECMAScript 的一个新功能,它允许你在程序的任意位置异步加载一个模块,TC39 JavaScript 委员会有一个提案,目前处于第四阶段,它被称为 [import() proposal for JavaScript](https://github.com/tc39/proposal-dynamic-import)。 此外,**webpack** bundler 有一个 [`Code Splitting`](https://webpack.js.org/guides/code-splitting/) 功能,它能允许你将代码拆分为许多块,这些块在将来可被异步下载。因此,你可以在程序中首先提供一个最小的程序启动包,并在将来异步加载其他模块。 这很自然就会让人想到(如果我们工作在 webpack dev 的工作流程中)[TypeScript 2.4 dynamic import expressions](https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#dynamic-import-expressions) 将会把你最终生成的 JavaScript 代码自动分割成很多块。但是这似乎并不容易实现,因为它依赖于我们正在使用的 `tsconfig.json` 配置文件。 webpack 实现代码分割的方式有两种:使用 `import()` (首选,ECMAScript 的提案)和 `require.ensure()` (最后考虑,webpack 具体实现)。因此,我们期望 TypeScript 的输出是保留 `import()` 语句,而不是将其转化为其他任何代码。 让我们来看一个例子,在这个例子中,我们演示了如何配置 webpack 和 TypeScript 2.4 +。 在下面的代码中,我希望懒加载 `moment` 库,同时我也希望使用代码分割的功能,这意味 `moment` 会被分割到一个单独的 JavaScript 文件,当它被使用时,会被异步加载。 ```ts import(/* webpackChunkName: "momentjs" */ 'moment') .then(moment => { // 懒加载的模块拥有所有的类型,并且能够按期工作 // 类型检查会工作,代码引用也会工作 :100: const time = moment().format(); console.log('TypeScript >= 2.4.0 Dynamic Import Expression:'); console.log(time); }) .catch(err => { console.log('Failed to load moment', err); }); ``` 这是 `tsconfig.json` 的配置文件: ```js { "compilerOptions": { "target": "es5", "module": "esnext", "lib": [ "dom", "es5", "scripthost", "es2015.promise" ], "jsx": "react", "declaration": false, "sourceMap": true, "outDir": "./dist/js", "strict": true, "moduleResolution": "node", "typeRoots": [ "./node_modules/@types" ], "types": [ "node", "react", "react-dom" ] } } ``` :::danger 重要的提示 - 使用 `"module": "esnext"` 选项:TypeScript 保留 `import()` 语句,该语句用于 Webpack Code Splitting。 - 进一步了解有关信息,推荐阅读这篇文章:[Dynamic Import Expressions and webpack 2 Code Splitting integration with TypeScript 2.4.](https://blog.josequinto.com/2017/06/29/dynamic-import-expressions-and-webpack-code-splitting-integration-with-typescript-2-4/) ::: ================================================ FILE: docs/project/modules.md ================================================ # 模块 ## 全局模块 在默认情况下,当你开始在一个新的 TypeScript 文件中写下代码时,它处于全局命名空间中。如在 foo.ts 里的以下代码。 ```ts const foo = 123; ``` 如果你在相同的项目里创建了一个新的文件 `bar.ts`,TypeScript 类型系统将会允许你使用变量 `foo`,就好像它在全局可用一样: ```ts const bar = foo; // allowed ``` 毋庸置疑,使用全局变量空间是危险的,因为它会与文件内的代码命名冲突。我们推荐使用下文中将要提到的文件模块。 ## 文件模块 文件模块也被称为外部模块。如果在你的 TypeScript 文件的根级别位置含有 `import` 或者 `export`,那么它会在这个文件中创建一个本地的作用域。因此,我们需要把上文 `foo.ts` 改成如下方式(注意 `export` 用法): ```ts export const foo = 123; ``` 在全局命名空间里,我们不再有 `foo`,这可以通过创建一个新文件 `bar.ts` 来证明: ```ts const bar = foo; // ERROR: "cannot find name 'foo'" ``` 如果你想在 `bar.ts` 里使用来自 `foo.ts` 的内容,你必须显式地导入它,更新后的 `bar.ts` 如下所示。 ```ts import { foo } from './foo'; const bar = foo; // allow ``` 在 `bar.ts` 文件里使用 `import` 时,它不仅允许你使用从其他文件导入的内容,还会将此文件 `bar.ts` 标记为一个模块,文件内定义的声明也不会“污染”全局命名空间 ## 文件模块详情 文件模块拥有强大的功能和较强的可用性。下面我们来讨论它的功能及一些用法。 ### 澄清:commonjs, amd, es modules, others 首先,我们需要澄清这些模块系统的不一致性。我将会提供给你我当前的建议,以及消除一些你的顾虑。 你可以根据不同的 `module` 选项来把 TypeScript 编译成不同的 JavaScript 模块类型,这有一些你可以忽略的东西: - AMD:不要使用它,它仅能在浏览器工作; - SystemJS:这是一个好的实验,已经被 ES 模块替代; - ES 模块:它并没有准备好。 使用 `module: commonjs` 选项来替代这些模式,将会是一个好的主意。 怎么书写 TypeScript 模块呢?,这也是一件让人困惑的事。在今天我们应该这么做: - 放弃使用 `import/require` 语法即 `import foo = require('foo')` 写法 - 推荐使用 ES 模块语法 这很酷,接下来,让我们看看 ES 模块语法。 :::tip 使用 `module: commonjs` 选项以及使用 ES 模块语法导入、导出、编写模块。 ::: ### ES 模块语法 - 使用 `export` 关键字导出一个变量或类型 ```ts // foo.ts export const someVar = 123; export type someType = { foo: string; }; ``` - `export` 的写法除了上面这种,还有另外一种: ```ts // foo.ts const someVar = 123; type someType = { type: string; }; export { someVar, someType }; ``` - 你也可以用重命名变量的方式导出: ```ts // foo.ts const someVar = 123; export { someVar as aDifferentName }; ``` - 使用 `import` 关键字导入一个变量或者是一个类型: ```ts // bar.ts import { someVar, someType } from './foo'; ``` - 通过重命名的方式导入变量或者类型: ```ts // bar.ts import { someVar as aDifferentName } from './foo'; ``` - 除了指定加载某个输出值,还可以使用整体加载,即用星号(\*)指定一个对象,所有输出值都加载在这个对象上面: ```ts // bar.ts import * as foo from './foo'; // 你可以使用 `foo.someVar` 和 `foo.someType` 以及其他任何从 `foo` 导出的变量或者类型 ``` - 只导入模块: ```ts import 'core-js'; // 一个普通的 polyfill 库 ``` - 从其他模块导入后整体导出: ```ts export * from './foo'; ``` - 从其他模块导入后,部分导出: ```ts export { someVar } from './foo'; ``` - 通过重命名,部分导出从另一个模块导入的项目: ```ts export { someVar as aDifferentName } from './foo'; ``` #### 默认导入/导出 我并不喜欢用默认导出,虽然有默认导出的语法: - 使用 `export default` - 在一个变量之前(不需要使用 `let/const/var`); - 在一个函数之前; - 在一个类之前。 ```ts // some var export default (someVar = 123); // some function export default function someFunction() {} // some class export default class someClass {} ``` - 导入使用 `import someName from 'someModule'` 语法(你可以根据需要为导入命名): ```ts import someLocalNameForThisFile from './foo'; ``` ### 模块路径 :::tip 如果你需要使用 `moduleResolution: node` 选项,你应该将此选项放入你的配置文件中。如果你使用了 `module: commonjs` 选项, `moduleResolution: node` 将会默认开启。 ::: 这里存在两种截然不同的模块: - 相对模块路径(路径以 `.` 开头,例如:`./someFile` 或者 `../../someFolder/someFile` 等); - 其他动态查找模块(如:`core-js`,`typestyle`,`react` 或者甚至是 `react/core` 等)。 它们的主要区别在于系统如何解析模块。 :::tip 我将会使用一个概念性术语,`place` -- 将在提及查找模式后解释它。 ::: #### 相对模块路径 这很简单,仅仅是按照相对路径来就可以了: - 如果文件 `bar.ts` 中含有 `import * as foo from './foo'`,那么 `foo` 文件必须与 `bar.ts` 文件存在于相同的文件夹下 - 如果文件 `bar.ts` 中含有 `import * as foo from '../foo'`,那么 `foo` 文件所存在的地方必须是 `bar.ts` 的上一级目录; - 如果文件 `bar.ts` 中含有 `import * as foo from '../someFolder/foo'`,那么 `foo` 文件所在的文件夹 `someFolder` 必须与 `bar.ts` 文件所在文件夹在相同的目录下。 你还可以思考一下其他相对路径导入的场景。:smiley: #### 动态查找 当导入路径不是相对路径时,模块解析将会模仿 [Node 模块解析策略](https://nodejs.org/api/modules.html#modules_all_together),下面我将给出一个简单例子: - 当你使用 `import * as foo from 'foo'`,将会按如下顺序查找模块: - `./node_modules/foo` - `../node_modules/foo` - `../../node_modules/foo` - 直到系统的根目录 - 当你使用 `import * as foo from 'something/foo'`,将会按照如下顺序查找内容 - `./node_modules/something/foo` - `../node_modules/something/foo` - `../../node_modules/something/foo` - 直到系统的根目录 ### 什么是 `place` 当我提及被检查的 `place` 时,我想表达的是在这个 `place` 上,TypeScript 将会检查以下内容(例如一个 `foo` 的 `place`): - 如果这个 `place` 表示一个文件,如:`foo.ts`,欢呼! - 否则,如果这个 `place` 是一个文件夹,并且存在一个文件 `foo/index.ts`,欢呼! - 否则,如果这个 `place` 是一个文件夹,并且存在一个 `foo/package.json` 文件,在该文件中指定 `types` 的文件存在,那么就欢呼! - 否则,如果这个 `place` 是一个文件夹,并且存在一个 `package.json` 文件,在该文件中指定 `main` 的文件存在,那么就欢呼! 从文件类型上来说,我实际上是指 `.ts`, `.d.ts` 或者 `.js` 就是这样,现在你已经是一个模块查找专家(这并不是一个小小的成功)。 ### 重写类型的动态查找 在你的项目里,你可以通过 `declare module 'somePath'` 声明一个全局模块的方式,来解决查找模块路径的问题。 ```ts // global.d.ts declare module 'foo' { // some variable declarations export var bar: number; } ``` 接着 : ```ts // anyOtherTsFileInYourProject.ts import * as foo from 'foo'; // TypeScript 将假设(在没有做其他查找的情况下) // foo 是 { bar: number } ``` ### `import/require` 仅仅是导入类型 以下导入语法: ```ts import foo = require('foo'); ``` 它实际上只做了两件事: - 导入 foo 模块的所有类型信息; - 确定 foo 模块运行时的依赖关系。 你可以选择仅加载类型信息,而没有运行时的依赖关系。在继续之前,你可能需要重新阅读本书 [声明空间部分](./declarationspaces.md) 部分。 如果你没有把导入的名称当做变量声明空间来用,在编译成 JavaScript 时,导入的模块将会被完全移除。这最好用例子来解释,下面我们将会给出一些示例。 #### 例子 1 ```ts import foo = require('foo'); ``` 将会编译成 JavaScript: ```js ``` 这是正确的,一个没有被使用的空文件。 #### 例子 2 ```ts import foo = require('foo'); var bar: foo; ``` 将会被编译成: ```js let bar; ``` 这是因为 foo (或者其他任何属性如:`foo.bas`)没有被当做一个变量使用。 #### 例子 3 ```ts import foo = require('foo'); const bar = foo; ``` 将会被编译成(假设是 commonjs): ```js const foo = require('foo'); const bar = foo; ``` 这是因为 `foo` 被当做变量使用了。 #### 使用例子:懒加载 类型推断需要提前完成,这意味着,如果你想在 `bar` 文件里,使用从其他文件 `foo` 导出的类型,你将不得不这么做: ```ts import foo = require('foo'); let bar: foo.SomeType; ``` 然而,在某些情景下,你只想在需要时加载模块 `foo`,此时你需要仅在类型注解中使用导入的模块名称,而**不**是在变量中使用。在编译成 JavaScript 时,这些将会被移除。接着,你可以手动导入你需要的模块。 作为一个例子,考虑以下基于 `commonjs` 的代码,我们仅在一个函数内导入 `foo` 模块: ```ts import foo = require('foo'); export function loadFoo() { // 这是懒加载 foo,原始的加载仅仅用来做类型注解 const _foo: typeof foo = require('foo'); // 现在,你可以使用 `_foo` 替代 `foo` 来作为一个变量使用 } ``` 一个同样简单的 `amd` 模块(使用 requirejs): ```ts import foo = require('foo'); export function loadFoo() { // 这是懒加载 foo,原始的加载仅仅用来做类型注解 require(['foo'], (_foo: typeof foo) => { // 现在,你可以使用 `_foo` 替代 `foo` 来作为一个变量使用 }); } ``` 这些通常在以下情景使用: - 在 web app 里, 当你在特定路由上加载 JavaScript 时; - 在 node 应用里,当你只想加载特定模块,用来加快启动速度时。 #### 使用例子:打破循环依赖 类似于懒加载的使用用例,某些模块加载器(commonjs/node 和 amd/requirejs)不能很好的处理循环依赖。在这种情况下,一方面我们使用延迟加载代码,并在另一方面预先加载模块是很实用的。 #### 使用例子:确保导入 当你加载一个模块,只是想引入其附加的作用(如:模块可能会注册一些像 [CodeMirror addons](https://codemirror.net/doc/manual.html#addons))时,然而,如果你仅仅是 `import/require` (导入)一些并没有与你的模块或者模块加载器有任何依赖的 JavaScript 代码,(如:webpack),经过 TypeScript 编译后,这些将会被完全忽视。在这种情况下,你可以使用一个 `ensureImport` 变量,来确保编译的 JavaScript 依赖与模块。如: ```ts import foo = require('./foo'); import bar = require('./bar'); import bas = require('./bas'); const ensureImport: any = foo || bar || bas; ``` ## global.d.ts 在上文中,当我们讨论文件模块时,比较了全局变量与文件模块,并且我们推荐使用基于文件的模块,而不是选择污染全局命名空间。 然而,如果你的团队里有 TypeScript 初学者,你可以提供他们一个 `global.d.ts` 文件,用来将一些接口或者类型放入全局命名空间里,这些定义的接口和类型能在你的所有 TypeScript 代码里使用。 :::tip 对于任何需要编译成 `JavaScript` 的代码,我们强烈建议你放入文件模块里。 ::: - `global.d.ts` 是一种扩充 `lib.d.ts` 很好的方式,如果你需要的话。 - 当你从 `JS` 迁移到 `TS` 时,定义 `declare module "some-library-you-dont-care-to-get-defs-for"` 能让你快速开始。 ================================================ FILE: docs/project/namespaces.md ================================================ # 命名空间 在 JavaScript 使用命名空间时, 这有一个常用的、方便的语法: ```js (function(something) { something.foo = 123; })(something || (something = {})); ``` `something || (something = {})` 允许匿名函数 `function (something) {}` 向现有对象添加内容,或者创建一个新对象,然后向该对象添加内容。这意味着你可以拥有两个由某些边界拆成的块。 ```js (function(something) { something.foo = 123; })(something || (something = {})); console.log(something); // { foo: 123 } (function(something) { something.bar = 456; })(something || (something = {})); console.log(something); // { foo: 123, bar: 456 } ``` 在确保创建的变量不会泄漏至全局命名空间时,这种方式在 JavaScript 中很常见。当基于文件模块使用时,你无须担心这点,但是该模式仍然适用于一组函数的逻辑分组。因此 TypeScript 提供了 `namespace` 关键字来描述这种分组,如下所示。 ```ts namespace Utility { export function log(msg) { console.log(msg); } export function error(msg) { console.log(msg); } } // usage Utility.log('Call me'); Utility.error('maybe'); ``` `namespace` 关键字编译后的 JavaScript 代码,与我们早些时候看到的 JavaScript 代码一样。 ```js (function (Utility) { // 添加属性至 Utility })(Utility || Utility = {}); ``` 值得注意的一点是,命名空间是支持嵌套的。因此,你可以做一些类似于在 `Utility` 命名空间下嵌套一个命名空间 `Messaging` 的事情。 对于大多数项目,我们建议使用外部模块和命名空间,来快速演示和移植旧的 JavaScript 代码。 ================================================ FILE: docs/tips/avoidExportDefault.md ================================================ # `export default` 被认为是有害的 假如你有一个包含以下内容的 `foo.ts` 文件: ```ts class Foo {} export default Foo; ``` 你可能会使用 ES6 语法导入它(在 `bar.ts` 里): ```ts import Foo from './foo'; ``` 这存在一些可维护性的问题: - 如果你在 `foo.ts` 里重构 `Foo`,在 `bar.ts` 文件中,它将不会被重新命名; - 如果你最终需要从 `foo.ts` 文件中导出更多有用的信息(在你的很多文件中都存在这种情景),那么你必须兼顾导入语法。 由于这些原因,我推荐在导入时使用简单的 `export` 与解构的形式,如 `foo.ts`: ```ts export class Foo {} ``` 接着: ```ts import { Foo } from './Foo'; ``` 下面,我将会介绍更多的原因。 ## 可发现性差 默认导出的可发现性非常差,你不能智能的辨别一个模块它是否有默认导出。 在使用默认导出时,你什么也没有得到(可能它有默认导出,可能它没有)。 ```ts import /* here */ from 'something'; ``` 没有默认导出,你可以用以下方式获取智能提示: ```ts import /* here */ 'something'; ``` ## 自动完成 不管你是否了解导出,你都可以在 `import { /* here */ } from './foo'` 的 `here` 位置,来了解导出模块的信息。 ## CommonJS 互用 对于必须使用 `const { default } = require('module/foo')` 而不是 `const { Foo } = require('module/foo')` 的 CommonJS 的用户来说,这会是一个糟糕的体验。当你导入一个模块时,你很可能想重命名 `default` 作为导入的名字。 ## 防止拼写错误 当你在开发时使用 `import Foo from './foo'` 时,并不会得到有关于拼写的任何错误,其他人可能会这么写 `import foo from './foo'`; ## 再次导出 再次导出是没必要的,但是在 `npm` 包的根文件 `index` 却是很常见。如:`import Foo from './foo';export { Foo }`(默认导出)VS `export * from './foo'` (命名导出)。 ## 动态导入 在动态的 `import` 中,默认导出会以 `default` 的名字暴露自己,如: ```ts const HighChart = await import('https://code.highcharts.com/js/es-modules/masters/highcharts.src.js'); HighChart.default.chart('container', { ... }); // Notice `.default` ``` ================================================ FILE: docs/tips/barrel.md ================================================ # Barrel Barrel 就像是一个容器,它的作用是把分散在多个模块的导出合并到一个模块里导出。一般来说,barrel 本身就是一个包含模块的文件,这个模块做的就是重新导出其他(多个)模块导出的东西。 想象一下,在一个库中,具有如下结构的类。 ```ts // demo/foo.ts export class Foo {} // demo/bar.ts export class Bar {} // demo/baz.ts export class Baz {} ``` 如果不用 barrel,那么用户在引入时就需要三条 `import` 语句: ```ts import { Foo } from '../demo/foo'; import { Bar } from '../demo/bar'; import { Baz } from '../demo/baz'; ``` 但如果我们在同级添加 barrel 文件 `demo/index.ts`,然后这样定义它: ```ts // demo/index.ts export * from './foo'; // 重新导出 foo 导出的东西 export * from './bar'; // 重新导出 bar 导出的东西 export * from './baz'; // 重新导出 baz 导出的东西 ``` 现在,用户就可以直接用一条 `import` 语句从 barrel file 导入所有东西: ```ts import { Foo, Bar, Baz } from '../demo'; // ../demo,会自动解析成 ../demo/index.ts ``` ## 命名导出 除了使用通配符 `*` 导出模块中的所有东西,我们也可以选择要导出什么以及如何导出。试想一个存在多个函数的 `baz.ts`: ```ts // demo/foo.ts export class Foo {} // demo/bar.ts export class Bar {} // demo/baz.ts export function getBaz() {} export function setBaz() {} ``` 如果不想在 `demo` 模块上直接提供 `getBaz` 和 `setBaz` 接口,你可以把它们挂载到一个变量下。你需要做的只是在 barrel file 里导入全部并命名,然后导出命名后的名称即可。 ```ts // demo/index.ts export * from './foo'; // 重新导出 foo 导出的东西 export * from './bar'; // 重新导出 bar 导出的东西 import * as baz from './baz'; // 导入 baz 中所有的东西,并命名为 baz export { baz }; // 导出命名后的名称 ``` 现在,用户需要这样调用: ```ts import { Foo, Bar, baz } from '../demo'; // ../demo,会自动解析成 ../demo/index.ts // 使用 baz.getBaz(); baz.setBaz(); // …… ``` ================================================ FILE: docs/tips/bind.md ================================================ # Bind 是有害的 ::: tip 译者注:在这个 [PR](https://github.com/Microsoft/TypeScript/pull/27028?from=timeline&isappinstalled=0) 下,已经解决 `bind`、`call`、`apply` 类型正确推导的问题,预计在 3.2 版本中发布。 ::: 这是在 `lib.d.ts` 中 `bind` 的定义: ```ts bind(thisArg: any, ...argArray: any[]): any ``` 你可以看到他的返回值是 `any`,这意味着在函数上调用 `bind` 会导致你在原始函数调用签名上将会完全失去类型的安全检查。 如下所示: ```ts function twoParams(a: number, b: number) { return a + b; } let curryOne = twoParams.bind(null, 123); curryOne(456); // ok curryOne('456'); // ok ``` 一个更好的方式的是使用类型注解的箭头函数: ```ts function twoParams(a: number, b: number) { return a + b; } let curryOne = (x: number) => twoParams(123, x); curryOne(456); // ok curryOne('456'); // Error ``` 如果你想用一个柯里化的函数,你可以看看[此章节](./curry.md): ## 类成员 另一个常见用途是在传递类函数时使用 `bind` 来确保 `this` 的正确值,不要这么做。 在接下来的示例中,如果你使用了 `bind`,你将会失去函数参数的类型安全: ```ts class Adder { constructor(public a: string) {} add(b: string): string { return this.a + b; } } function useAdd(add: (x: number) => number) { return add(456); } let adder = new Adder('mary had a little 🐑'); useAdd(adder.add.bind(adder)); // 没有编译的错误 useAdd(x => adder.add(x)); // Error: number 不能分配给 string ``` 如果你想传递一个类成员的函数,使用箭头函数。例如: ```ts class Adder { constructor(public a: string) {} // 此时,这个函数可以安全传递 add = (b: string): string => { return this.a + b; }; } ``` 另一种方法是手动指定要绑定的变量的类型: ```ts const add: typeof adder.add = adder.add.bind(adder); ``` ================================================ FILE: docs/tips/buildToggles.md ================================================ # 构建切换 根据 JavaScript 项目的运行环境进行切换环境变量是很常见的,通过 webpack 可以很轻松地做到这一点,因为它支持基于环境变量的死代码排除。 在你的 `package.json script` 里,添加不同的编译目标: ```json "build:test": "webpack -p --config ./src/webpack.config.js", "build:prod": "webpack -p --define process.env.NODE_ENV='\"production\"' --config ./src/webpack.config.js" ``` 当然,假设你已经安装了 webpack `npm install webpack`,现在,你可以运行 `npm run build:test` 了。 使用环境变量也超级简单: ```ts /** * This interface makes sure we don't miss adding a property to both `prod` and `test` */ interface Config { someItem: string; } /** * We only export a single thing. The config. */ export let config: Config; /** * `process.env.NODE_ENV` definition is driven from webpack * * The whole `else` block will be removed in the emitted JavaScript * for a production build */ if (process.env.NODE_ENV === 'production') { config = { someItem: 'prod' }; console.log('Running in prod'); } else { config = { someItem: 'test' }; console.log('Running in test'); } ``` ::: tip 我们使用 `process.env.NODE_ENV` 仅仅是因为绝大多数 JavaScript 库中都使用此变量,例如:`React`。 ::: ================================================ FILE: docs/tips/classAreUseful.md ================================================ # 类是有用的 以下结构在应用中很常见: ```ts function foo() { let someProperty; // 一些其他的初始化代码 function someMethod() { // 用 someProperty 做一些事情 // 可能有其他属性 } // 可能有其他的方法 return { someMethod // 可能有其他方法 }; } ``` 它被称为模块模式(利用 JavaScript 的闭包)。 如果你使用[文件模块](../project/modules.md#文件模块)(你确实应该将全局变量视为错误),文件中的代码与示例一样,都不是全局变量。 然而,开发者有时会写以下类似代码: ```ts let someProperty; function foo() { // 一些初始化代码 } foo(); someProperty = 123; // 其他初始化代码 // 一些其它未导出 // later export function someMethod() {} ``` 尽管我并不是一个特别喜欢使用**继承**的人,但是我确实发现让开发者使用类,可以在一定程度上更好的组织他们的代码,例如: ```ts class Foo { public someProperty; constructor() { // 一些初始化内容 } public someMethod() { // ..code } public someUtility() { // .. code } } export = new Foo(); ``` 这并不仅仅有利于开发者,在创建基于类的更出色可视化工具中,它更常见。并且,这有利于项目的理解和维护。 ::: tip 在浅层次的结构中,如果它们能够提供明显的重复使用和减少模版的好处,那么在这个观点里,我并没有错误。 ::: ================================================ FILE: docs/tips/covarianceAndContravariance.md ================================================ # 协变与逆变 > [原文链接: what are covariance and contravariance](https://www.stephanboyer.com/post/132/what-are-covariance-and-contravariance) [子类型](https://en.wikipedia.org/wiki/Subtyping) 在编程理论上是一个复杂的话题,而他的复杂之处来自于一对经常会被混淆的现象,我们称之为*协变*与*逆变*。这篇文章将会解释上述两个概念。 开始文章之前我们先约定如下的标记: - `A ≼ B` 意味着 `A` 是 `B` 的子类型。 - `A → B` 指的是以 `A` 为参数类型,以 `B` 为返回值类型的函数类型。 - `x : A` 意味着 `x` 的类型为 `A`。 ## 一个有趣的问题 假设我有如下三种类型: > `Greyhound ≼ Dog ≼ Animal` `Greyhound` (灰狗)是 `Dog` (狗)的子类型,而 `Dog` 则是 `Animal` (动物)的子类型。由于子类型通常是可传递的,因此我们也称 `Greyhound` 是 `Animal` 的子类型。 **问题**:以下哪种类型是 `Dog → Dog` 的子类型呢? 1. `Greyhound → Greyhound` 2. `Greyhound → Animal` 3. `Animal → Animal` 4. `Animal → Greyhound` 让我们来思考一下如何解答这个问题。首先我们假设 `f` 是一个以 `Dog → Dog` 为参数的函数。它的返回值并不重要,为了具体描述问题,我们假设函数结构体是这样的: `f : (Dog → Dog) → String`。 现在我想给函数 `f` 传入某个函数 `g` 来调用。我们来瞧瞧当 `g` 为以上四种类型时,会发生什么情况。 **1. 我们假设 `g : Greyhound → Greyhound`, `f(g)` 的类型是否安全?** 不安全,因为在f内调用它的参数`(g)`函数时,使用的参数可能是一个不同于灰狗但又是狗的子类型,例如 `GermanShepherd` (牧羊犬)。 **2. 我们假设 `g : Greyhound → Animal`, `f(g)` 的类型是否安全?** 不安全。理由同(1)。 **3. 我们假设 `g : Animal → Animal`, `f(g)` 的类型是否安全?** 不安全。因为 `f` 有可能在调用完参数之后,让返回值,也就是 `Animal` (动物)狗叫。并非所有动物都会狗叫。 **4. 我们假设 `g : Animal → Greyhound`, `f(g)` 的类型是否安全?** 是的,它的类型是安全的。首先,`f` 可能会以任何狗的品种来作为参数调用,而所有的狗都是动物。其次,它可能会假设结果是一条狗,而所有的灰狗都是狗。 ## 展开讲讲? 如上所述,我们得出结论: > `(Animal → Greyhound) ≼ (Dog → Dog)` 返回值类型很容易理解:灰狗是狗的子类型。但参数类型则是相反的:动物是狗的*父类*! 用合适的术语来描述这个奇怪的表现,可以说我们允许一个函数类型中,返回值类型是*协变*的,而参数类型是*逆变*的。返回值类型是协变的,意思是 `A ≼ B` 就意味着 `(T → A) ≼ (T → B)` 。参数类型是逆变的,意思是 `A ≼ B` 就意味着 `(B → T) ≼ (A → T)` ( `A` 和 `B` 的位置颠倒过来了)。 **一个有趣的现象**:在 `TypeScript` 中, [参数类型是双向协变的](https://github.com/Microsoft/TypeScript/wiki/FAQ#why-are-function-parameters-bivariant) ,也就是说既是协变又是逆变的,而这并不安全。但是现在你可以在 [`TypeScript 2.6`](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-6.html) 版本中通过 `--strictFunctionTypes` 或 `--strict` 标记来修复这个问题。 ## 那其他类型呢? **问题**:`List` 能否为 `List` 的子类型? 答案有点微妙。如果列表是不可变的(immutable),那么答案是肯定的,因为类型很安全。但是假如列表是可变的,那么答案绝对是否定的! 原因是,假设我需要一串 `List` 而你传给我一串 `List`。由于我认为我拥有的是一串 `List` ,我可能会尝试往列表插入一只 `Cat`。那么你的 `List` 里面就会有一只猫!类型系统不应该允许这种情况发生。 总结一下,我们可以允许不变的列表(immutable)在它的参数类型上是协变的,但是对于可变的列表(mutable),其参数类型则必须是不变的(invariant),既不是协变也不是逆变。 **一个有趣的现象**:在 `Java` 中,数组[既是可变的,又是协变的](https://en.wikipedia.org/wiki/Covariance_and_contravariance_%28computer_science%29#Covariant_arrays_in_Java_and_C.23)。当然,这并不安全。 ================================================ FILE: docs/tips/createArrays.md ================================================ # 创建数组 创建数组十分简单: ```ts const foo: string[] = []; ``` 你也可以在创建数组时使用 ES6 的 `Array.prototype.fill` 方法为数组填充数据: ```ts const foo: string[] = new Array(3).fill(''); console.log(foo); // 会输出 ['','',''] ``` ================================================ FILE: docs/tips/curry.md ================================================ # 柯里化 仅仅需要使用一系列箭头函数: ```ts // 一个柯里化函数 let add = (x: number) => (y: number) => x + y; // 简单使用 add(123)(456); // 部分应用 let add123 = add(123); // fully apply the function add123(456); ``` ================================================ FILE: docs/tips/functionParameters.md ================================================ # 函数参数 如果你有一个含有很多参数或者相同类型参数的函数,那么你可能需要考虑将函数改为接收对象的形式: 如下一个函数: ```ts function foo(flagA: boolean, flagB: boolean) { // 函数主体 } ``` 像这样的函数,你可能会很容易错误的调用它,如 `foo(flagB, flagA)`,并且你并不会从编译器得到想要的帮助。 你可以将函数变为接收对象的形式: ```ts function foo(config: { flagA: boolean; flagB: boolean }) { const { flagA, flagB } = config; } ``` 现在,函数将会被 `foo({ flagA, flagB })` 的形式调用,这样有利于发现错误及代码审查。 ::: tip 如果你的函数足够简单,并且你不希望增加代码,忽略这个建议。 ::: ================================================ FILE: docs/tips/infer.md ================================================ # infer ## 介绍 `infer` 最早出现在此 [PR](https://github.com/Microsoft/TypeScript/pull/21496) 中,表示在 `extends` 条件语句中待推断的类型变量。 简单示例如下: ```ts type ParamType = T extends (arg: infer P) => any ? P : T; ``` 在这个条件语句 `T extends (arg: infer P) => any ? P : T` 中,`infer P` 表示待推断的函数参数。 整句表示为:如果 `T` 能赋值给 `(arg: infer P) => any`,则结果是 `(arg: infer P) => any` 类型中的参数 `P`,否则返回为 `T`。 ```ts interface User { name: string; age: number; } type Func = (user: User) => void; type Param = ParamType; // Param = User type AA = ParamType; // string ``` ## 内置类型 在 2.8 版本中,TypeScript 内置了一些与 `infer` 有关的映射类型: - 用于提取函数类型的返回值类型: ```ts type ReturnType = T extends (...args: any[]) => infer P ? P : any; ``` 相比于文章开始给出的示例,`ReturnType` 只是将 `infer P` 从参数位置移动到返回值位置,因此此时 `P` 即是表示待推断的返回值类型。 ```ts type Func = () => User; type Test = ReturnType; // Test = User ``` - 用于提取构造函数中参数(实例)类型: 一个构造函数可以使用 `new` 来实例化,因此它的类型通常表示如下: ```ts type Constructor = new (...args: any[]) => any; ``` 当 `infer` 用于构造函数类型中,可用于参数位置 `new (...args: infer P) => any;` 和返回值位置 `new (...args: any[]) => infer P;`。 因此就内置如下两个映射类型: ```ts // 获取参数类型 type ConstructorParameters any> = T extends new (...args: infer P) => any ? P : never; // 获取实例类型 type InstanceType any> = T extends new (...args: any[]) => infer R ? R : any; class TestClass { constructor(public name: string, public age: number) {} } type Params = ConstructorParameters; // [string, number] type Instance = InstanceType; // TestClass ``` ## 一些用例 至此,相信你已经对 `infer` 已有基本了解,我们来看看一些使用它的「骚操作」: - **tuple** 转 **union** ,如:`[string, number]` -> `string | number` 解答之前,我们需要了解 tuple 类型在一定条件下,是可以赋值给数组类型: ```ts type TTuple = [string, number]; type TArray = Array; type Res = TTuple extends TArray ? true : false; // true type ResO = TArray extends TTuple ? true : false; // false ``` 因此,在配合 `infer` 时,这很容易做到: ```ts type ElementOf = T extends Array ? E : never; type TTuple = [string, number]; type ToUnion = ElementOf; // string | number ``` 在 [stackoverflow](https://stackoverflow.com/questions/44480644/typescript-string-union-to-string-array/45486495#45486495) 上看到另一种解法,比较简(牛)单(逼): ```ts type TTuple = [string, number]; type Res = TTuple[number]; // string | number ``` - **union** 转 **intersection**,如:`T1 | T2` -> `T1 & T2` 这个可能要稍微麻烦一点,需要 `infer` 配合「 [Distributive conditional types](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#distributive-conditional-types) 」使用。 在[相关链接](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#distributive-conditional-types)中,我们可以了解到「Distributive conditional types」是由「naked type parameter」构成的条件类型。而「naked type parameter」表示没有被 `Wrapped` 的类型(如:`Array`、`[T]`、`Promise` 等都是不是「naked type parameter」)。「Distributive conditional types」主要用于拆分 `extends` 左边部分的联合类型,举个例子:在条件类型 `T extends U ? X : Y` 中,当 `T` 是 `A | B` 时,会拆分成 `A extends U ? X : Y | B extends U ? X : Y`; 有了这个前提,再利用在逆变位置上,[同一类型变量的多个候选类型将会被推断为交叉类型](https://github.com/Microsoft/TypeScript/pull/21496)的特性,即 ```ts type T1 = { name: string }; type T2 = { age: number }; type Bar = T extends { a: (x: infer U) => void; b: (x: infer U) => void } ? U : never; type T20 = Bar<{ a: (x: string) => void; b: (x: string) => void }>; // string type T21 = Bar<{ a: (x: T1) => void; b: (x: T2) => void }>; // T1 & T2 ``` 因此,综合以上几点,我们可以得到在 [stackoverflow](https://stackoverflow.com/questions/50374908/transform-union-type-to-intersection-type) 上的一个答案: ```ts type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; type Result = UnionToIntersection; // T1 & T2 ``` 当传入 `T1 | T2` 时: - 第一步:`(U extends any ? (k: U) => void : never)` 会把 union 拆分成 `(T1 extends any ? (k: T1) => void : never) | (T2 extends any ? (k: T2)=> void : never)`,即是得到 `(k: T1) => void | (k: T2) => void`; - 第二步:`(k: T1) => void | (k: T2) => void extends ((k: infer I) => void) ? I : never`,根据上文,可以推断出 `I` 为 `T1 & T2`。 当然,你可以玩出更多花样,比如 [**union** 转 **tuple**](https://zhuanlan.zhihu.com/p/58704376)。 ## LeetCode 的一道 TypeScript 面试题 前段时间,在 [GitHub](https://github.com/LeetCode-OpenSource/hire/blob/master/typescript_zh.md) 上,发现一道来自 LeetCode TypeScript 的面试题,比较有意思,题目的大致意思是: 假设有一个这样的类型(原题中给出的是类,这里简化为 interface): ```ts interface Module { count: number; message: string; asyncMethod(input: Promise): Promise>; syncMethod(action: Action): Action; } ``` 在经过 `Connect` 函数之后,返回值类型为 ```ts type Result = { asyncMethod(input: T): Action; syncMethod(action: T): Action; } ``` 其中 `Action` 的定义为: ```ts interface Action { payload?: T; type: string; } ``` 这里主要考察两点 - 挑选出函数 - 此篇文章所提及的 `infer` 挑选函数的方法,已经在 [handbook](http://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html) 中已经给出,只需判断 value 能赋值给 Function 就行了: ```ts type FuncName = { [P in keyof T]: T[P] extends Function ? P : never }[keyof T]; type Connect = (module: Module) => { [T in FuncName]: Module[T] }; /* * type Connect = (module: Module) => { * asyncMethod: (input: Promise) => Promise>; * syncMethod: (action: Action) => Action; * } */ ``` 接下来就比较简单了,主要是利用条件类型 + `infer`,如果函数可以赋值给 `asyncMethod(input: Promise): Promise>`,则取值为 `asyncMethod(input: T): Action`。具体答案就不给出了,感兴趣的小伙伴可以尝试一下。 ================================================ FILE: docs/tips/lazyObjectLiteralInitialization.md ================================================ # 对象字面量的惰性初始化 在 JavaScript 中,像这样用字面量初始化对象的写法十分常见: ```ts let foo = {}; foo.bar = 123; foo.bas = 'Hello World'; ``` 但在 TypeScript 中,同样的写法就会报错: ```ts let foo = {}; foo.bar = 123; // Error: Property 'bar' does not exist on type '{}' foo.bas = 'Hello World'; // Error: Property 'bas' does not exist on type '{}' ``` 这是因为 TypeScript 在解析 `let foo = {}` 这段赋值语句时,会进行“类型推断”:它会认为等号左边 `foo` 的类型即为等号右边 `{}` 的类型。由于 `{}` 本没有任何属性,因此,像上面那样给 `foo` 添加属性时就会报错。 ## 最好的解决方案 最*好*的解决方案就是在为变量赋值的同时,添加属性及其对应的值: ```ts let foo = { bar: 123, bas: 'Hello World' }; ``` 这种写法也比较容易通过其他人或工具的代码审核,对后期维护也是有利的。 > 以下的快速解决方案采用*惰性*的思路,本质上是*在初始化变量时忘了添加属性*的做法。 ## 快速解决方案 如果你的 JavaScript 项目很大,那么在迁移到 TypeScript 的时候,上面的做法可能会比较麻烦。此时,你可以利用 TypeScript 的“类型断言”机制让代码顺利通过编译: ```ts let foo = {} as any; foo.bar = 123; foo.bas = 'Hello World'; ``` ## 折中的解决方案 当然,总是用 `any` 肯定是不好的,因为这样做其实是在想办法绕开 TypeScript 的类型检查。那么,折中的方案就是创建 `interface`,这样的好处在于: - 方便撰写类型文档 - TypeScript 会参与类型检查,确保类型安全 请看以下的示例: ```ts interface Foo { bar: number; bas: string; } let foo = {} as Foo; foo.bar = 123; foo.bas = 'Hello World'; ``` 使用 `interface` 可以确保类型安全,比如这种情况: ```ts interface Foo { bar: number; bas: string; } let foo = {} as Foo; foo.bar = 123; foo.bas = 'Hello World'; // 然后我们尝试这样做: foo.bar = 'Hello Stranger'; // 错误:你可能把 `bas` 写成了 `bar`,不能为数字类型的属性赋值字符串 ``` ================================================ FILE: docs/tips/limitPropertySetters.md ================================================ # 减少 setter 属性的使用 倾向于使用更精确的 `set/get` 函数(如 `setBar`, `getBar`),减少使用 `setter/getter`; 考虑以下代码: ```ts foo.bar = { a: 123, b: 456 }; ``` 存在 `setter/getter` 时: ```ts class Foo { a: number; b: number; set bar(value: { a: number; b: number }) { this.a = value.a; this.b = value.b; } } let foo = new Foo(); ``` 这并不是 `setter` 的一个好的使用场景,当开发人员阅读第一段代码时,不知道将要更改的所有内容的上下文。然而,当开发者使用 `foo.setBar(value)`,他可能会意识到在 `foo` 里可能会引起一些改变。 ================================================ FILE: docs/tips/metadata.md ================================================ # Reflect Metadata ## 基础 Reflect Metadata 是 ES7 的一个提案,它主要用来在声明的时候添加和读取元数据。TypeScript 在 1.5+ 的版本已经支持它,你只需要: - `npm i reflect-metadata --save`。 - 在 `tsconfig.json` 里配置 `emitDecoratorMetadata` 选项。 Reflect Metadata 的 API 可以用于类或者类的属性上,如: ```ts function metadata( metadataKey: any, metadataValue: any ): { (target: Function): void; (target: Object, propertyKey: string | symbol): void; }; ``` `Reflect.metadata` 当作 `Decorator` 使用,当修饰类时,在类上添加元数据,当修饰类属性时,在类原型的属性上添加元数据,如: ```ts @Reflect.metadata('inClass', 'A') class Test { @Reflect.metadata('inMethod', 'B') public hello(): string { return 'hello world'; } } console.log(Reflect.getMetadata('inClass', Test)); // 'A' console.log(Reflect.getMetadata('inMethod', new Test(), 'hello')); // 'B' ``` 它具有诸多使用场景。 ## 获取类型信息 譬如在 [`vue-property-decorator`](https://github.com/kaorun343/vue-property-decorator) 6.1 及其以下版本中,通过使用 `Reflect.getMetadata` API,`Prop` Decorator 能获取属性类型传至 Vue,简要代码如下: ```ts function Prop(): PropertyDecorator { return (target, key: string) => { const type = Reflect.getMetadata('design:type', target, key); console.log(`${key} type: ${type.name}`); // other... }; } class SomeClass { @Prop() public Aprop!: string; } ``` 运行代码可在控制台看到 `Aprop type: string`。除能获取属性类型外,通过 `Reflect.getMetadata("design:paramtypes", target, key)` 和 `Reflect.getMetadata("design:returntype", target, key)` 可以分别获取函数参数类型和返回值类型。 ## 自定义 `metadataKey` 除能获取类型信息外,常用于自定义 `metadataKey`,并在合适的时机获取它的值,示例如下: ```ts function classDecorator(): ClassDecorator { return target => { // 在类上定义元数据,key 为 `classMetaData`,value 为 `a` Reflect.defineMetadata('classMetaData', 'a', target); }; } function methodDecorator(): MethodDecorator { return (target, key, descriptor) => { // 在类的原型属性 'someMethod' 上定义元数据,key 为 `methodMetaData`,value 为 `b` Reflect.defineMetadata('methodMetaData', 'b', target, key); }; } @classDecorator() class SomeClass { @methodDecorator() someMethod() {} } Reflect.getMetadata('classMetaData', SomeClass); // 'a' Reflect.getMetadata('methodMetaData', new SomeClass(), 'someMethod'); // 'b' ``` ## 例子 ### 控制反转和依赖注入 在 Angular 2+ 的版本中,[控制反转与依赖注入](https://segmentfault.com/a/1190000008626680)便是基于此实现,现在,我们来实现一个简单版: ```ts type Constructor = new (...args: any[]) => T; const Injectable = (): ClassDecorator => target => {}; class OtherService { a = 1; } @Injectable() class TestService { constructor(public readonly otherService: OtherService) {} testMethod() { console.log(this.otherService.a); } } const Factory = (target: Constructor): T => { // 获取所有注入的服务 const providers = Reflect.getMetadata('design:paramtypes', target); // [OtherService] const args = providers.map((provider: Constructor) => new provider()); return new target(...args); }; Factory(TestService).testMethod(); // 1 ``` ### Controller 与 Get 的实现 如果你在使用 TypeScript 开发 Node 应用,相信你对 `Controller`、`Get`、`POST` 这些 Decorator,并不陌生: ```ts @Controller('/test') class SomeClass { @Get('/a') someGetMethod() { return 'hello world'; } @Post('/b') somePostMethod() {} } ``` 这些 Decorator 也是基于 `Reflect Metadata` 实现,这次,我们将 `metadataKey` 定义在 `descriptor` 的 `value` 上: ```ts const METHOD_METADATA = 'method'; const PATH_METADATA = 'path'; const Controller = (path: string): ClassDecorator => { return target => { Reflect.defineMetadata(PATH_METADATA, path, target); } } const createMappingDecorator = (method: string) => (path: string): MethodDecorator => { return (target, key, descriptor) => { Reflect.defineMetadata(PATH_METADATA, path, descriptor.value); Reflect.defineMetadata(METHOD_METADATA, method, descriptor.value); } } const Get = createMappingDecorator('GET'); const Post = createMappingDecorator('POST'); ``` 接着,创建一个函数,映射出 `route`: ```ts function mapRoute(instance: Object) { const prototype = Object.getPrototypeOf(instance); // 筛选出类的 methodName const methodsNames = Object.getOwnPropertyNames(prototype) .filter(item => !isConstructor(item) && isFunction(prototype[item])); return methodsNames.map(methodName => { const fn = prototype[methodName]; // 取出定义的 metadata const route = Reflect.getMetadata(PATH_METADATA, fn); const method = Reflect.getMetadata(METHOD_METADATA, fn); return { route, method, fn, methodName } }) }; ``` 因此,我们可以得到一些有用的信息: ```ts Reflect.getMetadata(PATH_METADATA, SomeClass); // '/test' mapRoute(new SomeClass()); /** * [{ * route: '/a', * method: 'GET', * fn: someGetMethod() { ... }, * methodName: 'someGetMethod' * },{ * route: '/b', * method: 'POST', * fn: somePostMethod() { ... }, * methodName: 'somePostMethod' * }] * */ ``` 最后,只需把 `route` 相关信息绑在 `express` 或者 `koa` 上就 ok 了。 ================================================ FILE: docs/tips/nominalTyping.md ================================================ # 名义化类型 TypeScript 的类型系统是结构化的,[这也是其主要的优点之一](https://basarat.gitbooks.io/typescript/content/docs/why-typescript.html)。然而,在实际的特定用例中,有时尽管变量具有相同的结构,你也想将他们视为不同类型。一个非常常见的用例是身份类型结构(它们可能只是在 C# 或者 Java 中表示一个它们语义化名字的字符串)。 这有一些社区使用的方式,我按照个人喜好降序排列: ## 使用字面量类型 这种模式使用泛型和字面量类型: ```ts // 泛型 Id 类型 type Id = { type: T; value: string; }; // 特殊的 Id 类型 type FooId = Id<'foo'>; type BarId = Id<'bar'>; // 可选:构造函数 const createFoo = (value: string): FooId => ({ type: 'foo', value }); const createBar = (value: string): BarId => ({ type: 'bar', value }); let foo = createFoo('sample'); let bar = createBar('sample'); foo = bar; // Error foo = foo; // Okey ``` - 优点 - 不需要类型断言。 - 缺点 - 如上结构 `{type,value}` 可能不那么尽如人意,而且需要服务器序列化支持。 ## 使用枚举 TypeScript 中[枚举](../typings/enums.md) 提供一定程度的名义化类型。如果两个枚举的命名不相同,则它们类型不相等。我们可以利用这个事实来为结构上兼容的类型,提供名义化类型。 解决办法包括: - 创建一个只有名字的枚举; - 利用这个枚举与实际结构体创建一个交叉类型(`&`)。 如下所示,当实际结构体仅仅是一个字符串时: ```ts // FOO enum FooIdBrand { _ = '' } type FooId = FooIdBrand & string; // BAR enum BarIdBrand { _ = '' } type BarId = BarIdBrand & string; // user let fooId: FooId; let barId: BarId; // 类型安全 fooId = barId; // error barId = fooId; // error // 创建一个新的 fooId = 'foo' as FooId; barId = 'bar' as BarId; // 两种类型都与基础兼容 let str: string; str = fooId; str = barId; ``` 请注意上文中的 `FooIdBrand` 与 `BarIdBrand`,它们都有一个 `_` 映射到空字符串的成员,即 `{ _ = '' }`。这可以强制 TypeScript 推断出这是一个基于字符串的枚举,而不是一个数字类型的枚举。这是很重要的,因为 TypeScript 会把一个空的枚举类型(`{}`)推断为一个数字类型的枚举,在 TypeScript 3.6.2 版本及其以上时,数字类型的枚举与 `string` 的交叉类型是 `never`。 ## 使用接口 因为 `number` 类型与 `enum` 类型在类型上是兼容的,因此我们不能使用上述提到的方法来处理它们。取而代之,我们可以使用接口打破这种类型的兼容性。TypeScript 编译团队仍然在使用这种方法,因此它值得一提。使用 `_` 前缀和 `Brand` 后缀是一种我强烈推荐的惯例方法([TypeScript 也这么推荐](https://github.com/Microsoft/TypeScript/blob/7b48a182c05ea4dea81bab73ecbbe9e013a79e99/src/compiler/types.ts#L693-L698))。 解决办法包括: - 在类型上添加一个不用的属性,用来打破类型兼容性; - 在新建或向下转换类型的时候使用断言。 如下所示: ```ts // FOO interface FooId extends String { _fooIdBrand: string; // 防止类型错误 } // BAR interface BarId extends String { _barIdBrand: string; // 防止类型错误 } // 使用 let fooId: FooId; let barId: BarId; // 类型安全 fooId = barId; // error barId = fooId; // error fooId = barId; // error barId = fooId; // error // 创建新的 fooId = 'foo' as any; barId = 'bar' as any; // 如果你需要以字符串作为基础 var str: string; str = fooId as any; str = barId as any; ``` ================================================ FILE: docs/tips/outFileCaution.md ================================================ # 谨慎使用 `--outFile` 由于以下几点原因,你应该谨慎使用 `--outFile` 选项: - 运行时的错误; - 快速编译; - 全局作用域; - 难以分析; - 难以扩展; - `_references`; - 代码重用; - 多目标; - 单独编译; ## 运行时的错误 如果你的代码依赖于上文中的 JavaScript,你可能会在运行时得到错误: - 类的继承在运行时中断。 有如下 `foo.ts`: ```ts class Foo {} ``` 以及 `bar.ts`: ```ts class Bar extends Foo {} ``` 如果你没有按正确的顺序编译它(例如:`tsc bar.ts foo.ts`),虽然它能够被编译成功,但是会在运行时抛出 `ReferenceError` 的错误。 - 模块拆分在运行时会失败。 `foo.ts`: ```ts namespace App { export const foo = 123; } ``` `bar.ts`: ```ts namespace App { export const bar = foo + 456; } ``` 与上文中一致,当你没有用正确的顺序编译它,它会在运行时将 `NaN` 赋值给 `bar`; ## 快速编译 如果你使用 `--out` 编译选项,而没有使用一些 `hacks` 时,单独的 `.ts` 文件是不会被编译成单独的 `.js` 文件。 `--out` 选项实际上使用了较慢的构建方式。 此外,由于 source map 基于长度编码,且对位置信息敏感,因此,大部分 source map 都会在编译时重新构建(如果你使用 source map)。 ## 全局作用域 当然,你可以使用命名空间,但是它仍然在 `window` 上(如果你在浏览器中打开),命名空间仅仅是一个临时的解决方式。`/// { this.count++; console.log(`Called : ${this.count}`); }; }(); called(); // Called : 1 called(); // Called : 2 ``` ================================================ FILE: docs/tips/staticConstructors.md ================================================ # TypeScript 中的静态构造函数 TypeScript 中的 `class` (JavaScript 中的 `class`)没有静态构造函数的功能,但是你可以通过调用它自己来获取相同的效果: ```ts class MyClass { static initalize() { // } } MyClass.initalize(); ``` ================================================ FILE: docs/tips/stringBasedEmuns.md ================================================ # 基于字符串的枚举 有时你需要在公共的键下收集一些字符串的集合。在 TypeScript 2.4 以前,它仅支持基于数字类型的枚举,如果你在使用 TypeScript 2.4 以上的版本,你通过可以使用[字符串字面量类型与联合类型组合使用创建基于字符串枚举类型的方式](../typings/literals.md#使用用例)。 ================================================ FILE: docs/tips/truthy.md ================================================ # Truthy JavaScript 有一个 `truthy` 概念,即在某些场景下会被推断为 `true`,例如除 `0` 以外的任何数字: ```ts if (123) { // 将会被推断出 `true` console.log('Any number other than 0 is truthy'); } ``` 你可以用下表来做参考: | **Variable Type** | **When it is falsy** | **When it is truthy** | | ------------------------------------------------ | -------------------- | --------------------- | | boolean | false | true | | string | ' ' (empty string) | any other string | | number | 0 NaN | any other number | | null | always | never | | Any other Object including empty ones like {},[] | never | always | ## 明确的 通过操作符 `!!`,你可以很容易的将某些值转化为布尔类型的值,例如:`!!foo`,它使用了两次 `!`,第一个 `!` 用来将其(在这里是 `foo`)转换为布尔值,但是这一操作取得的是其取反后的值,第二个取反时,能得到真正的布尔值。 这在很多地方都可以看到: ```ts // Direct variables const hasName = !!name; // As members of objects const someObj = { hasName: !!name }; // ReactJS { !!someName &&
{someName}
; } ``` ================================================ FILE: docs/tips/typeInstantiation.md ================================================ # 泛型的实例化类型 假如你有一个具有泛型参数的类型,如一个类 `Foo`: ```ts class Foo { foo: T; } ``` 你想为一个特定的类型创建单独的版本,可以通过将它拷贝到一个新变量里,并且用具体类型代替泛型的类型注解的方式来实现。例如,如果你想有一个类:`Foo`: ```ts class Foo { foo: T; } const FooNumber = Foo as { new (): Foo }; // ref 1 ``` 在 `ref 1` 中,你说 `FooNumber` 与 `Foo` 相同,但是,只是将其看作使用 `new` 运算符调用时的一个 `Foo` 实例。 ## 继承 类型断言模式是不安全的,因为编译器相信你在做正确的事情。在其他语言中用于类的常见模式是使用继承: ```ts class FooNumber extends Foo {} ``` ::: warning 这里需要注意的一点,如果你在基类上使用修饰器,继承类可能没有与基类相同的行为(它不再被修饰器包裹)。 ::: 当然,如果你不需要一个单独的类,你仍然写出一个有效的强制/断言模式,因此在开始时,我们便展示出了普通的断言模式: ```ts function id(x: T) { return x; } const idNum = id as { (x: number): number }; ``` > 灵感来源于:[stackoverflow question](https://stackoverflow.com/questions/34859911/instantiated-polymorphic-function-as-argument-in-typescript/34864705#34864705) ================================================ FILE: docs/tips/typesafeEventEmitter.md ================================================ # 类型安全的 Event Emitter 通常来说,在 Node.js 与传统的 JavaScript 里,你有一个单一的 Event Emitter,你可以用它来为不同的事件添加监听器。 ```ts const emitter = new EventEmitter(); // Emit emitter.emit('foo', foo); emitter.emit('bar', bar); // Listen emitter.on('foo', foo => console.log(foo)); emitter.on('bar', bar => console.log(bar)); ``` 实际上,在 `EventEmitter` 内部以映射数组的形式存储数据: ```ts { foo: [fooListeners], bar: [barListeners] } ``` 为了事件的类型安全,你可以为每个事件类型创建一个 emitter: ```ts const onFoo = new TypedEvent(); const onBar = new TypedEvent(); // Emit: onFoo.emit(foo); onBar.emit(bar); // Listen: onFoo.on(foo => console.log(foo)); onBar.on(bar => console.log(bar)); ``` 它一些优点: - 事件的类型,能以变量的形式被发现。 - Event Emitter 非常容易被重构。 - 事件数据结构是类型安全的。 ## 参考 TypedEvent ```ts export interface Listener { (event: T): any; } export interface Disposable { dispose(): any; } export class TypedEvent { private listeners: Listener[] = []; private listenersOncer: Listener[] = []; public on = (listener: Listener): Disposable => { this.listeners.push(listener); return { dispose: () => this.off(listener) }; }; public once = (listener: Listener): void => { this.listenersOncer.push(listener); }; public off = (listener: Listener) => { const callbackIndex = this.listeners.indexOf(listener); if (callbackIndex > -1) this.listeners.splice(callbackIndex, 1); }; public emit = (event: T) => { this.listeners.forEach(listener => listener(event)); this.listenersOncer.forEach(listener => listener(event)); this.listenersOncer = []; }; public pipe = (te: TypedEvent): Disposable => { return this.on(e => te.emit(e)); }; } ``` ================================================ FILE: docs/typings/ambient.md ================================================ # 环境声明 正如我们在为什么使用 [TypeScript](https://basarat.gitbooks.io/typescript/content/docs/why-typescript.html) 中所说: > TypeScript 的设计目标之一是让你在 TypeScript 中安全、轻松地使用现有的 JavaScript 库,TypeScript 通过声明文件来做到这一点 环境声明允许你安全地使用现有的 JavaScript 库,并且能让你的 JavaScript、CoffeeScript 或者其他需要编译成 JavaScript 的语言逐步迁移至 TypeScript。 学习为第三方 JavaScript 库编写环境声明,是一种为 TypeScript 写注解比较好的实践方式。 ## 声明文件 你可以通过 `declare` 关键字来告诉 TypeScript,你正在试图表述一个其他地方已经存在的代码,如:写在 JavaScript、CoffeeScript 或者是像浏览器和 Node.js 运行环境里的代码: ```ts foo = 123; // Error: 'foo' is not defined ``` 和: ```ts declare var foo: any; foo = 123; // allow ``` 你可以选择把这些声明放入 `.ts` 或者 `.d.ts` 里。在你实际的项目里,我们强烈建议你应该把声明放入独立的 `.d.ts` 里(可以从一个命名为 `global.d.ts` 或者 `vendor.d.ts` 文件开始)。 如果一个文件有扩展名 `.d.ts`,这意味着每个根级别的声明都必须以 `declare` 关键字作为前缀。这有利于让开发者清楚的知道,在这里 TypeScript 将不会把它编译成任何代码,同时开发者需要确保这些在编译时存在。 ::: tip - 环境声明就好像你与编译器之间的一个约定,如果在编译时它们不存在,但是你却使用了它们,程序将会在没有警告的情况下中断。 - 环境声明就好像是一个文档。如果源文件更新了,你应该同步更新。所以,当你在运行时有新的行为时,如果没有去更新环境声明,编译器将会报错。 ::: ## 变量 当你想告诉 TypeScript 编辑器关于 `process` 变量时,你可以这么做: ```ts declare let process: any; ``` ::: tip 你并不需要为 `process` 做这些,因为这已经存在于社区维护的 [`node.d.ts`](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/index.d.ts) ::: 这允许你使用 `process`,并能成功通过 TypeScript 的编译: ```ts process.exit(); ``` 我们推荐尽可能的使用接口,例如: ```ts interface Process { exit(code?: number): void; } declare let process: Process; ``` 因为这允许其他人扩充这些全局变量,并且会告诉 TypeScript 有关于这些声明的修改。例如:考虑到以下情况,我们添加一个 `exitWithLogging` 函数至 `process`: ```ts interface Process { exitWithLogging(code?: number): void; } process.exitWithLogging = function() { console.log('exiting'); process.exit.apply(process, arguments); }; ``` 接下来,让我们更详细的了解接口。 ================================================ FILE: docs/typings/callable.md ================================================ # 可调用的 你可以使用类型别名或者接口来表示一个可被调用的类型注解: ```ts interface ReturnString { (): string; } ``` 它可以表示一个返回值为 `string` 的函数: ```ts declare const foo: ReturnString; const bar = foo(); // bar 被推断为一个字符串。 ``` ## 一个实际的例子 当然,像这样一个可被调用的类型注解,你也可以根据实际来传递任何参数、可选参数以及 rest 参数,这有一个稍微复杂的例子: ```ts interface Complex { (foo: string, bar?: number, ...others: boolean[]): number; } ``` 一个接口可提供多种调用签名,用以特殊的函数重载: ```ts interface Overloaded { (foo: string): string; (foo: number): number; } // 实现接口的一个例子: function stringOrNumber(foo: number): number; function stringOrNumber(foo: string): string; function stringOrNumber(foo: any): any { if (typeof foo === 'number') { return foo * foo; } else if (typeof foo === 'string') { return `hello ${foo}`; } } const overloaded: Overloaded = stringOrNumber; // 使用 const str = overloaded(''); // str 被推断为 'string' const num = overloaded(123); // num 被推断为 'number' ``` 这也可以用于内联注解中: ```ts let overloaded: { (foo: string): string; (foo: number): number; }; ``` ## 箭头函数 为了使指定可调用的类型签名更容易,TypeScript 也允许你使用简单的箭头函数类型注解。例如,在一个以 number 类型为参数,以 string 类型为返回值的函数中,你可以这么写: ```ts const simple: (foo: number) => string = foo => foo.toString(); ``` ::: tip 它仅仅只能作为简单的箭头函数,你无法使用重载。如果想使用重载,你必须使用完整的 `{ (someArgs): someReturn }` 的语法 ::: ## 可实例化 可实例化仅仅是可调用的一种特殊情况,它使用 `new` 作为前缀。它意味着你需要使用 `new` 关键字去调用它: ```ts interface CallMeWithNewToGetString { new (): string; } // 使用 declare const Foo: CallMeWithNewToGetString; const bar = new Foo(); // bar 被推断为 string 类型 ``` ================================================ FILE: docs/typings/discrominatedUnion.md ================================================ # 辨析联合类型 当类中含有[字面量成员](./literals.md)时,我们可以用该类的属性来辨析联合类型。 作为一个例子,考虑 `Square` 和 `Rectangle` 的联合类型 `Shape`。`Square` 和 `Rectangle`有共同成员 `kind`,因此 `kind` 存在于 `Shape` 中。 ```ts interface Square { kind: 'square'; size: number; } interface Rectangle { kind: 'rectangle'; width: number; height: number; } type Shape = Square | Rectangle; ``` 如果你使用类型保护风格的检查(`==`、`===`、`!=`、`!==`)或者使用具有判断性的属性(在这里是 `kind`),TypeScript 将会认为你会使用的对象类型一定是拥有特殊字面量的,并且它会为你自动把类型范围变小: ```ts function area(s: Shape) { if (s.kind === 'square') { // 现在 TypeScript 知道 s 的类型是 Square // 所以你现在能安全使用它 return s.size * s.size; } else { // 不是一个 square ?因此 TypeScript 将会推算出 s 一定是 Rectangle return s.width * s.height; } } ``` ## 详细的检查 通常,联合类型的成员有一些自己的行为(代码): ```ts interface Square { kind: 'square'; size: number; } interface Rectangle { kind: 'rectangle'; width: number; height: number; } // 有人仅仅是添加了 `Circle` 类型 // 我们可能希望 TypeScript 能在任何被需要的地方抛出错误 interface Circle { kind: 'circle'; radius: number; } type Shape = Square | Rectangle | Circle; ``` 一个可能会让你的代码变差的例子: ```ts function area(s: Shape) { if (s.kind === 'square') { return s.size * s.size; } else if (s.kind === 'rectangle') { return s.width * s.height; } // 如果你能让 TypeScript 给你一个错误,这是不是很棒? } ``` 你可以通过一个简单的向下思想,来确保块中的类型被推断为与 `never` 类型兼容的类型。例如,你可以添加一个更详细的检查来捕获错误: ```ts function area(s: Shape) { if (s.kind === 'square') { return s.size * s.size; } else if (s.kind === 'rectangle') { return s.width * s.height; } else { // Error: 'Circle' 不能被赋值给 'never' const _exhaustiveCheck: never = s; } } ``` 它将强制你添加一种新的条件: ```ts function area(s: Shape) { if (s.kind === 'square') { return s.size * s.size; } else if (s.kind === 'rectangle') { return s.width * s.height; } else if (s.kind === 'circle') { return Math.PI * s.radius ** 2; } else { // ok const _exhaustiveCheck: never = s; } } ``` ## Switch ::: tip 你可以通过 `switch` 来实现以上例子。 ::: ```ts function area(s: Shape) { switch (s.kind) { case 'square': return s.size * s.size; case 'rectangle': return s.width * s.height; case 'circle': return Math.PI * s.radius ** 2; default: const _exhaustiveCheck: never = s; } } ``` ## strictNullChecks 如果你使用 `strictNullChecks` 选项来做详细的检查,你应该返回 `_exhaustiveCheck` 变量(类型是 `never`),否则 TypeScript 可能会推断返回值为 `undefined`: ```ts function area(s: Shape) { switch (s.kind) { case 'square': return s.size * s.size; case 'rectangle': return s.width * s.height; case 'circle': return Math.PI * s.radius ** 2; default: const _exhaustiveCheck: never = s; return _exhaustiveCheck; } } ``` ## Redux Redux 库正是使用的上述例子。 以下是添加了 TypeScript 类型注解的[redux 要点](https://github.com/reduxjs/redux#the-gist)。 ```ts import { createStore } from 'redux'; type Action = | { type: 'INCREMENT'; } | { type: 'DECREMENT'; }; /** * This is a reducer, a pure function with (state, action) => state signature. * It describes how an action transforms the state into the next state. * * The shape of the state is up to you: it can be a primitive, an array, an object, * or even an Immutable.js data structure. The only important part is that you should * not mutate the state object, but return a new object if the state changes. * * In this example, we use a `switch` statement and strings, but you can use a helper that * follows a different convention (such as function maps) if it makes sense for your * project. */ function counter(state = 0, action: Action) { switch (action.type) { case 'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } } // Create a Redux store holding the state of your app. // Its API is { subscribe, dispatch, getState }. let store = createStore(counter); // You can use subscribe() to update the UI in response to state changes. // Normally you'd use a view binding library (e.g. React Redux) rather than subscribe() directly. // However it can also be handy to persist the current state in the localStorage. store.subscribe(() => console.log(store.getState())); // The only way to mutate the internal state is to dispatch an action. // The actions can be serialized, logged or stored and later replayed. store.dispatch({ type: 'INCREMENT' }); // 1 store.dispatch({ type: 'INCREMENT' }); // 2 store.dispatch({ type: 'DECREMENT' }); // 1 ``` 与 TypeScript 一起使用可以有效的防止拼写错误,并且能提高重构和书写文档化代码的能力。 ================================================ FILE: docs/typings/enums.md ================================================ # 枚举 枚举是组织收集有关联变量的一种方式,许多程序语言(如:c/c#/Java)都有枚举数据类型。下面是定义一个 TypeScript 枚举类型的方式: ```ts enum CardSuit { Clubs, Diamonds, Hearts, Spades } // 简单的使用枚举类型 let Card = CardSuit.Clubs; // 类型安全 Card = 'not a member of card suit'; // Error: string 不能赋值给 `CardSuit` 类型 ``` 这些枚举类型的值都是数字类型,因此它们被称为数字类型枚举。 ## 数字类型枚举与数字类型 数字类型枚举,允许我们将数字类型或者其他任何与数字类型兼容的类型赋值给枚举类型的实例。 ```ts enum Color { Red, Green, Blue } let col = Color.Red; col = 0; // 有效的,这也是 Color.Red ``` ## 数字类型枚举与字符串类型 在我们继续深入学习枚举类型之前,先来看看它编译的 JavaScript 吧,以下是一个简单的 TypeScript 枚举类型: ```ts enum Tristate { False, True, Unknown } ``` 其被编译成 JavaScript 后如下所示: ```ts{3} var Tristate; (function(Tristate) { Tristate[(Tristate['False'] = 0)] = 'False'; Tristate[(Tristate['True'] = 1)] = 'True'; Tristate[(Tristate['Unknown'] = 2)] = 'Unknown'; })(Tristate || (Tristate = {})); ``` 先让我们聚焦 `Tristate[Tristate['False'] = 0] = 'False'` 这行代码,其中 `Tristate['False'] = 0` 的意思是将 `Tristate` 对象里的 `False` 成员值设置为 `0`。注意,JavaScript 赋值运算符返回的值是被赋予的值(在此例子中是 `0`),因此下一次 JavaScript 运行时执行的代码是 `Tristate[0] = 'False'`。意味着你可以使用 `Tristate` 变量来把字符串枚举类型改造成一个数字或者是数字类型的枚举类型,如下所示: ```ts enum Tristate { False, True, Unknown } console.log(Tristate[0]); // 'False' console.log(Tristate['False']); // 0 console.log(Tristate[Tristate.False]); // 'False' because `Tristate.False == 0` ``` ## 改变与数字枚举关联的数字 默认情况下,第一个枚举值是 `0`,然后每个后续值依次递增 1: ```ts enum Color { Red, // 0 Green, // 1 Blue // 2 } ``` 但是,你可以通过特定的赋值来改变给任何枚举成员关联的数字,如下例子,我们从 3 开始依次递增: ```ts enum Color { DarkRed = 3, // 3 DarkGreen, // 4 DarkBlue // 5 } ``` ::: tip 我通常用 `= 1` 初始化,因为在枚举类型值里,它能让你做一个安全可靠的检查。 ::: ## 使用数字类型作为标志 枚举的一个很好用途是使用枚举作为标志。这些标志允许你检查一组条件中的某个条件是否为真。考虑如下代码例子,我们有一组关于 animals 的属性: ```ts enum AnimalFlags { None = 0, HasClaws = 1 << 0, CanFly = 1 << 1, EatsFish = 1 << 2, Endangered = 1 << 3 } ``` 在这里,我们使用了左移的位运算符,将数字 `1` 的二进制向左移动位置得到数字 `0001`、`0010`、`0100` 和 `1000`(换成十进制结果是:1, 2, 4, 8)。当你在使用这种标记的时候,这些位运算符 `|` (或)、`&` (和)、`~` (非)将会是你最好的朋友: ```ts enum AnimalFlags { None = 0, HasClaws = 1 << 0, CanFly = 1 << 1 } interface Animal { flags: AnimalFlags; [key: string]: any; } function printAnimalAbilities(animal: Animal) { var animalFlags = animal.flags; if (animalFlags & AnimalFlags.HasClaws) { console.log('animal has claws'); } if (animalFlags & AnimalFlags.CanFly) { console.log('animal can fly'); } if (animalFlags == AnimalFlags.None) { console.log('nothing'); } } var animal = { flags: AnimalFlags.None }; printAnimalAbilities(animal); // nothing animal.flags |= AnimalFlags.HasClaws; printAnimalAbilities(animal); // animal has claws animal.flags &= ~AnimalFlags.HasClaws; printAnimalAbilities(animal); // nothing animal.flags |= AnimalFlags.HasClaws | AnimalFlags.CanFly; printAnimalAbilities(animal); // animal has claws, animal can fly ``` 在这里: - 我们使用 `|=` 来添加一个标志; - 组合使用 `&=` 和 `~` 来清理一个标志; - `|` 来合并标志。 ::: tip 你可以组合标志,用来在枚举类型中定义方便快捷的方式,如下 `EndangeredFlyingClawedFishEating`: ::: ```ts enum AnimalFlags { None = 0, HasClaws = 1 << 0, CanFly = 1 << 1, EatsFish = 1 << 2, Endangered = 1 << 3, EndangeredFlyingClawedFishEating = HasClaws | CanFly | EatsFish | Endangered } ``` ## 字符串枚举 在上文中,我们只看到了数字类型的枚举,实际上,枚举类型的值,也可以是字符串类型。 ```ts export enum EvidenceTypeEnum { UNKNOWN = '', PASSPORT_VISA = 'passport_visa', PASSPORT = 'passport', SIGHTED_STUDENT_CARD = 'sighted_tertiary_edu_id', SIGHTED_KEYPASS_CARD = 'sighted_keypass_card', SIGHTED_PROOF_OF_AGE_CARD = 'sighted_proof_of_age_card' } ``` 这些可以更容易被处理和调试,因为它们提供有意义/可调试的字符串。 你可以使用它们用于简单的字符串比较: ```ts // Where `someStringFromBackend` will be '' | 'passport_visa' | 'passport' ... etc. const value = someStringFromBackend as EvidenceTypeEnum; // Sample use in code if (value === EvidenceTypeEnum.PASSPORT) { console.log('You provided a passport'); console.log(value); // `passport` } ``` ## 常量枚举 ```ts enum Tristate { False, True, Unknown } const lie = Tristate.False; ``` `const lie = Tristate.False` 会被编译成 JavaScript `let lie = Tristate.False` (是的,编译后与编译前,几乎相同)。这意味着在运行执行时,它将会查找变量 `Tristate` 和 `Tristate.False`。在此处获得性能提升的一个小技巧是使用常量枚举: ```ts const enum Tristate { False, True, Unknown } const lie = Tristate.False; ``` 将会被编译成: ```js let lie = 0; ``` 编译器将会: - 内联枚举的任何用法(`0` 而不是 `Tristate.False`); - 不会为枚举类型编译成任何 JavaScript(在这个例子中,运行时没有 `Tristate` 变量),因为它使用内联语法。 ### 常量枚举 `preserveConstEnums` 选项 使用内联语法对性能有明显的提升作用。运行时没有 `Tristate` 变量的事实,是因为编译器帮助你把一些在运行时没有用到的不编译成 JavaScript。然而,你可能想让编译器仍然把枚举类型编译成 JavaScript,用于如上例子中从字符串到数字,或者是从数字到字符串的查找。在这种情景下,你可以使用编译选项 `--preserveConstEnums`,它会编译出 `var Tristate` 的定义,因此你在运行时,手动使用 `Tristate['False']` 和 `Tristate[0]`。并且这不会以任何方式影响内联。 ## 有静态方法的枚举 你可以使用 `enum` + `namespace` 的声明的方式向枚举类型添加静态方法。如下例所示,我们将静态成员 `isBusinessDay` 添加到枚举上: ```ts enum Weekday { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday } namespace Weekday { export function isBusinessDay(day: Weekday) { switch (day) { case Weekday.Saturday: case Weekday.Sunday: return false; default: return true; } } } const mon = Weekday.Monday; const sun = Weekday.Sunday; console.log(Weekday.isBusinessDay(mon)); // true console.log(Weekday.isBusinessDay(sun)); ``` ## 开放式枚举 ::: tip 你只有在不使用模块时,开放式的枚举才有意义,你应该使用模块,因此这部分在文章最后。 ::: 让我们再一次看看编译成 JavaScript 的枚举是什么样子: ```ts var Tristate; (function(Tristate) { Tristate[(Tristate['False'] = 0)] = 'False'; Tristate[(Tristate['True'] = 1)] = 'True'; Tristate[(Tristate['Unknown'] = 2)] = 'Unknown'; })(Tristate || (Tristate = {})); ``` 我们已经解释了 `Tristate[Tristate['False'] = 0] = 'False'` 部分,现在我们来看看包裹函数 `(function (Tristate) { /* code here */})(Tristate || (Tristate = {}))`,特别是 `(Tristate || (Tristate = {}))` 部分。这捕获了一个局部变量 `TriState`,它要么指向已经定义的`TriState` 值,要么使用一个新的空对象来初始化它。 这意味着你可以跨多个文件拆分(和扩展)枚举定义,如下所示,你可以把 `Color` 的定义拆分至两个块中: ```ts enum Color { Red, Green, Blue } enum Color { DarkRed = 3, DarkGreen, DarkBlue } ``` ::: tip 你应该在枚举的延续块中,重新初始化第一个成员(此处为 `DarkRed = 3`),使生成的代码不破坏先前定义的值(即0、1...等值)。如果您仍然不这样做,TypeScript 将会发出警告(错误信息:`In an enum with multiple declarations, only one declaration can omit an initializer for its first enum element.`)。 ::: ================================================ FILE: docs/typings/exceptionsHanding.md ================================================ # 异常处理 JavaScript 有一个 `Error` 类,用于处理异常。你可以通过 `throw` 关键字来抛出一个错误。然后通过 `try/catch` 块来捕获此错误: ```ts try { throw new Error('Something bad happened'); } catch (e) { console.log(e); } ``` ## 错误子类型 除内置的 `Error` 类外,还有一些额外的内置错误,它们继承自 `Error` 类: ### RangeError 当数字类型变量或者参数超出其有效范围时,出现 `RangeError` 的错误提示: ```ts // 使用过多参数调用 console console.log.apply(console, new Array(1000000000)); // RangeError: 数组长度无效 ``` ### ReferenceError 当引用无效时,会出现 `ReferenceError` 的错误提示: ```ts 'use strict'; console.log(notValidVar); // ReferenceError: notValidVar 未定义 ``` ### SyntaxError 当解析无效 JavaScript 代码时,会出现 `SyntaxError` 的错误提示: ```ts 1 *** 3 // SyntaxError: 无效的标记 * ``` ### TypeError 变量或者参数不是有效类型时,会出现 `TypeError` 的错误提示: ```ts '1.2'.toPrecision(1); // TypeError: '1.2'.toPrecision 不是函数。 ``` ### URIError 当传入无效参数至 `encodeURI()` 和 `decodeURI()` 时,会出现 `URIError` 的错误提示: ```ts decodeURI('%'); // URIError: URL 异常 ``` ## 使用 `Error` JavaScript 初学者可能有时候仅仅是抛出一个原始字符串: ```ts try { throw 'Something bad happened'; } catch (e) { console.log(e); } ``` **不要这么做**,使用 `Error` 对象的基本好处是,它能自动跟踪堆栈的属性构建以及生成位置。 原始字符串会导致极差的调试体验,并且在分析日志时,将会变得错综复杂。 ## 你并不需要 `throw` 抛出一个错误 传递一个 `Error` 对象是没问题的,这种方式在 `Node.js` 回调函数中非常常见,它用第一个参数作为错误对象进行回调处理。 ```ts function myFunction (callback: (e: Error)) { doSomethingAsync(function () { if (somethingWrong) { callback(new Error('This is my error')); } else { callback(); } }) } ``` ## 优秀的用例 「Exceptions should be exceptional」是计算机科学中常用用语。这里有一些原因说明在 JavaScript(TypeScript) 中也是如此。 ### 不清楚从哪里抛出错误 考虑如下代码块: ```ts try { const foo = runTask1(); const bar = runTask2(); } catch (e) { console.log('Error:', e); } ``` 下一个开发者可能并不清楚哪个函数可能会抛出错误。在没有阅读 `task1/task2` 代码以及他们可能会调用的函数时,对代码 `review` 的人员可能也不会知道错误会从哪里抛出。 ### 优雅的捕获错误 你可以通过为每个可能抛出错误的代码显式捕获,来使其优雅: ```ts try { const foo = runTask1(); } catch (e) { console.log('Error:', e); } try { const bar = runTask2(); } catch (e) { console.log('Error:', e); } ``` 但是现在,如果你想从第一个任务中传递变量到第二个任务中,代码会变的混乱(注意:foo 变量需要用 let 显式注解它,因为它不能从 `runTask1` 中返回出来): ```ts let foo: number; // Notice 使用 let 并且显式注明类型注解 try { foo = runTask1(); } catch (e) { console.log('Error:', e); } try { const bar = runTask2(foo); } catch (e) { console.log('Error:', e); } ``` ### 没有在类型系统中很好的表示 考虑如下函数: ```ts function validate(value: number) { if (value < 0 || value > 100) { throw new Error('Invalid value'); } } ``` 在这种情境下使用 `Error` 不是一个好的主意。因为没有用来验证函数的类型定义(如:`(value: number) => void`),取而代之一个更好的方式是创建一个验证方法: ```ts function validate( value: number ): { error?: string; } { if (value < 0 || value > 100) { return { error: 'Invalid value' }; } } ``` 现在它具有类型定义了。 ::: tip 除非你想用以非常通用(try/catch)的方式处理错误,否则不要抛出错误。 ::: ================================================ FILE: docs/typings/freshness.md ================================================ # Freshness 为了能让检查对象字面量类型更容易,TypeScript 提供 「[Freshness](https://github.com/Microsoft/TypeScript/pull/3823)」 的概念(它也被称为更严格的对象字面量检查)用来确保对象字面量在结构上类型兼容。 结构类型非常方便。考虑如下例子代码,它可以让你非常便利的从 JavaScript 迁移至 TypeScript,并且会提供类型安全: ```js function logName(something: { name: string }) { console.log(something.name); } const person = { name: 'matt', job: 'being awesome' }; const animal = { name: 'cow', diet: 'vegan, but has milk of own specie' }; const randow = { note: `I don't have a name property` }; logName(person); // ok logName(animal); // ok logName(randow); // Error: 没有 `name` 属性 ``` 但是,结构类型有一个缺点,它能误导你认为某些东西接收的数据比它实际的多。如下例,TypeScript 发出错误警告: ```ts function logName(something: { name: string }) { console.log(something.name); } logName({ name: 'matt' }); // ok logName({ name: 'matt', job: 'being awesome' }); // Error: 对象字面量只能指定已知属性,`job` 属性在这里并不存在。 ``` ::: warning 请注意,这种错误提示,只会发生在对象字面量上。 ::: 如果没有这种错误提示,我们可能会去寻找函数的调用 `logName({ name: 'matt', job: 'being awesome' })`,继而会认为 `logName` 可能会使用 `job` 属性做一些事情,然而实际上 `logName` 并没有使用它。 另外一个使用比较多的场景是与具有可选成员的接口一起使用,如果没有这样的对象字面量检查,当你输入错误单词的时候,并不会发出错误警告: ```ts function logIfHasName(something: { name?: string }) { if (something.name) { console.log(something.name); } } const person = { name: 'matt', job: 'being awesome' }; const animal = { name: 'cow', diet: 'vegan, but has milk of own species' }; logIfHasName(person); // okay logIfHasName(animal); // okay logIfHasName({ neme: 'I just misspelled name to neme' }); // Error: 对象字面量只能指定已知属性,`neme` 属性不存在。 ``` 之所以只对对象字面量进行类型检查,因为在这种情况下,那些实际上并没有被使用到的属性有可能会拼写错误或者会被误用。 ## 允许额外的属性 一个类型能够包含索引签名,以明确表明可以使用额外的属性: ```ts let x: { foo: number, [x: string]: any }; x = { foo: 1, baz: 2 }; // ok, 'baz' 属性匹配于索引签名 ``` ## 用例:React State Facebook ReactJS 为对象的 Freshness 提供了一个很好的用例,通常在组件中,你只使用少量属性,而不是传入所有,来调用 `setState`: ```ts // 假设 interface State { foo: string; bar: string; } // 你可能想做: this.setState({ foo: 'Hello' }); // Error: 没有属性 'bar' // 因为 state 包含 'foo' 与 'bar',TypeScript 会强制你这么做: this.setState({ foo: 'Hello', bar: this.state.bar }); ``` 如果你想使用 Freshness,你可能需要将所有成员标记为可选,这仍然会捕捉到拼写错误: ```ts // 假设 interface State { foo?: string; bar?: string; } // 你可能想做 this.setState({ foo: 'Hello' }); // Yay works fine! // 由于 Freshness,你也可以防止错别字 this.setState({ foos: 'Hello' }}; // Error: 对象只能指定已知属性 // 仍然会有类型检查 this.setState({ foo: 123 }); // Error: 无法将 number 类型赋值给 string 类型 ``` ================================================ FILE: docs/typings/functions.md ================================================ # 函数 函数类型在 TypeScript 类型系统中扮演着非常重要的角色,它们是可组合系统的核心构建块。 ## 参数注解 你可以注解函数参数,就像你可以注解其他变量一样: ```ts // variable annotation let sampleVariable: { bar: number }; // function parameter annotation function foo(sampleParameter: { bar: number }) {} ``` 这里我们使用了内联类型注解,除此之外,你还可以使用接口等其他方式。 ### 返回类型注解 你可以在函数参数列表之后使用与变量相同的样式来注解返回类型,如例子中 `:Foo`: ```ts interface Foo { foo: string; } // Return type annotated as `: Foo` function foo(sample: Foo): Foo { return sample; } ``` 我们在这里使用了一个 `interface`,但你可以自由地使用其他注解方式,例如内联注解。 通常,你不*需要*注解函数的返回类型,因为它可以由编译器推断: ```ts interface Foo { foo: string; } function foo(sample: Foo) { return sample; // inferred return type 'Foo' } ``` 但是,添加这些注解以帮助解决错误提示通常是一个好主意,例如: ```ts function foo() { return { fou: 'John Doe' }; // You might not find this misspelling of `foo` till it's too late } sendAsJSON(foo()); ``` 如果你不打算从函数返回任何内容,则可以将其标注为:`void` 。你通常可以删除 `void`, TypeScript 能推导出来: ### 可选参数 你可以将参数标记为可选: ```ts function foo(bar: number, bas?: string): void { // .. } foo(123); foo(123, 'hello'); ``` 或者,当调用者没有提供该参数时,你可以提供一个默认值(在参数声明后使用 `= someValue` ): ```ts function foo(bar: number, bas: string = 'hello') { console.log(bar, bas); } foo(123); // 123, hello foo(123, 'world'); // 123, world ``` ### 重载 TypeScript 允许你声明函数重载。这对于文档 + 类型安全来说很实用。请思考以下代码: ```ts function padding(a: number, b?: number, c?: number, d?: any) { if (b === undefined && c === undefined && d === undefined) { b = c = d = a; } else if (c === undefined && d === undefined) { c = a; d = b; } return { top: a, right: b, bottom: c, left: d }; } ``` 如果仔细查看代码,就会发现 a,b,c,d 的值会根据传入的参数数量而变化。此函数也只需要 1 个,2 个或 4 个参数。可以使用函数重载来*强制*和*记录*这些约束。你只需多次声明函数头。最后一个函数头是在函数体内实际处于活动状态但不可用于外部。 如下所示: ```ts // 重载 function padding(all: number); function padding(topAndBottom: number, leftAndRight: number); function padding(top: number, right: number, bottom: number, left: number); // Actual implementation that is a true representation of all the cases the function body needs to handle function padding(a: number, b?: number, c?: number, d?: number) { if (b === undefined && c === undefined && d === undefined) { b = c = d = a; } else if (c === undefined && d === undefined) { c = a; d = b; } return { top: a, right: b, bottom: c, left: d }; } ``` 这里前三个函数头可有效调用 `padding`: ```ts padding(1); // Okay: all padding(1, 1); // Okay: topAndBottom, leftAndRight padding(1, 1, 1, 1); // Okay: top, right, bottom, left padding(1, 1, 1); // Error: Not a part of the available overloads ``` 当然,最终声明(从函数内部看到的真正声明)与所有重载兼容是很重要的。这是因为这是函数体需要考虑的函数调用的真实性质。 ::: tip TypeScript 中的函数重载没有任何运行时开销。它只允许你记录希望调用函数的方式,并且编译器会检查其余代码。 ::: ### 函数声明 > 快速开始:类型注解是你描述现有实现类型的一种方式 在没有提供函数实现的情况下,有两种声明函数类型的方式: ```ts type LongHand = { (a: number): number; }; type ShortHand = (a: number) => number; ``` 上面代码中的两个例子完全相同。但是,当你想使用函数重载时,只能用第一种方式: ```ts type LongHandAllowsOverloadDeclarations = { (a: number): number; (a: string): string; }; ``` ================================================ FILE: docs/typings/generices.md ================================================ # 泛型 设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是: - 类的实例成员 - 类的方法 - 函数参数 - 函数返回值 ## 动机和示例 下面是对一个先进先出的数据结构——队列,在 `TypeScript` 和 `JavaScript` 中的简单实现。 ```ts class Queue { private data = []; push = item => this.data.push(item); pop = () => this.data.shift(); } ``` 在上述代码中存在一个问题,它允许你向队列中添加任何类型的数据,当然,当数据被弹出队列时,也可以是任意类型。在下面的示例中,看起来人们可以向队列中添加`string` 类型的数据,但是实际上,该用法假定的是只有 `number` 类型会被添加到队列里。 ```ts class Queue { private data = []; push = item => this.data.push(item); pop = () => this.data.shift(); } const queue = new Queue(); queue.push(0); queue.push('1'); // Oops,一个错误 // 一个使用者,走入了误区 console.log(queue.pop().toPrecision(1)); console.log(queue.pop().toPrecision(1)); // RUNTIME ERROR ``` 一个解决的办法(事实上,这也是不支持泛型类型的唯一解决办法)是为这些约束创建特殊类,如快速创建数字类型的队列: ```ts class QueueNumber { private data = []; push = (item: number) => this.data.push(item); pop = (): number => this.data.shift(); } const queue = new QueueNumber(); queue.push(0); queue.push('1'); // Error: 不能推入一个 `string` 类型,只能是 `number` 类型 // 如果该错误得到修复,其他将不会出现问题 ``` 当然,快速也意味着痛苦。例如当你想创建一个字符串的队列时,你将不得不再次修改相当大的代码。我们真正想要的一种方式是无论什么类型被推入队列,被推出的类型都与推入类型一样。当你使用泛型时,这会很容易: ```ts // 创建一个泛型类 class Queue { private data: T[] = []; push = (item: T) => this.data.push(item); pop = (): T | undefined => this.data.shift(); } // 简单的使用 const queue = new Queue(); queue.push(0); queue.push('1'); // Error:不能推入一个 `string`,只有 number 类型被允许 ``` 另外一个我们见过的例子:一个 `reverse` 函数,现在在这个函数里提供了函数参数与函数返回值的约束: ```ts function reverse(items: T[]): T[] { const toreturn = []; for (let i = items.length - 1; i >= 0; i--) { toreturn.push(items[i]); } return toreturn; } const sample = [1, 2, 3]; let reversed = reverse(sample); reversed[0] = '1'; // Error reversed = ['1', '2']; // Error reversed[0] = 1; // ok reversed = [1, 2]; // ok ``` 在此章节中,你已经了解在*类*和*函数*上使用泛型的例子。一个值得补充一点的是,你可以为创建的成员函数添加泛型: ```ts class Utility { reverse(items: T[]): T[] { const toreturn = []; for (let i = items.length; i >= 0; i--) { toreturn.push(items[i]); } return toreturn; } } ``` ::: tip 你可以随意调用泛型参数,当你使用简单的泛型时,泛型常用 `T`、`U`、`V` 表示。如果在你的参数里,不止拥有一个泛型,你应该使用一个更语义化名称,如 `TKey` 和 `TValue` (通常情况下,以 `T` 作为泛型的前缀,在其他语言如 C++ 里,也被称为模板) ::: ## 误用的泛型 我见过开发者使用泛型仅仅是为了它的 hack。当你使用它时,你应该问问自己:你想用它来提供什么样的约束。如果你不能很好的回答它,你可能会误用泛型,如: ```ts declare function foo(arg: T): void; ``` 在这里,泛型完全没有必要使用,因为它仅用于单个参数的位置,使用如下方式可能更好: ```ts declare function foo(arg: any): void; ``` ## 设计模式:方便通用 考虑如下函数: ```ts declare function parse(name: string): T; ``` 在这种情况下,泛型 `T` 只在一个地方被使用了,它并没有在成员之间提供约束 `T`。这相当于一个如下的类型断言: ```ts declare function parse(name: string): any; const something = parse('something') as TypeOfSomething; ``` 仅使用一次的泛型并不比一个类型断言来的安全。它们都给你使用 API 提供了便利。 另一个明显的例子是,一个用于加载 json 返回值函数,它返回你任何传入类型的 `Promise`: ```ts const getJSON = (config: { url: string; headers?: { [key: string]: string } }): Promise => { const fetchConfig = { method: 'GET', Accept: 'application/json', 'Content-Type': 'application/json', ...(config.headers || {}) }; return fetch(config.url, fetchConfig).then(response => response.json()); }; ``` 请注意,你仍然需要明显的注解任何你需要的类型,但是 `getJSON` 的签名 `config => Promise` 能够减少你一些关键的步骤(你不需要注解 `loadUsers` 的返回类型,因为它能够被推出来): ```ts type LoadUserResponse = { user: { name: string; email: string; }[]; }; function loaderUser() { return getJSON({ url: 'https://example.com/users' }); } ``` 与此类似:使用 `Promise` 作为一个函数的返回值比一些如:`Promise` 的备选方案要好很多。 ### 配合 axios 使用 通常情况下,我们会把后端返回数据格式单独放入一个 interface 里: ```ts // 请求接口数据 export interface ResponseData { /** * 状态码 * @type { number } */ code: number; /** * 数据 * @type { T } */ result: T; /** * 消息 * @type { string } */ message: string; } ``` 当我们把 API 单独抽离成单个模块时: ```ts // 在 axios.ts 文件中对 axios 进行了处理,例如添加通用配置、拦截器等 import Ax from './axios'; import { ResponseData } from './interface.ts'; export function getUser() { return Ax.get>('/somepath') .then(res => res.data) .catch(err => console.error(err)); } ``` 接着我们写入返回的数据类型 `User`,这可以让 TypeScript 顺利推断出我们想要的类型: ```ts interface User { name: string; age: number; } async function test() { // user 被推断出为 // { // code: number, // result: { name: string, age: number }, // message: string // } const user = await getUser(); } ``` ================================================ FILE: docs/typings/indexSignatures.md ================================================ # 索引签名 可以用字符串访问 JavaScript 中的对象(TypeScript 中也一样),用来保存对其他对象的引用。 例如: ```ts let foo: any = {}; foo['Hello'] = 'World'; console.log(foo['Hello']); // World ``` 我们在键 `Hello` 下保存了一个字符串 `World`,除字符串外,它也可以保存任意的 JavaScript 对象,例如一个类的实例。 ```ts class Foo { constructor(public message: string) {} log() { console.log(this.message); } } let foo: any = {}; foo['Hello'] = new Foo('World'); foo['Hello'].log(); // World ``` 当你传入一个其他对象至索引签名时,JavaScript 会在得到结果之前会先调用 `.toString` 方法: ```ts let obj = { toString() { console.log('toString called'); return 'Hello'; } }; let foo: any = {}; foo[obj] = 'World'; // toString called console.log(foo[obj]); // toString called, World console.log(foo['Hello']); // World ``` ::: tip 只要索引位置使用了 `obj`,`toString` 方法都将会被调用。 ::: 数组有点稍微不同,对于一个 `number` 类型的索引签名,JavaScript 引擎将会尝试去优化(这取决于它是否是一个真的数组、存储的项目结构是否匹配等)。因此,`number` 应该被考虑作为一个有效的对象访问器(这与 `string` 不同),如下例子: ```ts let foo = ['World']; console.log(foo[0]); // World ``` 因此,这就是 JavaScript。现在让我们看看 TypeScript 对这些概念更优雅的处理。 ## TypeScript 索引签名 JavaScript 在一个对象类型的索引签名上会隐式调用 `toString` 方法,而在 TypeScript 中,为防止初学者砸伤自己的脚(我总是看到 stackoverflow 上有很多 JavaScript 使用者都会这样。),它将会抛出一个错误。 ```ts const obj = { toString() { return 'Hello'; } }; const foo: any = {}; // ERROR: 索引签名必须为 string, number.... foo[obj] = 'World'; // FIX: TypeScript 强制你必须明确这么做: foo[obj.toString()] = 'World'; ``` 强制用户必须明确的写出 `toString()` 的原因是:在对象上默认执行的 `toString` 方法是有害的。例如 v8 引擎上总是会返回 `[object Object]` ```ts const obj = { message: 'Hello' }; let foo: any = {}; // ERROR: 索引签名必须为 string, number.... foo[obj] = 'World'; // 这里实际上就是你存储的地方 console.log(foo['[object Object]']); // World ``` 当然,数字类型是被允许的,这是因为: - 需要对数组 / 元组完美的支持; - 即使你在上例中使用 `number` 类型的值来替代 `obj`,`number` 类型默认的 `toString` 方法实现的很友好(不是 `[object Object]`)。 如下所示: ```ts console.log((1).toString()); // 1 console.log((2).toString()); // 2 ``` 因此,我们有以下结论: ::: tip TypeScript 的索引签名必须是 `string` 或者 `number`。 `symbols` 也是有效的,TypeScript 支持它。在接下来我们将会讲解它。 ::: ## 声明一个索引签名 在上文中,我们通过使用 `any` 来让 TypeScript 允许我们可以做任意我们想做的事情。实际上,我们可以明确的指定索引签名。例如:假设你想确认存储在对象中任何内容都符合 `{ message: string }` 的结构,你可以通过 `[index: string]: { message: string }` 来实现。 ```ts const foo: { [index: string]: { message: string }; } = {}; // 储存的东西必须符合结构 // ok foo['a'] = { message: 'some message' }; // Error, 必须包含 `message` foo['a'] = { messages: 'some message' }; // 读取时,也会有类型检查 // ok foo['a'].message; // Error: messages 不存在 foo['a'].messages; ``` ::: tip 索引签名的名称(如:`{ [index: string]: { message: string } }` 里的 `index` )除了可读性外,并没有任何意义。例如:如果有一个用户名,你可以使用 `{ username: string}: { message: string }`,这有利于下一个开发者理解你的代码。 ::: `number` 类型的索引也支持:`{ [count: number]: 'SomeOtherTypeYouWantToStoreEgRebate' }`。 ## 所有成员都必须符合字符串的索引签名 当你声明一个索引签名时,所有明确的成员都必须符合索引签名: ```ts // ok interface Foo { [key: string]: number; x: number; y: number; } // Error interface Bar { [key: string]: number; x: number; y: string; // Error: y 属性必须为 number 类型 } ``` 这可以给你提供安全性,任何以字符串的访问都能得到相同结果。 ```ts interface Foo { [key: string]: number; x: number; } let foo: Foo = { x: 1, y: 2 }; // 直接 foo['x']; // number // 间接 const x = 'x'; foo[x]; // number ``` ## 使用一组有限的字符串字面量 一个索引签名可以通过映射类型来使索引字符串为联合类型中的一员,如下所示: ```ts type Index = 'a' | 'b' | 'c'; type FromIndex = { [k in Index]?: number }; const good: FromIndex = { b: 1, c: 2 }; // Error: // `{ b: 1, c: 2, d: 3 }` 不能分配给 'FromIndex' // 对象字面量只能指定已知类型,'d' 不存在 'FromIndex' 类型上 const bad: FromIndex = { b: 1, c: 2, d: 3 }; ``` 这通常与 `keyof/typeof` 一起使用,来获取变量的类型,在下一章节中,我们将解释它。 变量的规则一般可以延迟被推断: ```ts type FromSomeIndex = { [key in K]: number }; ``` ## 同时拥有 `string` 和 `number` 类型的索引签名 这并不是一个常见的用例,但是 TypeScript 支持它。 `string` 类型的索引签名比 `number` 类型的索引签名更严格。这是故意设计,它允许你有如下类型: ```ts interface ArrStr { [key: string]: string | number; // 必须包括所用成员类型 [index: number]: string; // 字符串索引类型的子级 // example length: number; } ``` ## 设计模式:索引签名的嵌套 ::: tip 添加索引签名时,需要考虑的 API。 ::: 在 JavaScript 社区你将会见到很多滥用索引签名的 API。如 JavaScript 库中使用 CSS 的常见模式: ```ts interface NestedCSS { color?: string; // strictNullChecks=false 时索引签名可为 undefined [selector: string]: string | NestedCSS; } const example: NestedCSS = { color: 'red', '.subclass': { color: 'blue' } }; ``` 尽量不要使用这种把字符串索引签名与有效变量混合使用。如果属性名称中有拼写错误,这个错误不会被捕获到: ```ts const failsSilently: NestedCSS = { colour: 'red' // 'colour' 不会被捕捉到错误 }; ``` 取而代之,我们把索引签名分离到自己的属性里,如命名为 `nest`(或者 `children`、`subnodes` 等): ```ts interface NestedCSS { color?: string; nest?: { [selector: string]: NestedCSS; }; } const example: NestedCSS = { color: 'red', nest: { '.subclass': { color: 'blue' } } } const failsSliently: NestedCSS = { colour: 'red' // TS Error: 未知属性 'colour' } ``` ## 索引签名中排除某些属性 有时,你需要把属性合并至索引签名(虽然我们并不建议这么做,你应该使用上文中提到的嵌套索引签名的形式),如下例子: ```ts type FieldState = { value: string; }; type FromState = { isValid: boolean; // Error: 不符合索引签名 [filedName: string]: FieldState; }; ``` TypeScript 会报错,因为添加的索引签名,并不兼容它原有的类型,使用交叉类型可以解决上述问题: ```ts type FieldState = { value: string; }; type FormState = { isValid: boolean } & { [fieldName: string]: FieldState }; ``` 请注意尽管你可以声明它至一个已存在的 TypeScript 类型上,但是你不能创建如下的对象: ```ts type FieldState = { value: string; }; type FormState = { isValid: boolean } & { [fieldName: string]: FieldState }; // 将它用于从某些地方获取的 JavaScript 对象 declare const foo: FormState; const isValidBool = foo.isValid; const somethingFieldState = foo['something']; // 使用它来创建一个对象时,将不会工作 const bar: FormState = { // 'isValid' 不能赋值给 'FieldState' isValid: false }; ``` ================================================ FILE: docs/typings/interfaces.md ================================================ # 接口 接口运行时的影响为 0。在 TypeScript 接口中有很多方式来声明变量的结构。 下面两个是等效的声明, 示例 A 使用内联注解,示例 B 使用接口形式: ```ts // 示例 A declare const myPoint: { x: number; y: number }; // 示例 B interface Point { x: number; y: number; } declare const myPoint: Point; ``` 示例 B 的好处在于,如果有人创建了一个基于 `myPoint` 的库来添加新成员, 那么他可以轻松将此成员添加到 `myPoint` 的现有声明中: ```ts // Lib a.d.ts interface Point { x: number, y: number } declare const myPoint: Point // Lib b.d.ts interface Point { z: number } // Your code myPoint.z // Allowed! ``` TypeScript 接口是开放式的,这是 TypeScript 的一个重要原则,它允许你使用接口来模仿 JavaScript 的可扩展性。 ## 类可以实现接口 如果你希望在类中使用必须要被遵循的接口(类)或别人定义的对象结构,可以使用 `implements` 关键字来确保其兼容性: ```ts interface Point { x: number; y: number; } class MyPoint implements Point { x: number; y: number; // Same as Point } ``` 基本上,在 `implements(实现)` 存在的情况下,该外部 `Point` 接口的任何更改都将导致代码库中的编译错误,因此可以轻松地使其保持同步: ```ts interface Point { x: number; y: number; z: number; // New member } class MyPoint implements Point { // ERROR : missing member `z` x: number; y: number; } ``` 注意,`implements` 限制了类实例的结构,如下所示: ```ts let foo: Point = new MyPoint(); ``` 但像 `foo: Point = MyPoint` 这样的代码,与其并不是一回事。 ## 注意 ### 并非每个接口都是很容易实现的 接口旨在声明 JavaScript 中可能存在的任意结构。 思考以下例子,可以使用 `new` 调用某些内容: ```ts interface Crazy { new (): { hello: number; }; } ``` 你可能会有下面这样的代码: ```ts class CrazyClass implements Crazy { constructor() { return { hello: 123 }; } } // Because const crazy = new CrazyClass(); // crazy would be { hello:123 } ``` 你可以使用接口声明所有“疯狂的”的 JavaScript 代码,甚至可以安全地在 TypeScript 中使用它们。但这并不意味着你可以使用 TypeScript 类来实现它们。 ================================================ FILE: docs/typings/lib.md ================================================ # `lib.d.ts` 当你安装 `TypeScript` 时,会顺带安装一个 `lib.d.ts` 声明文件。这个文件包含 JavaScript 运行时以及 DOM 中存在各种常见的环境声明。 - 它自动包含在 TypeScript 项目的编译上下文中; - 它能让你快速开始书写经过类型检查的 JavaScript 代码。 你可以通过指定 `--noLib` 的编译器命令行标志(或者在 `tsconfig.json` 中指定选项 `noLib: true`)从上下文中排除此文件。 ## 使用例子 看如下例子: ```ts const foo = 123; const bar = foo.toString(); ``` 这段代码的类型检查正常,因为 `lib.d.ts` 为所有 JavaScript 对象定义了 `toString` 方法。 如果你在 `noLib` 选项下,使用相同的代码,这将会出现类型检查错误: ```ts const foo = 123; const bar = foo.toString(); // Error: 属性 toString 不存在类型 number 上 ``` 现在你已经理解了 `lib.d.ts` 的重要性,至于它的内容是怎么样的,我们接下来将会解释。 ## 观察 `lib.d.ts` 的内容 `lib.d.ts` 的内容主要是一些变量声明(如:`window`、`document`、`math`)和一些类似的接口声明(如:`Window`、`Document`、`Math`)。 寻找代码类型(如:`Math.floor`)的最简单方式是使用 IDE 的 `F12`(跳转到定义)。 让我们来看一个变量声明的示例,如 `window` 被定义为: ```ts declare var window: Window; ``` 这只是一个简单的 `declare var`,后面跟一个变量名称(`window`)和一个用来类型注解的接口(`Window`),这些变量通常指向一些全局的接口,例如,以下是 `Window` 接口的一小部分: ```ts interface Window extends EventTarget, WindowTimers, WindowSessionStorage, WindowLocalStorage, WindowConsole, GlobalEventHandlers, IDBEnvironment, WindowBase64 { animationStartTime: number; applicationCache: ApplicationCache; clientInformation: Navigator; closed: boolean; crypto: Crypto; // so on and so forth... } ``` 你可以在这些接口里看到大量的类型信息,当你不使用 TypeScript 时,你需要将它们保存在你的大脑里。现在你可以使用 `intellisense` 之类东西,从而可以减少对知识的记忆。 使用这些全局变量是有利的。在不更改 `lib.d.ts` 的情况下,它可以让你添加额外的属性。接下来,我们将介绍这些概念。 ## 修改原始类型 在 TypeScript 中,接口是开放式的,这意味着当你想使用不存在的成员时,只需要将它们添加至 `lib.d.ts` 中的接口声明中即可,TypeScript 将会自动接收它。注意,你需要在[全局模块](../project/modules.md)中做这些修改,以使这些接口与 `lib.d.ts` 相关联。我们推荐你创建一个称为 `global.d.ts` 的特殊文件。 这里有我们需要添加至 `Window`,`Math`,`Date` 的一些例子: ### Window 仅仅是添加至 `Window` 接口: ```ts interface Window { helloWorld(): void; } ``` 这将允许你以类型安全的形式使用它: ```ts // Add it at runtime window.helloWorld = () => console.log('hello world'); // Call it window.helloWorld(); // 滥用会导致错误 window.helloWorld('gracius'); // Error: 提供的参数与目标不匹配 ``` ### Math 全局变量 `Math` 在 `lib.d.ts` 中被定义为: ```ts /** An intrinsic object that provides basic mathematics functionality and constants. */ declare var Math: Math; ``` 即变量 `Math` 是 `Math` 的一个实例,`Math` 接口被定义为: ```ts interface Math { E: number; LN10: number; // others ... } ``` 当你想在 `Math` 全局变量上添加你需要的属性时,你只需要把它添加到 `Math` 的全局接口上即可,例如:在[`seedrandom Project`](https://www.npmjs.com/package/seedrandom)项目里,它添加了 `seedrandom` 函数至全局的 `Math` 对象上,这很容易被声明: ```ts interface Math { seedrandom(seed?: string): void; } ``` 你可以像下面一样使用它: ```ts Math.seedrandom(); Math.seedrandom('Any string you want'); ``` ### Date 如果你在 `lib.d.ts` 中寻找 `Date` 定义的声明,你将会找到如下代码: ```ts declare var Date: DateConstructor; ``` 接口 `DateConstructor` 与上文中 `Math` 和 `Window` 接口一样,它涵盖了可以使用的 `Date` 全局变量的成员(如:`Date.now()`)。除此之外,它还包含了可以让你创建 `Date` 实例的构造函数签名(如:`new Date()`)。`DateConstructor` 接口的一部分代码如下所示: ```ts interface DateConstructor { new (): Date; // 一些其他的构造函数签名 now(): number; // 其他成员函数 } ``` 在 [datejs](https://github.com/abritinthebay/datejs) 里,它在 `Date` 的全局变量以及 `Date` 实例上同时添加了成员,因此这个库的 TypeScript 定义看起来像如下所示(社区已经[定义](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/datejs/index.d.ts)好了): ```ts // DateJS 公开的静态方法 interface DateConstructor { /** Gets a date that is set to the current date. The time is set to the start of the day (00:00 or 12:00 AM) */ today(): Date; // ... so on and so forth } // DateJS 公开的实例方法 interface Date { /** Adds the specified number of milliseconds to this instance. */ addMilliseconds(milliseconds: number): Date; // ... so on and so forth } ``` 这允许你在类型安全的情况下做: ```ts const today = Date.today(); const todayAfter1second = today.addMilliseconds(1000); ``` ### string 如果你在 `lib.d.ts` 里寻找 `string`,你将会找到与 `Date` 相类似的内容(全局变量 `String`,`StringConstructor` 接口,`String` 接口)。但值得注意的是,`String` 接口也会影响字符串字面量,如下所示: ```ts interface String { endsWith(suffix: string): boolean; } String.prototype.endsWith = function(suffix: string): boolean { const str: string = this; return str && str.indexOf(suffix, str.length - suffix.length) !== -1; }; console.log('foo bar'.endsWith('bas')); // false console.log('foo bas'.endsWith('bas')); // true ``` ### 终极 string 基于可维护性,我们推荐创建一个 `global.d.ts` 文件。然而,如果你愿意,你可以通过使用 `declare global { /* global namespace */ }`,从文件模块中进入全局命名空间: ```ts // 确保是模块 export {}; declare global { interface String { endsWith(suffix: string): boolean; } } String.prototype.endsWith = function(suffix: string): boolean { const str: string = this; return str && str.indexOf(suffix, str.length - suffix.length) !== -1; }; console.log('foo bar'.endsWith('bas')); // false console.log('foo bas'.endsWith('bas')); // true ``` ## 使用你自己定义的 `lib.d.ts` 正如上文所说,使用 `--noLib` 编译选项会导致 TypeScript 排除自动包含的 `lib.d.ts` 文件。为什么这个功能是有效的,我例举了一些常见原因: - 运行的 JavaScript 环境与基于标准浏览器运行时环境有很大不同; - 你希望在代码里严格的控制全局变量,例如:`lib.d.ts` 将 `item` 定义为全局变量,你不希望它泄漏到你的代码里。 一旦你排除了默认的 `lib.d.ts` 文件,你就可以在编译上下文中包含一个命名相似的文件,TypeScript 将提取该文件进行类型检查。 ::: tip 小心使用 `--noLib` 选项,一旦你使用了它,当你把你的项目分享给其他人时,它们也将被迫使用 `--noLib` 选项,更糟糕的是,如果将这些代码放入你的项目中,你可能需要将它们移植到基于你的代码的 `lib` 中。 ::: ## 编译目标对 `lib.d.ts` 的影响 设置编译目标为 `es6` 时,能导致 `lib.d.ts` 包含更多像 Promise 现代(es6)内容的环境声明。编译器目标的这种作用,改变了代码的环境,这对某些人来说是理想的,但是这对另外一些人来说造成了困扰,因为它将编译出的代码与环境混为一谈。 当你想对环境进行更细粒的控制时,你应该使用我们接下来将要讨论的 `--lib` 选项。 ## `--lib` 选项 有时,你想要解耦编译目标(即生成的 JavaScript 版本)和环境库支持之间的关系。例如对于 Promise,你的编译目标是 `--target es5`,但是你仍然想使用它,这时,你可以使用 `lib` 对它进行控制。 ::: tip 使用 `--lib` 选项可以将任何 `lib` 与 `--target` 解耦。 ::: 你可以通过命令行或者在 `tsconfig.json` 中提供此选项(推荐): ### 命令行 ```ts tsc --target es5 --lib dom,es6 ``` ### config.json ```json "compilerOptions": { "lib": ["dom", "es6"] } ``` `lib` 分类如下: - JavaScript 功能 - es5 - es6 - es2015 - es7 - es2016 - es2017 - esnext - 运行环境 - dom - dom.iterable - webworker - scripthost - ESNext 功能选项 - es2015.core - es2015.collection - es2015.generator - es2015.iterable - es2015.promise - es2015.proxy - es2015.reflect - es2015.symbol - es2015.symbol.wellknown - es2016.array.include - es2017.object - es2017.sharedmemory - esnext.asynciterable ::: tip NOTE `--lib` 选项提供非常精细的控制,因此你最有可能从运行环境与 JavaScript 功能类别中分别选择一项,如果你没有指定 `--lib`,则会导入默认库: - `--target` 选项为 es5 时,会导入 es5, dom, scripthost。 - `--target` 选项为 es6 时,会导入 es6, dom, dom.iterable, scripthost。 ::: 我个人的推荐: ```json "compilerOptions": { "target": "es5", "lib": ["es6", "dom"] } ``` 包括使用 Symbol 的 ES5 使用例子: ```json "compilerOptions": { "target": "es5", "lib": ["es5", "dom", "scripthost", "es2015.symbol"] } ``` ## 在旧的 JavaScript 引擎时使用 Polyfill > [关于此主题的一个视频](https://egghead.io/lessons/typescript-using-es6-and-esnext-with-typescript) 要使用一些新功能如 `Map`、`Set`、`Promise`(随着时间推移会变化),你可以使用现代的 `lib` 选项,并且需要安装 `core-js`: ```shell npm install core-js --save-dev ``` 接着,在你的项目里导入它: ```ts import 'core-js'; ``` ================================================ FILE: docs/typings/literals.md ================================================ # 字面量类型 字面量是 JavaScript 本身提供的一个准确变量。 ## 字符串字面量 你可以使用一个字符串字面量作为一个类型: ```ts let foo: 'Hello'; ``` 在这里,我们创建了一个被称为 `foo` 变量,它仅接收一个字面量值为 `Hello` 的变量: ```ts let foo: 'Hello'; foo = 'Bar'; // Error: 'bar' 不能赋值给类型 'Hello' ``` 它们本身并不是很实用,但是可以在一个联合类型中组合创建一个强大的(实用的)抽象: ```ts type CardinalDirection = 'North' | 'East' | 'South' | 'West'; function move(distance: number, direction: CardinalDirection) { // ... } move(1, 'North'); // ok move(1, 'Nurth'); // Error ``` ## 其他字面量类型 TypeScript 同样也提供 `boolean` 和 `number` 的字面量类型: ```ts type OneToFive = 1 | 2 | 3 | 4 | 5; type Bools = true | false; ``` ## 推断 通常,你会得到一个类似于 `Type string is not assignable to type 'foo'` 的错误,如下: ```ts function iTakeFoo(foo: 'foo') {} const test = { someProp: 'foo' }; iTakeFoo(test.someProp); // Error: Argument of type string is not assignable to parameter of type 'foo' ``` 这是由于 `test` 被推断为 `{ someProp: string }`,我们可以采用一个简单的类型断言来告诉 TypeScript 你想推断的字面量: ```ts function iTakeFoo(foo: 'foo') {} const test = { someProp: 'foo' as 'foo' }; iTakeFoo(test.someProp); // ok ``` 或者使用类型注解的方式,来帮助 TypeScript 推断正确的类型: ```ts function iTakeFoo(foo: 'foo') {} type Test = { someProp: 'foo'; }; const test: Test = { // 推断 `someProp` 永远是 'foo' someProp: 'foo' }; iTakeFoo(test.someProp); // ok ``` ## 使用用例 TypeScript 枚举类型是基于数字的,你可以使用带字符串字面量的联合类型,来模拟一个基于字符串的枚举类型,就好像上文中提出的 `CardinalDirection`。你甚至可以使用下面的函数来生成 `key: value` 的结构: ```ts // 用于创建字符串列表映射至 `K: V` 的函数 function strEnum(o: Array): { [K in T]: K } { return o.reduce((res, key) => { res[key] = key; return res; }, Object.create(null)); } ``` 然后,你就可以使用 `keyof`、`typeof` 来生成字符串的联合类型。下面是一个完全的例子: ```ts // 用于创建字符串列表映射至 `K: V` 的函数 function strEnum(o: Array): { [K in T]: K } { return o.reduce((res, key) => { res[key] = key; return res; }, Object.create(null)); } // 创建 K: V const Direction = strEnum(['North', 'South', 'East', 'West']); // 创建一个类型 type Direction = keyof typeof Direction; // 简单的使用 let sample: Direction; sample = Direction.North; // Okay sample = 'North'; // Okay sample = 'AnythingElse'; // ERROR! ``` ## 辨析联合类型 我们将会在此书的稍后章节讲解它。 ================================================ FILE: docs/typings/migrating.md ================================================ # 从 JavaScript 迁移 首先,假设如下: - 你了解 JavaScript; - 你了解在项目中常用的方式和构建工具(如:webpack)。 有了以上假设,一般来说,将 JavaScript 代码迁移至 TypeScript 包括以下步骤: - 添加一个 `tsconfig.json` 文件; - 把文件扩展名从 `.js` 改成 `.ts`,开始使用 `any` 来减少错误; - 开始在 TypeScript 中写代码,尽可能的减少 `any` 的使用; - 回到旧代码,开始添加类型注解,并修复已识别的错误; - 为第三方 JavaScript 代码定义环境声明。 让我们进一步讨论其中的几个关键点。 记住:所有的 JavaScript 代码都是有效的 TypeScript 代码。这意味着,如果让 TypeScript 编译器编译 TypeScript 里的 JavaScript 代码,编译后的结果将会与原始的 JavaScript 代码一模一样。也就是说,把文件扩展名从 `.js` 改成 `.ts` 将不会造成任何负面的影响。 ## 减少错误 代码被迁移至 TypeScript 后,TypeScript 将会立即对你的代码进行类型检查,你的 JavaScript 代码可能并不像想象中那样整齐了,因此你可能会收到一些报错信息。这时,可以使用 `any` 来解决大部分的报错问题: ```typescript let foo = 123; let bar = 'hey'; bar = foo; // Error: 不能把 number 类型赋值给 string 类型 ``` 虽然这些错误是有效的,并且在大多数情况下,根据这些错误所推断出的信息比代码库的不同部分的原始作者想象的更好,但是你的重点是在逐步更新旧代码库的同时,用 TypeScript 编写新代码。在这里,你可以使用类型断言来减少此错误: ```typescript let foo = 123; let bar = 'hey'; bar = foo as any; // ok ``` 从另一方面来说,你可能想用 `any` 用作类型注解: ```typescript function foo() { return 1; } let bar = 'hey'; bar = foo(); // Error: 不能把一个 number 类型赋值给 string 类型 ``` 减少这种错误: ```typescript function foo(): any { // 添加 'any' return 1; } let bar = 'hey'; bar = foo(); ``` ::: warning NOTICE 使用此种方式来减少错误是危险的,但是它允许你将注意力转移到你的新 TypeScript 代码错误上。当你进行下一步前,最好要留下 `// TODO` 的注释。 ::: ## 第三方代码 你可以将你的 JavaScript 代码改成 TypeScript 代码,但是你不能让整个世界都使用 TypeScript。这正是 TypeScript 环境声明支持的地方。我建议你以创建一个 `vendor.d.ts` 文件作为开始(`.d.ts` 文件扩展名指定这个文件是一个声明文件),然后我向文件里添加东西。或者,你也可以创建一个针对于特定库的声明文件,如为 jquery 创建 `jquery.d.ts` 文件。 ::: tip NOTICE 几乎排名前 90% 的 JavaScript 库的声明文件存在于 [DefinitelyTyped](https://github.com/borisyankov/DefinitelyTyped) 仓库里,在创建自己定义的声明文件之前,我们建议你先去仓库中寻找是否有对应的声明文件。尽管如此,创建一个声明文件这种快速但不好的方式是减小使用 TypeScript 初始阻力的重要步骤 ::: 根据 `jquery` 的使用,你可以非常简单快速的为它创建一个定义: ```typescript declare var $: any; ``` 有时,你可能想在某些内容(如 `jQuery`)上添加显式的注解,并且你会在类型声明空间中使用它。你可以通过 `type` 关键字快速的实现它: ```typescript declare type JQuery = any; declare var $: JQuery; ``` 这提供给你一个更清晰的使用模式。 一个高质量的 `jquery.d.ts` 已经在 [DefinitelyTyped](https://github.com/borisyankov/DefinitelyTyped) 中存在。现在你已经知道如何在使用第三方 JavaScript 模块时,快速克服从 JavaScript 至 TypeScript 的阻力了。在接下去的章节,我们将会讨论环境声明。 ## 第三方的 NPM 模块 与全局变量声明相似,你可以快速的定义一个全局模块,如:对于 `jquery`,如果你想把它作为一个模块来使用([NPM](https://www.npmjs.com/package/jquery)),可以自己通过以下方式实现: ```typescript declare module 'jquery'; ``` 然后你就可以在必要时导入它: ```typescript import * as $ from 'jquery'; ``` ::: tip 再一次说明,一个高质量的 `jquery.d.ts` 已经在 [DefinitelyTyped](https://github.com/borisyankov/DefinitelyTyped) 中存在,但是可能在你的包里没有,那么,你现在有一个简单快速的方式来继续迁移。 ::: ## 额外的非 JavaScript 资源 在 TypeScript 中,甚至可以允许你导入任何文件,例如 `.css` 文件(如果你使用的是 webpack 样式加载器或 css 模块),你只要添加如下代码(放在 `global.d.ts`): ```typescript declare module '*.css'; ``` 现在你可以使用 `import * as foo from './some/file.css'`。 与此相似,如果你想使用 html 模版(例如:angular),你可以: ```typescript declare module '*.html'; ``` ================================================ FILE: docs/typings/mixins.md ================================================ # 混合 TypeScript (和 JavaScript) 类只能严格的单继承,因此你不能做: ```ts class User extends Tagged, Timestamped { // ERROR : 不能多重继承 // .. } ``` 从可重用组件构建类的另一种方式是通过基类来构建它们,这种方式称为混合。 这个主意是简单的,采用函数 B 接受一个类 A,并且返回一个带有新功能的类的方式来替代 A 类扩展 B 来获取 B 上的功能,前者中的 B 即是混合。 ::: tip 「混合」是一个函数: - 传入一个构造函数; - 创建一个带有新功能,并且扩展构造函数的新类; - 返回这个新类。 ::: 一个完整的例子: ```ts // 所有 mixins 都需要 type Constructor = new (...args: any[]) => T; ///////////// // mixins 例子 //////////// // 添加属性的混合例子 function Timestamped(Base: TBase) { return class extends Base { timestamp = Date.now(); }; } // 添加属性和方法的混合例子 function Activatable(Base: TBase) { return class extends Base { isActivated = false; activate() { this.isActivated = true; } deactivate() { this.isActivated = false; } }; } /////////// // 组合类 /////////// // 简单的类 class User { name = ''; } // 添加 Timestamped 的 User const TimestampedUser = Timestamped(User); // Tina Timestamped 和 Activatable 的类 const TimestampedActivatableUser = Timestamped(Activatable(User)); ////////// // 使用组合类 ////////// const timestampedUserExample = new TimestampedUser(); console.log(timestampedUserExample.timestamp); const timestampedActivatableUserExample = new TimestampedActivatableUser(); console.log(timestampedActivatableUserExample.timestamp); console.log(timestampedActivatableUserExample.isActivated); ``` 让我们分解这个例子。 ## 创建一个构造函数 混合接受一个类,并且使用新功能扩展它。因此,我们需要定义构造函数的类型: ```ts type Constructor = new (...args: any[]) => T; ``` ## 扩展一个类并且返回它 ```ts // 添加属性的混合例子 function Timestamped(Base: TBase) { return class extends Base { timestamp = Date.now(); }; } ``` ================================================ FILE: docs/typings/movingTypes.md ================================================ # 流动的类型 TypeScript 类型系统非常强大,它支持其他任何单一语言无法实现的类型流动和类型片段。 这是因为 TypeScript 的设计目的之一是让你无缝与像 JavaScript 这类高动态的语言一起工作。在这里,我们介绍一些在 TypeScript 中使用移动类型的技巧。 关键的动机:当你改变了其中一个时,其他相关的会自动更新,并且当有事情变糟糕时,你会得到一个友好的提示,就好像一个被精心设计过的约束系统。 ## 复制类型和值 如果你想移动一个类,你可能会想要做以下事情: ```ts class Foo {} const Bar = Foo; let bar: Bar; // Error: 不能找到名称 'Bar' ``` 这会得到一个错误,因为 `const` 仅仅是复制了 `Foo` 到一个变量声明空间,因此你无法把 `Bar` 当作一个类型声明使用。正确的方式是使用 `import` 关键字,请注意,如果你在使用 `namespace` 或者 `modules`,使用 `import` 是你唯一能用的方式: ```ts namespace importing { export class Foo {} } import Bar = importing.Foo; let bar: Bar; // ok ``` 这个 `import` 技巧,仅适合于类型和变量。 ## 捕获变量的类型 你可以通过 `typeof` 操作符在类型注解中使用变量。这允许你告诉编译器,一个变量的类型与其他类型相同,如下所示: ```ts let foo = 123; let bar: typeof foo; // 'bar' 类型与 'foo' 类型相同(在这里是: 'number') bar = 456; // ok bar = '789'; // Error: 'string' 不能分配给 'number' 类型 ``` ## 捕获类成员的类型 与捕获变量的类型相似,你仅仅是需要声明一个变量用来捕获到的类型: ```ts class Foo { foo: number; // 我们想要捕获的类型 } declare let _foo: Foo; // 与之前做法相同 let bar: typeof _foo.foo; ``` ## 捕获字符串类型 许多 JavaScript 库和框架都使用原始的 JavaScript 字符串,你可以使用 `const` 定义一个变量捕获它的类型: ```ts // 捕获字符串的类型与值 const foo = 'Hello World'; // 使用一个捕获的类型 let bar: typeof foo; // bar 仅能被赋值 'Hello World' bar = 'Hello World'; // ok bar = 'anything else'; // Error ``` 在这个例子里,`bar` 有字面量类型 `Hello World`,我们在[字面量类型](./literals.md)章节已经深入讨论。 ## 捕获键的名称 `keyof` 操作符能让你捕获一个类型的键。例如,你可以使用它来捕获变量的键名称,在通过使用 `typeof` 来获取类型之后: ```ts const colors = { red: 'red', blue: 'blue' }; type Colors = keyof typeof colors; let color: Colors; // color 的类型是 'red' | 'blue' color = 'red'; // ok color = 'blue'; // ok color = 'anythingElse'; // Error ``` 这允许你很容易地拥有像字符串枚举+常量这样的类型,如上例所示。 ================================================ FILE: docs/typings/neverType.md ================================================ # Never ::: tip [一个关于 never 的介绍视频](https://egghead.io/lessons/typescript-use-the-never-type-to-avoid-code-with-dead-ends-using-typescript) ::: 程序语言的设计确实应该存在一个底部类型的概念,当你在分析代码流的时候,这会是一个理所当然存在的类型。TypeScript 就是这样一种分析代码流的语言(:sunglasses:),因此它需要一个可靠的,代表永远不会发生的类型。 `never` 类型是 TypeScript 中的底层类型。它自然被分配的一些例子: - 一个从来不会有返回值的函数(如:如果函数内含有 `while(true) {}`); - 一个总是会抛出错误的函数(如:`function foo() { throw new Error('Not Implemented') }`,`foo` 的返回类型是 `never`); 你也可以将它用做类型注解: ```ts let foo: never; // ok ``` 但是,`never` 类型仅能被赋值给另外一个 `never`: ```ts let foo: never = 123; // Error: number 类型不能赋值给 never 类型 // ok, 作为函数返回类型的 never let bar: never = (() => { throw new Error('Throw my hands in the air like I just dont care'); })(); ``` 很棒,现在让我们看看它的关键用例。 ## 用例:详细的检查 ```ts function foo(x: string | number): boolean { if (typeof x === 'string') { return true; } else if (typeof x === 'number') { return false; } // 如果不是一个 never 类型,这会报错: // - 不是所有条件都有返回值 (严格模式下) // - 或者检查到无法访问的代码 // 但是由于 TypeScript 理解 `fail` 函数返回为 `never` 类型 // 它可以让你调用它,因为你可能会在运行时用它来做安全或者详细的检查。 return fail('Unexhaustive'); } function fail(message: string): never { throw new Error(message); } ``` `never` 仅能被赋值给另外一个 `never` 类型,因此你可以用它来进行编译时的全面的检查,我们将会在[辨析联合类型](./discrominatedUnion.md)中讲解它。 ## 与 `void` 的差异 一旦有人告诉你,`never` 表示一个从来不会优雅的返回的函数时,你可能马上就会想到与此类似的 `void`,然而实际上,`void` 表示没有任何类型,`never` 表示永远不存在的值的类型。 当一个函数返回空值时,它的返回值为 void 类型,但是,当一个函数永不返回时(或者总是抛出错误),它的返回值为 never 类型。void 类型可以被赋值(在 strictNullChecking 为 false 时),但是除了 never 本身以外,其他任何类型不能赋值给 never。 ================================================ FILE: docs/typings/overview.md ================================================ # 概览 ## TypeScript 类型系统 在讨论[为什么使用 TypeScript](https://jkchao.github.io/typescript-book-chinese/#whys) 时,我们表述了 TypeScript 类型系统的主要功能。以下是一些关键点: - TypeScript 的类型系统被设计为可选的,因此,你的 JavaScript 就是 TypeScript; - TypeScript 不会阻止 JavaScript 的运行,即使存在类型错误也不例外,这能让你的 JavaScript 逐步迁移至 TypeScript。 现在让我们开始学习 TypeScript 类型系统的语法吧,在这一章节中,你将能给你的代码加上类型注解,并且能看到它的益处。这将为我们进一步了解类型系统做铺垫。 ## 基本注解 如前文所提及,类型注解使用 `:TypeAnnotation` 语法。在类型声明空间中可用的任何内容都可以用作类型注解。 在下面这个例子中,使用了变量、函数参数以及函数返回值的类型注解: ```ts const num: number = 123; function identity(num: number): number { return num; } ``` ## 原始类型 JavaScript 原始类型也同样适应于 TypeScript 的类型系统,因此 `string`、`number`、`boolean` 也可以被用作类型注解: ```ts let num: number; let str: string; let bool: boolean; num = 123; num = 123.456; num = '123'; // Error str = '123'; str = 123; // Error bool = true; bool = false; bool = 'false'; // Error ``` ## 数组 TypeScript 为数组提供了专用的类型语法,因此你可以很轻易的注解数组。它使用后缀 `[]`, 接着你可以根据需要补充任何有效的类型注解(如:`:boolean[]`)。它能让你安全的使用任何有关数组的操作,而且它也能防止一些类似于赋值错误类型给成员的行为。如下所示: ```ts let boolArray: boolean[]; boolArray = [true, false]; console.log(boolArray[0]); // true console.log(boolArray.length); // 2 boolArray[1] = true; boolArray = [false, false]; boolArray[0] = 'false'; // Error boolArray = 'false'; // Error boolArray = [true, 'false']; // Error ``` ## 接口 接口是 TypeScript 的一个核心知识,它能合并众多类型声明至一个类型声明: ```ts interface Name { first: string; second: string; } let name: Name; name = { first: 'John', second: 'Doe' }; name = { // Error: 'Second is missing' first: 'John' }; name = { // Error: 'Second is the wrong type' first: 'John', second: 1337 }; ``` 在这里,我们把类型注解:`first: string` + `second: string` 合并到了一个新的类型注解 `Name` 里,这样能强制对每个成员进行类型检查。接口在 TypeScript 拥有强大的力量,稍后,我们将会用一个内容专门阐述如何更好的使用它。 ## 内联类型注解 与创建一个接口不同,你可以使用内联注解语法注解任何内容:`:{ /*Structure*/ }`: ```ts let name: { first: string; second: string; }; name = { first: 'John', second: 'Doe' }; name = { // Error: 'Second is missing' first: 'John' }; name = { // Error: 'Second is the wrong type' first: 'John', second: 1337 }; ``` 内联类型能为你快速的提供一个类型注解。它可以帮助你省去为类型起名的麻烦(你可能会使用一个很糟糕的名称)。然而,如果你发现需要多次使用相同的内联注解时,那么考虑把它重构为一个接口(或者是 `type alias`,它会在接下来的部分提到)是一个不错的主意。 ## 特殊类型 除了被提到的一些原始类型,在 TypeScript 中,还存在一些特殊的类型,它们是 `any`、 `null`、 `undefined` 以及 `void`。 ### any `any` 类型在 TypeScript 类型系统中占有特殊的地位。它提供给你一个类型系统的「后门」,TypeScript 将会把类型检查关闭。在类型系统里 `any` 能够兼容所有的类型(包括它自己)。因此,所有类型都能被赋值给它,它也能被赋值给其他任何类型。以下有一个证明例子: ```ts let power: any; // 赋值任意类型 power = '123'; power = 123; // 它也兼容任何类型 let num: number; power = num; num = power; ``` 当你把 JavaScript 迁移至 TypeScript 时,你将会经常性使用 `any`。但你必须减少对它的依赖,因为你需要确保类型安全。当使用 `any` 时,你基本上是在告诉 TypeScript 编译器不要进行任何的类型检查。 ### null 和 undefined 在类型系统中,JavaScript 中的 null 和 undefined 字面量和其他被标注了 `any` 类型的变量一样,都能被赋值给任意类型的变量,如下例子所示: ```ts // strictNullChecks: false let num: number; let str: string; // 这些类型能被赋予 num = null; str = undefined; ``` ### void 使用 `:void` 来表示一个函数没有一个返回值 ```ts function log(message: string): void { console.log(message); } ``` ## 泛型 在计算机科学中,许多算法和数据结构并不会依赖于对象的实际类型。但是,你仍然会想在每个变量里强制提供约束。例如:在一个函数中,它接受一个列表,并且返回这个列表的反向排序,这里的约束是指传入至函数的参数与函数的返回值: ```ts function reverse(items: T[]): T[] { const toreturn = []; for (let i = items.length - 1; i >= 0; i--) { toreturn.push(items[i]); } return toreturn; } const sample = [1, 2, 3]; let reversed = reverse(sample); console.log(reversed); // 3, 2, 1 // Safety reversed[0] = '1'; // Error reversed = ['1', '2']; // Error reversed[0] = 1; // ok reversed = [1, 2]; // ok ``` 在上个例子中,函数 `reverse` 接受一个类型为 `T`(注意在 `reverse` 中的类型参数) 的数组(`items: T[]`),返回值为类型 T 的一个数组(注意:T[]),函数 `reverse` 的返回值类型与它接受的参数的类型一样。当你传入 `const sample = [1, 2, 3]` 时,TypeScript 能推断出 `reverse` 为 `number[]` 类型,从而能给你类型安全。与此相似,当你传入一个类型为 `string[]` 类型的数组时,TypeScript 能推断 `reverse` 为 `string`[] 类型,如下例子所示: ```ts const strArr = ['1', '2']; let reversedStrs = reverse(strArr); reversedStrs = [1, 2]; // Error ``` 事实上,JavaScript 数组已经拥有了 `reverse` 的方法,TypeScript 也确实使用了泛型来定义其结构: ```ts interface Array { reverse(): T[]; } ``` 这意味着,当你在数组上调用 `.reverse` 方法时,将会获得类型安全: ```ts let numArr = [1, 2]; let reversedNums = numArr.reverse(); reversedNums = ['1', '2']; // Error ``` 当稍后在 [环境声明](./ambient.md) 章节中提及 `lib.d.ts` 时,我们会讨论更多关于 `Array` 的信息。 ## 联合类型 在 JavaScript 中,你可能希望属性为多种类型之一,如字符串或者数组。这正是 TypeScript 中联合类型能派上用场的地方(它使用 `|` 作为标记,如 `string | number`)。关于联合类型,一个常见的用例是一个可以接受字符串数组或单个字符串的函数: ```ts function formatCommandline(command: string[] | string) { let line = ''; if (typeof command === 'string') { line = command.trim(); } else { line = command.join(' ').trim(); } // Do stuff with line: string } ``` ## 交叉类型 在 JavaScript 中, `extend` 是一种非常常见的模式,在这种模式中,你可以从两个对象中创建一个新对象,新对象拥有着两个对象所有的功能。交叉类型可以让你安全的使用此种模式: ```ts function extend(first: T, second: U): T & U { const result = {}; for (let id in first) { (result)[id] = first[id]; } for (let id in second) { if (!result.hasOwnProperty(id)) { (result)[id] = second[id]; } } return result; } const x = extend({ a: 'hello' }, { b: 42 }); // 现在 x 拥有了 a 属性与 b 属性 const a = x.a; const b = x.b; ``` ## 元组类型 JavaScript 并不支持元组,开发者们通常只能使用数组来表示元组。而 TypeScript 支持它,开发者可以使用 `:[typeofmember1, typeofmember2]` 的形式,为元组添加类型注解,元组可以包含任意数量的成员,示例: ```ts let nameNumber: [string, number]; // Ok nameNumber = ['Jenny', 221345]; // Error nameNumber = ['Jenny', '221345']; ``` 将其与 TypeScript 中的解构一起使用: ```ts let nameNumber: [string, number]; nameNumber = ['Jenny', 322134]; const [name, num] = nameNumber; ``` ## 类型别名 TypeScript 提供了为类型注解设置别名的便捷语法,你可以使用 `type SomeName = someValidTypeAnnotation` 来创建别名: ```ts type StrOrNum = string | number; // 使用 let sample: StrOrNum; sample = 123; sample = '123'; // 会检查类型 sample = true; // Error ``` 与接口不同,你可以为任意的类型注解提供类型别名(在联合类型和交叉类型中比较实用),下面是一些能让你熟悉类型别名语法的示例。 ```ts type Text = string | { text: string }; type Coordinates = [number, number]; type Callback = (data: string) => void; ``` :::tip - 如果你需要使用类型注解的层次结构,请使用接口。它能使用 `implements` 和 `extends` - 为一个简单的对象类型(如上面例子中的 Coordinates)使用类型别名,只需要给它一个语义化的名字即可。另外,当你想给联合类型和交叉类型提供一个语义化的名称时,一个类型别名将会是一个好的选择。 ::: ## 最后 现在你已经能够为你的大部分 JavaScript 代码添加类型注解,接着,让我们深入了解 TypeScript 的类型系统吧。 ================================================ FILE: docs/typings/readonly.md ================================================ # readonly TypeScript 类型系统允许你在一个接口里使用 `readonly` 来标记属性。它能让你以一种更安全的方式工作(不可预期的改变是很糟糕的): ```ts function foo(config: { readonly bar: number, readonly bas: number }) { // .. } const config = { bar: 123, bas: 123 }; foo(config); // 现在你能够确保 'config' 不能够被改变了 ``` 当然,你也可以在 `interface` 和 `type` 里使用 `readonly`: ```ts type Foo = { readonly bar: number; readonly bas: number; }; // 初始化 const foo: Foo = { bar: 123, bas: 456 }; // 不能被改变 foo.bar = 456; // Error: foo.bar 为仅读属性 ``` 你也能指定一个类的属性为只读,然后在声明时或者构造函数中初始化它们,如下所示: ```ts class Foo { readonly bar = 1; // OK readonly baz: string; constructor() { this.baz = 'hello'; // OK } } ``` ## Readonly 这有一个 `Readonly` 的映射类型,它接收一个泛型 `T`,用来把它的所有属性标记为只读类型: ```ts type Foo = { bar: number; bas: number; }; type FooReadonly = Readonly; const foo: Foo = { bar: 123, bas: 456 }; const fooReadonly: FooReadonly = { bar: 123, bas: 456 }; foo.bar = 456; // ok fooReadonly.bar = 456; // Error: bar 属性只读 ``` ## 其他的使用用例 ### ReactJS `ReactJS` 是一个喜欢用不变数据的库,你可以标记你的 `Props` 和 `State` 为不可变数据: ```ts interface Props { readonly foo: number; } interface State { readonly bar: number; } export class Something extends React.Component { someMethod() { // 你可以放心,没有人会像下面这么做 this.props.foo = 123; // Error: props 是不可变的 this.state.baz = 456; // Error: 你应该使用 this.setState() } } ``` 然而,你并没有必要这么做,`React` 的声明文件已经标记这些为 `readonly`(通过传入泛型参数至一个内部包装,来把每个属性标记为 `readonly`,如上例子所示), ```ts export class Something extends React.Component<{ foo: number }, { baz: number }> { someMethod() { this.props.foo = 123; // Error: props 是不可变的 this.state.baz = 456; // Error: 你应该使用 this.setState() } } ``` ### 绝对的不可变 你甚至可以把索引签名标记为只读: ```ts interface Foo { readonly [x: number]: number; } // 使用 const foo: Foo = { 0: 123, 2: 345 }; console.log(foo[0]); // ok(读取) foo[0] = 456; // Error: 属性只读 ``` 如果你想以不变的方式使用原生 JavaScript 数组,可以使用 TypeScript 提供的 `ReadonlyArray` 接口: ```ts let foo: ReadonlyArray = [1, 2, 3]; console.log(foo[0]); // ok foo.push(4); // Error: ReadonlyArray 上不存在 `push`,因为他会改变数组 foo = foo.concat(4); // ok, 创建了一个复制 ``` ### 自动推断 在一些情况下,编译器能把一些特定的属性推断为 `readonly`,例如在一个 `class` 中,如果你有一个只含有 `getter` 但是没有 `setter` 的属性,他能被推断为只读: ```ts class Person { firstName: string = 'John'; lastName: string = 'Doe'; get fullName() { return this.firstName + this.lastName; } } const person = new Person(); console.log(person.fullName); // John Doe person.fullName = 'Dear Reader'; // Error, fullName 只读 ``` ## 与 `const` 的不同 `const` - 用于变量; - 变量不能重新赋值给其他任何事物。 `readonly` - 用于属性; - 用于别名,可以修改属性; 简单的例子 1: ```ts const foo = 123; // 变量 let bar: { readonly bar: number; // 属性 }; ``` 简单的例子 2: ```ts const foo: { readonly bar: number; } = { bar: 123 }; function iMutateFoo(foo: { bar: number }) { foo.bar = 456; } iMutateFoo(foo); console.log(foo.bar); // 456 ``` `readonly` 能确保“我”不能修改属性,但是当你把这个属性交给其他并没有这种保证的使用者(允许出于类型兼容性的原因),他们能改变它。当然,如果 `iMutateFoo` 明确的表示,他们的参数不可修改,那么编译器会发出错误警告: ```ts interface Foo { readonly bar: number; } let foo: Foo = { bar: 123 }; function iTakeFoo(foo: Foo) { foo.bar = 456; // Error: bar 属性只读 } iTakeFoo(foo); ``` ================================================ FILE: docs/typings/thisType.md ================================================ # ThisType 通过 `ThisType` 我们可以在对象字面量中键入 `this`,并提供通过上下文类型控制 `this` 类型的便捷方式。它只有在 `--noImplicitThis` 的选项下才有效。 现在,在对象字面量方法中的 `this` 类型,将由以下决定: - 如果这个方法显式指定了 `this` 参数,那么 `this` 具有该参数的类型。(下例子中 `bar`) - 否则,如果方法由带 `this` 参数的签名进行上下文键入,那么 `this` 具有该参数的类型。(下例子中 `foo`) - 否则,如果 `--noImplicitThis` 选项已经启用,并且对象字面量中包含由 `ThisType` 键入的上下文类型,那么 `this` 的类型为 `T`。 - 否则,如果 `--noImplicitThis` 选项已经启用,并且对象字面量中不包含由 `ThisType` 键入的上下文类型,那么 `this` 的类型为该上下文类型。 - 否则,如果 `--noImplicitThis` 选项已经启用,`this` 具有该对象字面量的类型。 - 否则,`this` 的类型为 `any`。 一些例子: ```ts // Compile with --noImplicitThis type Point = { x: number; y: number; moveBy(dx: number, dy: number): void; }; let p: Point = { x: 10, y: 20, moveBy(dx, dy) { this.x += dx; // this has type Point this.y += dy; // this has type Point } }; let foo = { x: 'hello', f(n: number) { this; // { x: string, f(n: number): void } } }; let bar = { x: 'hello', f(this: { message: string }) { this; // { message: string } } }; ``` 类似的方式,当使用 `--noImplicitThis` 时,函数表达式赋值给 `obj.xxx` 或者 `obj[xxx]` 的目标时,在函数中 `this` 的类型将会是 `obj`: ```ts // Compile with --noImplicitThis obj.f = function(n) { return this.x - n; // 'this' has same type as 'obj' }; obj['f'] = function(n) { return this.x - n; // 'this' has same type as 'obj' }; ``` 通过 API 转换参数的形式来生成 `this` 的值的情景下,可以通过创建一个新的 `ThisType` 标记接口,可用于在上下文中表明转换后的类型。尤其是当字面量中的上下文类型为 `ThisType` 或者是包含 `ThisType` 的交集时,显得尤为有效,对象字面量方法中 `this` 的类型即为 `T`。 ```ts // Compile with --noImplicitThis type ObjectDescriptor = { data?: D; methods?: M & ThisType; // Type of 'this' in methods is D & M }; function makeObject(desc: ObjectDescriptor): D & M { let data: object = desc.data || {}; let methods: object = desc.methods || {}; return { ...data, ...methods } as D & M; } let obj = makeObject({ data: { x: 0, y: 0 }, methods: { moveBy(dx: number, dy: number) { this.x += dx; // Strongly typed this this.y += dy; // Strongly typed this } } }); obj.x = 10; obj.y = 20; obj.moveBy(5, 5); ``` 在上面的例子中,`makeObject` 参数中的对象属性 `methods` 具有包含 `ThisType` 的上下文类型,因此对象中 `methods` 属性下的方法的 `this` 类型为 `{ x: number, y: number } & { moveBy(dx: number, dy: number): number }`。 `ThisType` 的接口,在 `lib.d.ts` 只是被声明为空的接口,除了可以在对象字面量上下文中可以被识别以外,该接口的作用等同于任意空接口。 ================================================ FILE: docs/typings/typeAssertion.md ================================================ # 类型断言 TypeScript 允许你覆盖它的推断,并且能以你任何你想要的方式分析它,这种机制被称为「类型断言」。TypeScript 类型断言用来告诉编译器你比它更了解这个类型,并且它不应该再发出错误。 类型断言的一个常见用例是当你从 JavaScript 迁移到 TypeScript 时: ```ts const foo = {}; foo.bar = 123; // Error: 'bar' 属性不存在于 ‘{}’ foo.bas = 'hello'; // Error: 'bas' 属性不存在于 '{}' ``` 这里的代码发出了错误警告,因为 `foo` 的类型推断为 `{}`,即没有属性的对象。因此,你不能在它的属性上添加 `bar` 或 `bas`,你可以通过类型断言来避免此问题: ```ts interface Foo { bar: number; bas: string; } const foo = {} as Foo; foo.bar = 123; foo.bas = 'hello'; ``` ## `as foo` 与 `` 最初的断言语法如下所示: ```ts let foo: any; let bar = foo; // 现在 bar 的类型是 'string' ``` 然而,当你在 JSX 中使用 `` 的断言语法时,这会与 JSX 的语法存在歧义: ```ts let foo = bar;; ``` 因此,为了一致性,我们建议你使用 `as foo` 的语法来为类型断言。 ## 类型断言与类型转换 它之所以不被称为「类型转换」,是因为转换通常意味着某种运行时的支持。但是,类型断言纯粹是一个编译时语法,同时,它也是一种为编译器提供关于如何分析代码的方法。 ## 类型断言被认为是有害的 在很多情景下,断言能让你更容易的从遗留项目中迁移(甚至将其他代码粘贴复制到你的项目中),然而,你应该小心谨慎的使用断言。让我们用最初的代码作为示例,如果你没有按约定添加属性,TypeScript 编译器并不会对此发出错误警告: ```ts interface Foo { bar: number; bas: string; } const foo = {} as Foo; // ahhh, 忘记了什么? ``` 另外一个常见的想法是使用类型断言来提供代码的提示: ```ts interface Foo { bar: number; bas: string; } const foo = { // 编译器将会提供关于 Foo 属性的代码提示 // 但是开发人员也很容易忘记添加所有的属性 // 同样,如果 Foo 被重构,这段代码也可能被破坏(例如,一个新的属性被添加)。 }; ``` 这也会存在一个同样的问题,如果你忘记了某个属性,编译器同样也不会发出错误警告。使用一种更好的方式: ```ts interface Foo { bar: number; bas: string; } const foo: Foo = { // 编译器将会提供 Foo 属性的代码提示 }; ``` 在某些情景下,你可能需要创建一个临时的变量,但至少,你不会使用一个承诺(可能是假的),而是依靠类型推断来检查你的代码。 ## 双重断言 类型断言,尽管我们已经证明了它并不是那么安全,但它也还是有用武之地。如下一个非常实用的例子所示,当使用者了解传入参数更具体的类型时,类型断言能按预期工作: ```ts function handler(event: Event) { const mouseEvent = event as MouseEvent; } ``` 然而,如下例子中的代码将会报错,尽管使用者已经使用了类型断言: ```ts function handler(event: Event) { const element = event as HTMLElement; // Error: 'Event' 和 'HTMLElement' 中的任何一个都不能赋值给另外一个 } ``` 如果你仍然想使用那个类型,你可以使用双重断言。首先断言成兼容所有类型的 `any`,编译器将不会报错: ```ts function handler(event: Event) { const element = (event as any) as HTMLElement; // ok } ``` ### TypeScript 是怎么确定单个断言是否足够 当 `S` 类型是 `T` 类型的子集,或者 `T` 类型是 `S` 类型的子集时,`S` 能被成功断言成 `T`。这是为了在进行类型断言时提供额外的安全性,完全毫无根据的断言是危险的,如果你想这么做,你可以使用 `any`。 ================================================ FILE: docs/typings/typeCompatibility.md ================================================ # 类型兼容性 类型兼容性用于确定一个类型是否能赋值给其他类型。 如 `string` 类型与 `number` 类型不兼容: ```ts let str: string = 'Hello'; let num: number = 123; str = num; // Error: 'number' 不能赋值给 'string' num = str; // Error: 'string' 不能赋值给 'number' ``` ## 安全性 TypeScript 类型系统设计比较方便,它允许你有一些不正确的行为。例如:任何类型都能被赋值给 `any`,这意味着告诉编译器你可以做任何你想做的事情: ```ts let foo: any = 123; foo = 'hello'; foo.toPrecision(3); ``` ## 结构化 TypeScript 对象是一种结构类型,这意味着只要结构匹配,名称也就无关紧要了: ```ts interface Point { x: number; y: number; } class Point2D { constructor(public x: number, public y: number) {} } let p: Point; // ok, 因为是结构化的类型 p = new Point2D(1, 2); ``` 这允许你动态创建对象(就好像你在 `vanilla JS` 中使用一样),并且它如果能被推断,该对象仍然具有安全性。 ```ts interface Point2D { x: number; y: number; } interface Point3D { x: number; y: number; z: number; } const point2D: Point2D = { x: 0, y: 10 }; const point3D: Point3D = { x: 0, y: 10, z: 20 }; function iTakePoint2D(point: Point2D) { /* do something */ } iTakePoint2D(point2D); // ok, 完全匹配 iTakePoint2D(point3D); // 额外的信息,没关系 iTakePoint2D({ x: 0 }); // Error: 没有 'y' ``` ## 变体 对类型兼容性来说,变体是一个利于理解和重要的概念。 对一个简单类型 `Base` 和 `Child` 来说,如果 `Child` 是 `Base` 的子类,`Child` 的实例能被赋值给 `Base` 类型的变量。 ::: tip 这是多态性。 ::: 在由 `Base` 和 `Child` 组合的复杂类型的类型兼容性中,它取决于相同场景下的 `Base` 与 `Child` 的变体: - 协变(Covariant):只在同一个方向; - 逆变(Contravariant):只在相反的方向; - 双向协变(Bivariant):包括同一个方向和不同方向; - 不变(Invariant):如果类型不完全相同,则它们是不兼容的。 ::: tip 对于存在完全可变数据的健全的类型系统(如 JavaScript),`Invariant` 是一个唯一的有效可选属性,但是如我们所讨论的,*便利性*迫使我们作出一些不是很安全的选择。 ::: 关于协变和逆变的更多内容,请参考:[协变与逆变](/tips/covarianceAndContravariance.html)。 ## 函数 当你在比较两个函数时,这有一些你需要考虑到的事情。 ### 返回类型 协变(Covariant):返回类型必须包含足够的数据。 ```ts interface Point2D { x: number; y: number; } interface Point3D { x: number; y: number; z: number; } let iMakePoint2D = (): Point2D => ({ x: 0, y: 0 }); let iMakePoint3D = (): Point3D => ({ x: 0, y: 0, z: 0 }); iMakePoint2D = iMakePoint3D; iMakePoint3D = iMakePoint2D; // ERROR: Point2D 不能赋值给 Point3D ``` ### 参数数量 更少的参数数量是好的(如:函数能够选择性的忽略一些多余的参数),但是你得保证有足够的参数被使用了: ```ts const iTakeSomethingAndPassItAnErr = (x: (err: Error, data: any) => void) => { /* 做一些其他的 */ }; iTakeSomethingAndPassItAnErr(() => null); // ok iTakeSomethingAndPassItAnErr(err => null); // ok iTakeSomethingAndPassItAnErr((err, data) => null); // ok // Error: 参数类型 `(err: any, data: any, more: any) => null` 不能赋值给参数类型 `(err: Error, data: any) => void` iTakeSomethingAndPassItAnErr((err, data, more) => null); ``` ### 可选的和 rest 参数 可选的(预先确定的)和 Rest 参数(任何数量的参数)都是兼容的: ```ts let foo = (x: number, y: number) => {}; let bar = (x?: number, y?: number) => {}; let bas = (...args: number[]) => {}; foo = bar = bas; bas = bar = foo; ``` ::: tip Note 可选的(上例子中的 `bar`)与不可选的(上例子中的 `foo`)仅在选项为 `strictNullChecks` 为 `false` 时兼容。 ::: ### 函数参数类型 双向协变(Bivariant):旨在支持常见的事件处理方案。 ```ts // 事件等级 interface Event { timestamp: number; } interface MouseEvent extends Event { x: number; y: number; } interface KeyEvent extends Event { keyCode: number; } // 简单的事件监听 enum EventType { Mouse, Keyboard } function addEventListener(eventType: EventType, handler: (n: Event) => void) { // ... } // 不安全,但是有用,常见。函数参数的比较是双向协变。 addEventListener(EventType.Mouse, (e: MouseEvent) => console.log(e.x + ',' + e.y)); // 在安全情景下的一种不好方案 addEventListener(EventType.Mouse, (e: Event) => console.log((e).x + ',' + (e).y)); addEventListener(EventType.Mouse, <(e: Event) => void>((e: MouseEvent) => console.log(e.x + ',' + e.y))); // 仍然不允许明确的错误,对完全不兼容的类型会强制检查 addEventListener(EventType.Mouse, (e: number) => console.log(e)); ``` 同样的,你也可以把 `Array` 赋值给 `Array` (协变),因为函数是兼容的。数组的协变需要所有的函数 `Array` 都能赋值给 `Array`,例如 `push(t: Child)` 能被赋值给 `push(t: Base)`,这都可以通过函数参数双向协变实现。 下面的代码对于其他语言的开发者来说,可能会感到很困惑,因为他们认为是有错误的,可是 Typescript 并不会报错: ```ts interface Point2D { x: number; y: number; } interface Point3D { x: number; y: number; z: number; } let iTakePoint2D = (point: Point2D) => {}; let iTakePoint3D = (point: Point3D) => {}; iTakePoint3D = iTakePoint2D; // ok, 这是合理的 iTakePoint2D = iTakePoint3D; // ok,为什么? ``` ## 枚举 - 枚举与数字类型相互兼容 ```ts enum Status { Ready, Waiting } let status = Status.Ready; let num = 0; status = num; num = status; ``` - 来自于不同枚举的枚举变量,被认为是不兼容的: ```ts enum Status { Ready, Waiting } enum Color { Red, Blue, Green } let status = Status.Ready; let color = Color.Red; status = color; // Error ``` ## 类 - 仅仅只有实例成员和方法会相比较,构造函数和静态成员不会被检查。 ```ts class Animal { feet: number; constructor(name: string, numFeet: number) {} } class Size { feet: number; constructor(meters: number) {} } let a: Animal; let s: Size; a = s; // OK s = a; // OK ``` - 私有的和受保护的成员必须来自于相同的类。 ```ts class Animal { protected feet: number; } class Cat extends Animal {} let animal: Animal; let cat: Cat; animal = cat; // ok cat = animal; // ok class Size { protected feet: number; } let size: Size; animal = size; // ERROR size = animal; // ERROR ``` ## 泛型 TypeScript 类型系统基于变量的结构,仅当类型参数在被一个成员使用时,才会影响兼容性。如下例子中,`T` 对兼容性没有影响: ```ts interface Empty {} let x: Empty; let y: Empty; x = y; // ok ``` 当 `T` 被成员使用时,它将在实例化泛型后影响兼容性: ```ts interface Empty { data: T; } let x: Empty; let y: Empty; x = y; // Error ``` 如果尚未实例化泛型参数,则在检查兼容性之前将其替换为 `any`: ```ts let identity = function(x: T): T { // ... }; let reverse = function(y: U): U { // ... }; identity = reverse; // ok, 因为 `(x: any) => any` 匹配 `(y: any) => any` ``` 类中的泛型兼容性与前文所提及一致: ```ts class List { add(val: T) {} } class Animal { name: string; } class Cat extends Animal { meow() { // .. } } const animals = new List(); animals.add(new Animal()); // ok animals.add(new Cat()); // ok const cats = new List(); cats.add(new Animal()); // Error cats.add(new Cat()); // ok ``` ## 脚注:不变性(Invariance) 我们说过,不变性可能是唯一一个听起来合理的选项,这里有一个关于 `contra` 和 `co` 的变体,被认为对数组是不安全的。 ```ts class Animal { constructor(public name: string) {} } class Cat extends Animal { meow() { console.log('cat'); } } let animal = new Animal('animal'); let cat = new Cat('cat'); // 多态 // Animal <= Cat animal = cat; // ok cat = animal; // ERROR: cat 继承于 animal // 演示每个数组形式 let animalArr: Animal[] = [animal]; let catArr: Cat[] = [cat]; // 明显的坏处,逆变 // Animal <= Cat // Animal[] >= Cat[] catArr = animalArr; // ok, 如有有逆变 catArr[0].meow(); // 允许,但是会在运行时报错 // 另外一个坏处,协变 // Animal <= Cat // Animal[] <= Cat[] animalArr = catArr; // ok,协变 animalArr.push(new Animal('another animal')); // 仅仅是 push 一个 animal 至 carArr 里 catArr.forEach(c => c.meow()); // 允许,但是会在运行时报错。 ``` ================================================ FILE: docs/typings/typeGuard.md ================================================ # 类型保护 类型保护允许你使用更小范围下的对象类型。 ## typeof TypeScript 熟知 JavaScript 中 `instanceof` 和 `typeof` 运算符的用法。如果你在一个条件块中使用这些,TypeScript 将会推导出在条件块中的的变量类型。如下例所示,TypeScript 将会辨别 `string` 上是否存在特定的函数,以及是否发生了拼写错误: ```ts function doSome(x: number | string) { if (typeof x === 'string') { // 在这个块中,TypeScript 知道 `x` 的类型必须是 `string` console.log(x.subtr(1)); // Error: 'subtr' 方法并没有存在于 `string` 上 console.log(x.substr(1)); // ok } x.substr(1); // Error: 无法保证 `x` 是 `string` 类型 } ``` ## instanceof 这有一个关于 `class` 和 `instanceof` 的例子: ```ts class Foo { foo = 123; common = '123'; } class Bar { bar = 123; common = '123'; } function doStuff(arg: Foo | Bar) { if (arg instanceof Foo) { console.log(arg.foo); // ok console.log(arg.bar); // Error } if (arg instanceof Bar) { console.log(arg.foo); // Error console.log(arg.bar); // ok } } doStuff(new Foo()); doStuff(new Bar()); ``` TypeScript 甚至能够理解 `else`。当你使用 `if` 来缩小类型时,TypeScript 知道在其他块中的类型并不是 `if` 中的类型: ```ts class Foo { foo = 123; } class Bar { bar = 123; } function doStuff(arg: Foo | Bar) { if (arg instanceof Foo) { console.log(arg.foo); // ok console.log(arg.bar); // Error } else { // 这个块中,一定是 'Bar' console.log(arg.foo); // Error console.log(arg.bar); // ok } } doStuff(new Foo()); doStuff(new Bar()); ``` ## in `in` 操作符可以安全的检查一个对象上是否存在一个属性,它通常也被作为类型保护使用: ```ts interface A { x: number; } interface B { y: string; } function doStuff(q: A | B) { if ('x' in q) { // q: A } else { // q: B } } ``` ## 字面量类型保护 当你在联合类型里使用字面量类型时,你可以检查它们是否有区别: ```ts type Foo = { kind: 'foo'; // 字面量类型 foo: number; }; type Bar = { kind: 'bar'; // 字面量类型 bar: number; }; function doStuff(arg: Foo | Bar) { if (arg.kind === 'foo') { console.log(arg.foo); // ok console.log(arg.bar); // Error } else { // 一定是 Bar console.log(arg.foo); // Error console.log(arg.bar); // ok } } ``` ## 使用定义的类型保护 JavaScript 并没有内置非常丰富的、运行时的自我检查机制。当你在使用普通的 JavaScript 对象时(使用结构类型,更有益处),你甚至无法访问 `instanceof` 和 `typeof`。在这种情景下,你可以创建*用户自定义的类型保护函数*,这仅仅是一个返回值为类似于`someArgumentName is SomeType` 的函数,如下: ```ts // 仅仅是一个 interface interface Foo { foo: number; common: string; } interface Bar { bar: number; common: string; } // 用户自己定义的类型保护! function isFoo(arg: Foo | Bar): arg is Foo { return (arg as Foo).foo !== undefined; } // 用户自己定义的类型保护使用用例: function doStuff(arg: Foo | Bar) { if (isFoo(arg)) { console.log(arg.foo); // ok console.log(arg.bar); // Error } else { console.log(arg.foo); // Error console.log(arg.bar); // ok } } doStuff({ foo: 123, common: '123' }); doStuff({ bar: 123, common: '123' }); ``` ================================================ FILE: docs/typings/typeInference.md ================================================ # 类型推断 TypeScript 能根据一些简单的规则推断(检查)变量的类型,你可以通过实践,很快的了解它们。 ## 定义变量 变量的类型,由定义推断: ```ts let foo = 123; // foo 是 'number' let bar = 'hello'; // bar 是 'string' foo = bar; // Error: 不能将 'string' 赋值给 `number` ``` 这是一个从右向左流动类型的示例。 ## 函数返回类型 返回类型能被 `return` 语句推断,如下所示,推断函数返回为一个数字: ```ts function add(a: number, b: number) { return a + b; } ``` 这是一个从底部流出类型的例子。 ## 赋值 函数参数类型/返回值也能通过赋值来推断。如下所示,`foo` 的类型是 `Adder`,他能让 `foo` 的参数 `a`、`b` 是 `number` 类型。 ```ts type Adder = (a: number, b: number) => number; let foo: Adder = (a, b) => a + b; ``` 这个事实可以用下面的代码来证明,TypeScript 会发出正如你期望发出的错误警告: ```ts type Adder = (a: number, b: number) => number; let foo: Adder = (a, b) => { a = 'hello'; // Error:不能把 'string' 类型赋值给 'number' 类型 return a + b; }; ``` 这是一个从左向右流动类型的示例。 如果你创建一个函数,并且函数参数为一个回调函数,相同的赋值规则也适用于它。从 `argument` 至 `parameter` 只是变量赋值的另一种形式。 ```ts type Adder = (a: number, b: number) => number; function iTakeAnAdder(adder: Adder) { return adder(1, 2); } iTakeAnAdder((a, b) => { a = 'hello'; // Error: 不能把 'string' 类型赋值给 'number' 类型 return a + b; }); ``` ## 结构化 这些简单的规则也适用于结构化的存在(对象字面量),例如在下面这种情况下 `foo` 的类型被推断为 `{ a: number, b: number }`: ```ts const foo = { a: 123, b: 456 }; foo.a = 'hello'; // Error:不能把 'string' 类型赋值给 'number' 类型 ``` 数组也一样: ```ts const bar = [1, 2, 3]; bar[0] = 'hello'; // Error:不能把 'string' 类型赋值给 'number' 类型 ``` ## 解构 这些也适用于解构中: ```ts const foo = { a: 123, b: 456 }; let { a } = foo; a = 'hello'; // Error:不能把 'string' 类型赋值给 'number' 类型 ``` 数组中: ```ts const bar = [1, 2]; let [a, b] = bar; a = 'hello'; // Error:不能把 'string' 类型赋值给 'number' 类型 ``` 如果函数参数能够被推断出来,那么解构亦是如此。在如下例子中,函数参数能够被解构为 `a/b` 成员: ```ts type Adder = (number: { a: number; b: number }) => number; function iTakeAnAdder(adder: Adder) { return adder({ a: 1, b: 2 }); } iTakeAnAdder(({ a, b }) => { // a, b 的类型能被推断出来 a = 'hello'; // Error:不能把 'string' 类型赋值给 'number' 类型 return a + b; }); ``` ## 类型保护 在前面章节[类型保护](./typeGuard.md)中,我们已经知道它如何帮助我们改变和缩小类型范围(特别是在联合类型下)。类型保护只是一个块中变量另一种推断形式。 ## 警告 ### 小心使用参数 如果类型不能被赋值推断出来,类型也将不会流入函数参数中。例如如下的一个例子,编译器并不知道 `foo` 的类型,所它也就不能推断出 `a` 或者 `b` 的类型。 ```ts const foo = (a, b) => { /* do something */ }; ``` 然而,如果 `foo` 添加了类型注解,函数参数也就能被推断(`a`,`b` 都能被推断为 `number` 类型): ```ts type TwoNumberFunction = (a: number, b: number) => void; const foo: TwoNumberFunction = (a, b) => { /* do something */ }; ``` ### 小心使用返回值 尽管 TypeScript 一般情况下能推断函数的返回值,但是它可能并不是你想要的。例如如下的 `foo` 函数,它的返回值为 `any`: ```ts function foo(a: number, b: number) { return a + addOne(b); } // 一些使用 JavaScript 库的特殊函数 function addOne(a) { return a + 1; } ``` 这是因为返回值的类型被一个缺少类型定义的 `addOne` 函数所影响(`a` 是 `any`,所以 `addOne` 返回值为 `any`,`foo` 的返回值是也是 `any`)。 ::: tip 我发现最简单的方式是明确的写上函数返回值,毕竟这些注解是一个定理,而函数是注解的一个证据。 ::: 这里还有一些其他可以想象的情景,但是有一个好消息是有编译器选项 `noImplicitAny` 可以捕获这些 bug。 ### `noImplicitAny` 选项 `noImplicitAny` 用来告诉编译器,当无法推断一个变量时发出一个错误(或者只能推断为一个隐式的 `any` 类型),你可以: - 通过显式添加 `:any` 的类型注解,来让它成为一个 `any` 类型; - 通过一些更正确的类型注解来帮助 TypeScript 推断类型。 ================================================ FILE: docs/typings/types.md ================================================ # @types 毫无疑问,[DefinitelyTyped](https://github.com/borisyankov/DefinitelyTyped) 是 TypeScript 最大的优势之一,社区已经记录了 90% 的顶级 JavaScript 库。 这意味着,你可以非常高效地使用这些库,而无须在单独的窗口打开相应文档以确保输入的正确性。 ## 使用 `@types` 你可以通过 `npm` 来安装使用 `@types`,例如为 `jquery` 添加声明文件: ```shell npm install @types/jquery --save-dev ``` `@types` 支持全局和模块类型定义。 ### 全局 `@types` 默认情况下,TypeScript 会自动包含支持全局使用的任何声明定义。例如,对于 jquery,你应该能够在项目中开始全局使用 `$`。 ### 模块 `@types` 安装完之后,不需要特别的配置,你就可以像使用模块一样使用它: ```ts import * as $ from 'jquery'; // 现在你可以此模块中任意使用$了 :) ``` ## 控制全局 可以看出,对于某些团队而言,拥有允许全局使用的定义是一个问题。因此,你可以通过配置 `tsconfig.json` 的 `compilerOptions.types` 选项,引入有意义的类型: ```ts { "compilerOptions": { "types" : [ "jquery" ] } } ``` 如上例所示,通过配置 `compilerOptions.types: [ "jquery" ]` 后,只允许使用 `jquery` 的 `@types` 包,即使这个人安装了另一个声明文件,比如 `npm install @types/node`,它的全局变量(例如 `process`)也不会泄漏到你的代码中,直到你将它们添加到 tsconfig.json 类型选项。 ================================================ FILE: package.json ================================================ { "name": "typescript-book-chinese", "version": "1.0.0", "description": "typescript-book-chinese", "main": "index.js", "directories": { "doc": "docs" }, "scripts": { "dev": "vuepress dev docs", "build": "vuepress build docs", "precommit": "lint-staged", "contributor:add": "all-contributors add", "contributor:generate": "all-contributors generate" }, "lint-staged": { "linters": { "*.{js,json,css,md}": [ "prettier --write", "git add" ] }, "ignore": [] }, "repository": { "type": "git", "url": "git+https://github.com/jkchao/typescript-book-chinese.git" }, "keywords": [ "typescript", "vue" ], "author": "jkchao", "license": "ISC", "bugs": { "url": "https://github.com/jkchao/typescript-book-chinese/issues" }, "homepage": "https://github.com/jkchao/typescript-book-chinese#readme", "devDependencies": { "@commitlint/cli": "^7.5.2", "@commitlint/config-angular": "^7.5.0", "@vuepress/plugin-back-to-top": "^1.0.0-alpha.44", "@vuepress/plugin-container": "^1.0.0-alpha.44", "@vuepress/plugin-google-analytics": "^1.0.0-alpha.44", "@vuepress/plugin-pwa": "^1.0.0-alpha.44", "all-contributors-cli": "^5.4.0", "eslint": "^5.6.1", "husky": "^1.3.1", "lint-staged": "^8.1.5", "prettier": "1.14.2", "vuepress": "^1.0.0-alpha.44" }, "dependencies": {} }