[{"data":1,"prerenderedAt":12895},["ShallowReactive",2],{"\u002Ftutorials\u002Fts":3,"tutorials-search\u002Ftutorials\u002Fts":12416},{"id":4,"title":5,"body":6,"category":86,"cover":12406,"date":12407,"description":12408,"extension":12409,"meta":12410,"navigation":566,"path":12411,"seo":12412,"sitemap":12413,"stem":12414,"__hash__":12415},"tutorials\u002Ftutorials\u002Fts.md","TypeScript 教程：从类型标注到工程化",{"type":7,"value":8,"toc":12198},"minimark",[9,14,27,32,44,75,81,149,165,169,200,203,242,246,253,329,336,339,347,365,368,371,494,523,526,529,614,627,633,643,711,717,733,782,788,794,797,808,829,843,846,886,889,903,1042,1053,1061,1071,1154,1178,1239,1245,1263,1321,1330,1336,1344,1400,1514,1548,1554,1563,1665,1677,1689,1694,1760,1803,1808,1817,1825,1829,1836,1944,1947,1955,1958,2042,2054,2136,2143,2152,2278,2287,2292,2351,2366,2370,2373,2463,2470,2473,2476,2632,2639,2643,2650,2658,2735,2746,2760,2768,2853,2865,2869,2873,2884,3118,3161,3168,3190,3327,3355,3362,3370,3373,3382,3498,3505,3508,3511,3618,3624,3627,3633,3748,3758,3762,3766,3772,3891,3898,3904,3933,4041,4050,4058,4090,4094,4104,4108,4194,4213,4219,4255,4261,4266,4379,4384,4389,4407,4476,4479,4507,4632,4646,4654,4667,4812,4827,4834,4841,4957,4960,4965,4968,4987,4999,5006,5013,5089,5115,5127,5134,5148,5197,5200,5204,5210,5227,5295,5321,5327,5339,5419,5425,5431,5440,5554,5585,5592,5604,5607,5615,5672,5679,5760,5773,5776,5801,5813,5817,5823,5902,5916,5923,5933,6008,6011,6017,6027,6084,6087,6091,6111,6123,6130,6315,6332,6336,6342,6402,6409,6416,6428,6565,6571,6576,6650,6656,6663,6674,6766,6785,6795,6798,6869,6878,6882,6895,6985,6994,7000,7010,7017,7022,7129,7145,7274,7297,7301,7308,7316,7455,7461,7464,7470,7565,7568,7572,7592,7599,7609,7612,7615,7859,7875,7878,7881,7998,8013,8019,8025,8181,8193,8199,8207,8261,8280,8284,8287,8394,8415,8419,8426,8431,8443,8455,8732,8741,8750,8759,8961,8976,8988,8995,9205,9217,9228,9231,9451,9460,9467,9479,9594,9604,9611,9616,9722,9732,9735,9745,9752,9759,9763,9769,9842,9881,9887,9899,10032,10037,10041,10048,10220,10232,10235,10245,10255,10275,10281,10295,10334,10356,10362,10383,10456,10464,10469,10544,10563,10573,10576,10611,10624,10627,10637,10675,10678,10682,10723,10727,10735,10738,10779,10805,10846,10854,10857,10919,10939,10945,10955,11065,11089,11098,11150,11156,11191,11198,11205,11225,11241,11245,11254,11258,11262,11265,11362,11378,11384,11397,11429,11435,11439,11460,11467,11501,11505,11512,11565,11575,11581,11619,11626,11639,11643,11672,11682,11716,11720,11727,11732,11736,11960,11963,11967,11985,12001,12005,12023,12027,12033,12147,12175,12178,12194],[10,11,13],"h2",{"id":12},"一typescript-是什么类型检查与转译是两件事","一、TypeScript 是什么：类型检查与转译是两件事",[15,16,17,18,22,23,26],"p",{},"先纠正一句最流行的口号。人们说「TypeScript 是 JavaScript 的超集」，这话对了一半：",[19,20,21],"strong",{},"语法上","它确实是超集——任何合法的 JS 都是合法的 TS，你只是在 JS 之上加了一层可以写的类型标注。但 TS 不是「多了一套新语法能做的事」，运行时它",[19,24,25],{},"不提供任何 JS 没有的能力","。它做的事情只有一件额外的：在你运行代码之前，多问一句「这个类型对得上吗」。",[28,29,31],"h3",{"id":30},"tsc-到底干了两件事","tsc 到底干了两件事",[15,33,34,35,39,40,43],{},"命令行里敲 ",[36,37,38],"code",{},"tsc","，它其实同时在做两件",[19,41,42],{},"互相独立","的事：",[45,46,47,58],"ol",{},[48,49,50,53,54,57],"li",{},[19,51,52],{},"类型检查","：读你的 ",[36,55,56],{},".ts","，对照类型规则挑毛病，有问题就打印错误。",[48,59,60,63,64,66,67,70,71,74],{},[19,61,62],{},"转译（emit）","：把 ",[36,65,56],{}," 里的类型全部抹掉，再按 ",[36,68,69],{},"target"," 把新语法降级，输出成一份 ",[36,72,73],{},".js","。",[15,76,77,78,74],{},"关键认知是：",[19,79,80],{},"这两件事的结论彼此不影响",[82,83,88],"pre",{"className":84,"code":85,"language":86,"meta":87,"style":87},"language-ts shiki shiki-themes github-dark","\u002F\u002F 这份代码有类型错误，但它照样会被转译出 .js\nlet n: number = 'hello'   \u002F\u002F tsc 报错：Type 'string' is not assignable to type 'number'\nconsole.log(n.toUpperCase())  \u002F\u002F 运行时 n 其实是字符串，这行反而能跑\n","ts","",[36,89,90,99,127],{"__ignoreMap":87},[91,92,95],"span",{"class":93,"line":94},"line",1,[91,96,98],{"class":97},"sAwPA","\u002F\u002F 这份代码有类型错误，但它照样会被转译出 .js\n",[91,100,102,106,110,113,117,120,124],{"class":93,"line":101},2,[91,103,105],{"class":104},"snl16","let",[91,107,109],{"class":108},"s95oV"," n",[91,111,112],{"class":104},":",[91,114,116],{"class":115},"sDLfK"," number",[91,118,119],{"class":104}," =",[91,121,123],{"class":122},"sU2Wk"," 'hello'",[91,125,126],{"class":97},"   \u002F\u002F tsc 报错：Type 'string' is not assignable to type 'number'\n",[91,128,130,133,137,140,143,146],{"class":93,"line":129},3,[91,131,132],{"class":108},"console.",[91,134,136],{"class":135},"svObZ","log",[91,138,139],{"class":108},"(n.",[91,141,142],{"class":135},"toUpperCase",[91,144,145],{"class":108},"())  ",[91,147,148],{"class":97},"\u002F\u002F 运行时 n 其实是字符串，这行反而能跑\n",[15,150,151,153,154,156,157,160,161,164],{},[36,152,38],{}," 会一边打印那条错误、一边把 ",[36,155,73],{}," 生成出来（除非你开了 ",[36,158,159],{},"noEmitOnError","）。也就是说",[19,162,163],{},"类型报错拦不住转译","——这跟编译型语言「语法不过就产不出二进制」的直觉完全不同。理解了这点，后面很多「为什么有红波浪线还能跑起来」的困惑就都通了。",[28,166,168],{"id":167},"为什么运行时不需要-typescript","为什么运行时不需要 TypeScript",[15,170,171,172,175,176,178,179,182,183,186,187,186,190,186,193,186,196,199],{},"浏览器和 Node ",[19,173,174],{},"从来不认识 TypeScript","。你发出去的、被执行的，永远是类型被删光之后的 JS。类型只活在你写代码和 ",[36,177,38],{},"\u002F构建工具检查的那几分钟里，从不出现在最终的产物中。这条机制有个正式名字：",[19,180,181],{},"类型擦除（type erasure）","——转译时所有 ",[36,184,185],{},": number","、",[36,188,189],{},"interface",[36,191,192],{},"\u003CT>",[36,194,195],{},"as",[36,197,198],{},"!"," 都会被原样撕掉，留下的就是等价 JS。",[15,201,202],{},"由此推出三条实践后果，值得现在就记住：",[204,205,206,218,236],"ul",{},[48,207,208,211,212,214,215,217],{},[19,209,210],{},"类型错了不等于程序出错","：",[36,213,38],{}," 报红，程序可能照跑；",[36,216,38],{}," 全绿，程序也可能崩——因为类型只保证「标注自洽」，不保证「运行时真实」。",[48,219,220,223,224,227,228,231,232,235],{},[19,221,222],{},"泛型、接口、类型别名在运行时不存在","：你不能 ",[36,225,226],{},"if (x is User)"," 里靠 ",[36,229,230],{},"User"," 这个 type 判断，type 是编译期的，",[36,233,234],{},"instanceof"," 只认 class 这种运行时的东西（第七章展开）。",[48,237,238,241],{},[19,239,240],{},"别指望类型能在运行时保护你","：外部数据（接口返回、用户输入）永远是「未经检验」的，类型标注只是你对编译器许的愿，编译器不会替你去核实网络回来的 JSON（第十五、十七章会反复强调）。",[28,243,245],{"id":244},"那-ts-到底图什么","那 TS 到底图什么",[15,247,248,249,252],{},"既然运行时无非就是 JS，为什么要多这一层？答案全在",[19,250,251],{},"写代码的那段时间","里：",[254,255,256,269],"table",{},[257,258,259],"thead",{},[260,261,262,266],"tr",{},[263,264,265],"th",{},"你得到的",[263,267,268],{},"从哪来",[270,271,272,285,301,309,317],"tbody",{},[260,273,274,278],{},[275,276,277],"td",{},"自动补全",[275,279,280,281,284],{},"编译器知道 ",[36,282,283],{},"user."," 后面能接什么",[260,286,287,290],{},[275,288,289],{},"提前暴露错误",[275,291,292,293,296,297,300],{},"拼错属性名、少传参数、",[36,294,295],{},"null"," 直接 ",[36,298,299],{},".map","，写的时候就红",[260,302,303,306],{},[275,304,305],{},"安全重构",[275,307,308],{},"改一个字段名，所有用到它的地方全变红，一个不漏",[260,310,311,314],{},[275,312,313],{},"类型即文档",[275,315,316],{},"函数签名就是最准的说明书，不会像注释那样过期",[260,318,319,322],{},[275,320,321],{},"接住第三方库",[275,323,324,325,328],{},"有 ",[36,326,327],{},".d.ts"," 声明的库，你在编辑器里就能看见它的全部 API",[15,330,331,332,335],{},"一句话：",[19,333,334],{},"TS 把「运行时才炸的 bug」往前挪到「编辑时就红」","。它买的是确定性，不是性能，也不是新能力。",[15,337,338],{},"本章没有示例可点，因为这一章讲的是「转译」这个概念本身，而转译发生在构建阶段。从第二章起，凡是「点得动、看得出区别」的主题都会配可运行示例。",[10,340,342,343,346],{"id":341},"二第一个类型错误注解语法-t-与类型推断","二、第一个类型错误：注解语法 ",[36,344,345],{},": T"," 与类型推断",[15,348,349,350,352,353,356,357,360,361,364],{},"TS 最核心的动作就一个：给一个值「说出它的类型」。写 ",[36,351,345],{}," 叫",[19,354,355],{},"类型注解（annotation）","，但你会发现——",[19,358,359],{},"大多数时候你根本不写它","，因为 TS 会自己猜，这个猜叫",[19,362,363],{},"类型推断（inference）","。这门语言的体验好不好，八成取决于你会不会跟推断打交道。",[28,366,367],{"id":367},"注解语法长什么样",[15,369,370],{},"类型跟在冒号后面，出现在四个高频位置：",[82,372,374],{"className":84,"code":373,"language":86,"meta":87,"style":87},"let count: number = 0            \u002F\u002F 变量：值前面\nfunction add(a: number, b: number): number {   \u002F\u002F 参数：名字后面；返回值：括号后面\n  return a + b\n}\nconst users: string[] = ['a', 'b']  \u002F\u002F 数组：元素类型后面加 []\n",[36,375,376,395,437,451,457],{"__ignoreMap":87},[91,377,378,380,383,385,387,389,392],{"class":93,"line":94},[91,379,105],{"class":104},[91,381,382],{"class":108}," count",[91,384,112],{"class":104},[91,386,116],{"class":115},[91,388,119],{"class":104},[91,390,391],{"class":115}," 0",[91,393,394],{"class":97},"            \u002F\u002F 变量：值前面\n",[91,396,397,400,403,406,410,412,414,417,420,422,424,427,429,431,434],{"class":93,"line":101},[91,398,399],{"class":104},"function",[91,401,402],{"class":135}," add",[91,404,405],{"class":108},"(",[91,407,409],{"class":408},"s9osk","a",[91,411,112],{"class":104},[91,413,116],{"class":115},[91,415,416],{"class":108},", ",[91,418,419],{"class":408},"b",[91,421,112],{"class":104},[91,423,116],{"class":115},[91,425,426],{"class":108},")",[91,428,112],{"class":104},[91,430,116],{"class":115},[91,432,433],{"class":108}," {   ",[91,435,436],{"class":97},"\u002F\u002F 参数：名字后面；返回值：括号后面\n",[91,438,439,442,445,448],{"class":93,"line":129},[91,440,441],{"class":104},"  return",[91,443,444],{"class":108}," a ",[91,446,447],{"class":104},"+",[91,449,450],{"class":108}," b\n",[91,452,454],{"class":93,"line":453},4,[91,455,456],{"class":108},"}\n",[91,458,460,463,466,468,471,474,477,480,483,485,488,491],{"class":93,"line":459},5,[91,461,462],{"class":104},"const",[91,464,465],{"class":115}," users",[91,467,112],{"class":104},[91,469,470],{"class":115}," string",[91,472,473],{"class":108},"[] ",[91,475,476],{"class":104},"=",[91,478,479],{"class":108}," [",[91,481,482],{"class":122},"'a'",[91,484,416],{"class":108},[91,486,487],{"class":122},"'b'",[91,489,490],{"class":108},"]  ",[91,492,493],{"class":97},"\u002F\u002F 数组：元素类型后面加 []\n",[15,495,496,497,500,501,504,505,507,508,510,511,514,515,518,519,522],{},"参数必须",[19,498,499],{},"逐个","标注，",[36,502,503],{},"function add(a, b: number)"," 里的 ",[36,506,409],{}," 不会自动继承 ",[36,509,419],{}," 的类型，它会退回隐式 ",[36,512,513],{},"any","（",[36,516,517],{},"noImplicitAny"," 开着的严格模式下这里就会报错）。返回值可以不写让编译器推断，但",[19,520,521],{},"导出的公共函数最好显式写","——这样改内部实现时调用方不受影响，接口是一份契约。",[28,524,525],{"id":525},"为什么有推断你还写注解",[15,527,528],{},"因为推断是有方向、有极限的，注解是你对编译器的一次「纠偏」：",[82,530,532],{"className":84,"code":531,"language":86,"meta":87,"style":87},"let x = 'hello'      \u002F\u002F 推断成 string（注意：不是字面量类型，见第五章）\nconst y = 'hello'    \u002F\u002F 推断成 'hello'（字面量窄化，const 更精确）\n\nfunction parse(input: string) {   \u002F\u002F 参数不标就报错，所以必须写\n  return JSON.parse(input)        \u002F\u002F 返回值被推断成 any（JSON.parse 的返回是 any）\n}\n",[36,533,534,548,562,568,590,609],{"__ignoreMap":87},[91,535,536,538,541,543,545],{"class":93,"line":94},[91,537,105],{"class":104},[91,539,540],{"class":108}," x ",[91,542,476],{"class":104},[91,544,123],{"class":122},[91,546,547],{"class":97},"      \u002F\u002F 推断成 string（注意：不是字面量类型，见第五章）\n",[91,549,550,552,555,557,559],{"class":93,"line":101},[91,551,462],{"class":104},[91,553,554],{"class":115}," y",[91,556,119],{"class":104},[91,558,123],{"class":122},[91,560,561],{"class":97},"    \u002F\u002F 推断成 'hello'（字面量窄化，const 更精确）\n",[91,563,564],{"class":93,"line":129},[91,565,567],{"emptyLinePlaceholder":566},true,"\n",[91,569,570,572,575,577,580,582,584,587],{"class":93,"line":453},[91,571,399],{"class":104},[91,573,574],{"class":135}," parse",[91,576,405],{"class":108},[91,578,579],{"class":408},"input",[91,581,112],{"class":104},[91,583,470],{"class":115},[91,585,586],{"class":108},") {   ",[91,588,589],{"class":97},"\u002F\u002F 参数不标就报错，所以必须写\n",[91,591,592,594,597,600,603,606],{"class":93,"line":459},[91,593,441],{"class":104},[91,595,596],{"class":115}," JSON",[91,598,599],{"class":108},".",[91,601,602],{"class":135},"parse",[91,604,605],{"class":108},"(input)        ",[91,607,608],{"class":97},"\u002F\u002F 返回值被推断成 any（JSON.parse 的返回是 any）\n",[91,610,612],{"class":93,"line":611},6,[91,613,456],{"class":108},[15,615,616,619,620,622,623,626],{},[36,617,618],{},"JSON.parse"," 返回 ",[36,621,513],{},"——这就是「any 顺着返回值泄漏」的源头之一（第十七章专门讲）。你在这里给它补一个返回注解 ",[36,624,625],{},": User","，就把不确定性挡在了函数边界内。",[28,628,630,632],{"id":629},"let-推断成联合类型",[36,631,105],{}," 推断成联合类型",[15,634,635,636,638,639,642],{},"TS 对 ",[36,637,105],{}," 有个聪明的默认：如果声明时给了初值、之后又可能改成别的类型，它会推断成一个",[19,640,641],{},"能容纳所有写入值的联合类型","，而不是死盯初值：",[82,644,646],{"className":84,"code":645,"language":86,"meta":87,"style":87},"let id = 1          \u002F\u002F 推断 number；随后 id = 'a' 会报错\nlet flag = true     \u002F\u002F 推断 boolean\n\n\u002F\u002F 但若你压根没给初值，它就退化成 any 或需要注解\nlet name            \u002F\u002F 无初值 + 严格模式：隐式 any，报错\nlet name2: string   \u002F\u002F 补注解即可\n",[36,647,648,663,678,682,687,697],{"__ignoreMap":87},[91,649,650,652,655,657,660],{"class":93,"line":94},[91,651,105],{"class":104},[91,653,654],{"class":108}," id ",[91,656,476],{"class":104},[91,658,659],{"class":115}," 1",[91,661,662],{"class":97},"          \u002F\u002F 推断 number；随后 id = 'a' 会报错\n",[91,664,665,667,670,672,675],{"class":93,"line":101},[91,666,105],{"class":104},[91,668,669],{"class":108}," flag ",[91,671,476],{"class":104},[91,673,674],{"class":115}," true",[91,676,677],{"class":97},"     \u002F\u002F 推断 boolean\n",[91,679,680],{"class":93,"line":129},[91,681,567],{"emptyLinePlaceholder":566},[91,683,684],{"class":93,"line":453},[91,685,686],{"class":97},"\u002F\u002F 但若你压根没给初值，它就退化成 any 或需要注解\n",[91,688,689,691,694],{"class":93,"line":459},[91,690,105],{"class":104},[91,692,693],{"class":108}," name            ",[91,695,696],{"class":97},"\u002F\u002F 无初值 + 严格模式：隐式 any，报错\n",[91,698,699,701,704,706,708],{"class":93,"line":611},[91,700,105],{"class":104},[91,702,703],{"class":108}," name2",[91,705,112],{"class":104},[91,707,470],{"class":115},[91,709,710],{"class":97},"   \u002F\u002F 补注解即可\n",[28,712,714,716],{"id":713},"const-的字面量窄化",[36,715,462],{}," 的字面量窄化",[15,718,719,721,722,725,726,729,730,211],{},[36,720,462],{}," 因为不能再改，编译器就敢把它的类型收成",[19,723,724],{},"那个具体的字面量","，而不是宽泛的 ",[36,727,728],{},"string","\u002F",[36,731,732],{},"number",[82,734,736],{"className":84,"code":735,"language":86,"meta":87,"style":87},"const dir = 'up'      \u002F\u002F 类型是 'up'（字面量类型），不是 string\nlet dir2 = 'up'       \u002F\u002F 类型是 string\nconst num = 42        \u002F\u002F 类型是 42\n",[36,737,738,753,767],{"__ignoreMap":87},[91,739,740,742,745,747,750],{"class":93,"line":94},[91,741,462],{"class":104},[91,743,744],{"class":115}," dir",[91,746,119],{"class":104},[91,748,749],{"class":122}," 'up'",[91,751,752],{"class":97},"      \u002F\u002F 类型是 'up'（字面量类型），不是 string\n",[91,754,755,757,760,762,764],{"class":93,"line":101},[91,756,105],{"class":104},[91,758,759],{"class":108}," dir2 ",[91,761,476],{"class":104},[91,763,749],{"class":122},[91,765,766],{"class":97},"       \u002F\u002F 类型是 string\n",[91,768,769,771,774,776,779],{"class":93,"line":129},[91,770,462],{"class":104},[91,772,773],{"class":115}," num",[91,775,119],{"class":104},[91,777,778],{"class":115}," 42",[91,780,781],{"class":97},"        \u002F\u002F 类型是 42\n",[15,783,784,785,74],{},"这条差异在第五章「可辨识联合」和第九章「字面量类型」里会起到关键作用，先埋个印象：",[19,786,787],{},"const 帮你把类型收得更窄，窄到具体值",[789,790],"demo-html",{"height":791,"src":792,"title":793},560,"\u002Ftutorials\u002Fts\u002Fdemos\u002F02-annotations.html","示例 02：注解语法与类型推断——哪些错会被挑出来",[28,795,796],{"id":796},"函数返回值的推断",[15,798,799,800,803,804,807],{},"不写返回类型时，TS 看 ",[36,801,802],{},"return"," 后面到底吐什么来反推。推断通常很准，但有个坑：",[19,805,806],{},"递归函数不写返回类型可能推错","（编译器还没算完函数体就遇到自己调用自己，会要求你显式标注）。公共 API 上老老实实写返回类型，既是自我约束，也是给下游的一份稳定契约。",[10,809,811,812,814,815,814,817,820,821,814,823,814,826],{"id":810},"三基础类型string-number-boolean数组与元组枚举以及-any-unknown-never","三、基础类型：",[36,813,728],{}," \u002F ",[36,816,732],{},[36,818,819],{},"boolean","、数组与元组、枚举，以及 ",[36,822,513],{},[36,824,825],{},"unknown",[36,827,828],{},"never",[15,830,831,832,835,836,729,838,729,840,842],{},"这一章把 TS 里最常被写错、也最常被误解的几种类型过一遍。JS 教程第三章已经讲清楚了 JS 世界里的八种值，所以这里",[19,833,834],{},"只讲类型系统那一层","：TS 怎么描述它们，以及三个特殊类型 ",[36,837,513],{},[36,839,825],{},[36,841,828],{}," 到底各管什么。",[28,844,845],{"id":845},"三种原始类型没有惊喜",[15,847,848,186,850,186,852,854,855,857,858,861,862,729,865,729,868,871,872,875,876,74,878,881,882,885],{},[36,849,728],{},[36,851,732],{},[36,853,819],{}," 就是 JS 的字符串、数字、布尔——注意 TS 的 ",[36,856,732],{}," 和 JS 一样",[19,859,860],{},"不区分整数与浮点","，没有 ",[36,863,864],{},"int",[36,866,867],{},"float",[36,869,870],{},"double"," 这些。别下意识写 ",[36,873,874],{},"let age: int","，那是 Java\u002FC# 的手感，TS 里只有 ",[36,877,732],{},[36,879,880],{},"bigint"," 和 ",[36,883,884],{},"symbol"," 也各有对应类型，用得少。",[28,887,888],{"id":888},"数组与元组",[15,890,891,892,895,896,899,900,902],{},"数组用 ",[36,893,894],{},"T[]"," 或 ",[36,897,898],{},"Array\u003CT>"," 写，两种等价，",[36,901,898],{}," 在类型复杂时更好读：",[82,904,906],{"className":84,"code":905,"language":86,"meta":87,"style":87},"let list: number[] = [1, 2, 3]\nlet list2: Array\u003Cnumber> = [1, 2, 3]   \u002F\u002F 完全等价\n\nlet pair: [string, number] = ['id', 1]  \u002F\u002F 元组：位置和类型都固定\npair = [1, 'id']                        \u002F\u002F ✗ 顺序错了也报错\n",[36,907,908,941,981,985,1021],{"__ignoreMap":87},[91,909,910,912,915,917,919,921,923,925,928,930,933,935,938],{"class":93,"line":94},[91,911,105],{"class":104},[91,913,914],{"class":108}," list",[91,916,112],{"class":104},[91,918,116],{"class":115},[91,920,473],{"class":108},[91,922,476],{"class":104},[91,924,479],{"class":108},[91,926,927],{"class":115},"1",[91,929,416],{"class":108},[91,931,932],{"class":115},"2",[91,934,416],{"class":108},[91,936,937],{"class":115},"3",[91,939,940],{"class":108},"]\n",[91,942,943,945,948,950,953,956,958,961,963,965,967,969,971,973,975,978],{"class":93,"line":101},[91,944,105],{"class":104},[91,946,947],{"class":108}," list2",[91,949,112],{"class":104},[91,951,952],{"class":135}," Array",[91,954,955],{"class":108},"\u003C",[91,957,732],{"class":115},[91,959,960],{"class":108},"> ",[91,962,476],{"class":104},[91,964,479],{"class":108},[91,966,927],{"class":115},[91,968,416],{"class":108},[91,970,932],{"class":115},[91,972,416],{"class":108},[91,974,937],{"class":115},[91,976,977],{"class":108},"]   ",[91,979,980],{"class":97},"\u002F\u002F 完全等价\n",[91,982,983],{"class":93,"line":129},[91,984,567],{"emptyLinePlaceholder":566},[91,986,987,989,992,994,996,998,1000,1002,1005,1007,1009,1012,1014,1016,1018],{"class":93,"line":453},[91,988,105],{"class":104},[91,990,991],{"class":108}," pair",[91,993,112],{"class":104},[91,995,479],{"class":108},[91,997,728],{"class":115},[91,999,416],{"class":108},[91,1001,732],{"class":115},[91,1003,1004],{"class":108},"] ",[91,1006,476],{"class":104},[91,1008,479],{"class":108},[91,1010,1011],{"class":122},"'id'",[91,1013,416],{"class":108},[91,1015,927],{"class":115},[91,1017,490],{"class":108},[91,1019,1020],{"class":97},"\u002F\u002F 元组：位置和类型都固定\n",[91,1022,1023,1026,1028,1030,1032,1034,1036,1039],{"class":93,"line":459},[91,1024,1025],{"class":108},"pair ",[91,1027,476],{"class":104},[91,1029,479],{"class":108},[91,1031,927],{"class":115},[91,1033,416],{"class":108},[91,1035,1011],{"class":122},[91,1037,1038],{"class":108},"]                        ",[91,1040,1041],{"class":97},"\u002F\u002F ✗ 顺序错了也报错\n",[15,1043,1044,1045,1048,1049,1052],{},"元组（tuple）是「长度和各位置类型都写死」的数组，常用于函数一次返回多个值、或表示定长的键值对。长度可变时还有可选元组元素 ",[36,1046,1047],{},"[string, number?]"," 和展开元组 ",[36,1050,1051],{},"[...T[], string]","，用得不多，知道有即可。",[28,1054,1056,1057,1060],{"id":1055},"枚举enum-与它的运行时代价","枚举：",[36,1058,1059],{},"enum"," 与它的运行时代价",[15,1062,1063,1064,1067,1068,1070],{},"枚举是 TS 里少见的",[19,1065,1066],{},"真的会往 JS 里生成代码","的特性（第七章会看到别的类型都被擦除，",[36,1069,1059],{}," 恰恰不会）。它给一组常量起名字：",[82,1072,1074],{"className":84,"code":1073,"language":86,"meta":87,"style":87},"enum Direction { Up = 0, Down = 1, Left = 2, Right = 3 }\nconst d = Direction.Up     \u002F\u002F 0\nDirection[0]               \u002F\u002F 'Up'——反向映射：数字也能查回名字\n",[36,1075,1076,1125,1140],{"__ignoreMap":87},[91,1077,1078,1080,1083,1086,1089,1091,1093,1095,1098,1100,1102,1104,1107,1109,1112,1114,1117,1119,1122],{"class":93,"line":94},[91,1079,1059],{"class":104},[91,1081,1082],{"class":135}," Direction",[91,1084,1085],{"class":108}," { ",[91,1087,1088],{"class":115},"Up",[91,1090,119],{"class":104},[91,1092,391],{"class":115},[91,1094,416],{"class":108},[91,1096,1097],{"class":115},"Down",[91,1099,119],{"class":104},[91,1101,659],{"class":115},[91,1103,416],{"class":108},[91,1105,1106],{"class":115},"Left",[91,1108,119],{"class":104},[91,1110,1111],{"class":115}," 2",[91,1113,416],{"class":108},[91,1115,1116],{"class":115},"Right",[91,1118,119],{"class":104},[91,1120,1121],{"class":115}," 3",[91,1123,1124],{"class":108}," }\n",[91,1126,1127,1129,1132,1134,1137],{"class":93,"line":101},[91,1128,462],{"class":104},[91,1130,1131],{"class":115}," d",[91,1133,119],{"class":104},[91,1135,1136],{"class":108}," Direction.Up     ",[91,1138,1139],{"class":97},"\u002F\u002F 0\n",[91,1141,1142,1145,1148,1151],{"class":93,"line":129},[91,1143,1144],{"class":108},"Direction[",[91,1146,1147],{"class":115},"0",[91,1149,1150],{"class":108},"]               ",[91,1152,1153],{"class":97},"\u002F\u002F 'Up'——反向映射：数字也能查回名字\n",[15,1155,1156,1157,1159,1160,1163,1164,1167,1168,1170,1171,1174,1175,1177],{},"正因为 ",[36,1158,1059],{}," 会编译成一个真实的对象，它有",[19,1161,1162],{},"运行时成本","：体积、反查、还有 ",[36,1165,1166],{},"const enum"," 那种「编译期直接内联成数字、不留对象」的特例（第十七章会讲 ",[36,1169,1166],{}," 在打包工具下的一系列坑）。实践中很多团队宁可用「联合字面量 + ",[36,1172,1173],{},"as const"," 对象」替代 ",[36,1176,1059],{},"，因为后者零运行时开销、还能享受字面量窄化：",[82,1179,1181],{"className":84,"code":1180,"language":86,"meta":87,"style":87},"const Direction = { Up: 0, Down: 1 } as const\ntype Direction = (typeof Direction)[keyof typeof Direction]   \u002F\u002F 0 | 1\n",[36,1182,1183,1209],{"__ignoreMap":87},[91,1184,1185,1187,1189,1191,1194,1196,1199,1201,1204,1206],{"class":93,"line":94},[91,1186,462],{"class":104},[91,1188,1082],{"class":115},[91,1190,119],{"class":104},[91,1192,1193],{"class":108}," { Up: ",[91,1195,1147],{"class":115},[91,1197,1198],{"class":108},", Down: ",[91,1200,927],{"class":115},[91,1202,1203],{"class":108}," } ",[91,1205,195],{"class":104},[91,1207,1208],{"class":104}," const\n",[91,1210,1211,1214,1216,1218,1221,1224,1227,1230,1233,1236],{"class":93,"line":101},[91,1212,1213],{"class":104},"type",[91,1215,1082],{"class":135},[91,1217,119],{"class":104},[91,1219,1220],{"class":108}," (",[91,1222,1223],{"class":104},"typeof",[91,1225,1226],{"class":108}," Direction)[",[91,1228,1229],{"class":104},"keyof",[91,1231,1232],{"class":104}," typeof",[91,1234,1235],{"class":108}," Direction]   ",[91,1237,1238],{"class":97},"\u002F\u002F 0 | 1\n",[28,1240,1242,1244],{"id":1241},"any逃生舱不是未知类型",[36,1243,513],{},"：逃生舱，不是「未知类型」",[15,1246,1247,1249,1250,1253,1254,1256,1257,74],{},[36,1248,513],{}," 的意思是「",[19,1251,1252],{},"关掉这个值上的所有类型检查","」。你想让编译器闭嘴时，写 ",[36,1255,513],{}," 就行——但代价是：从这一刻起，这个值身上发生的一切错误，编译器都不再提醒你。",[19,1258,1259,1260,1262],{},"用了 ",[36,1261,513],{},"，你实际上是把这块代码降级回了纯 JS",[82,1264,1266],{"className":84,"code":1265,"language":86,"meta":87,"style":87},"let x: any = 'text'\nx = 42\nx.toExponential(2)   \u002F\u002F 合法！any 上什么成员都允许\nx.somethingNotThere  \u002F\u002F 也不报错——这正是危险之处\n",[36,1267,1268,1285,1295,1313],{"__ignoreMap":87},[91,1269,1270,1272,1275,1277,1280,1282],{"class":93,"line":94},[91,1271,105],{"class":104},[91,1273,1274],{"class":108}," x",[91,1276,112],{"class":104},[91,1278,1279],{"class":115}," any",[91,1281,119],{"class":104},[91,1283,1284],{"class":122}," 'text'\n",[91,1286,1287,1290,1292],{"class":93,"line":101},[91,1288,1289],{"class":108},"x ",[91,1291,476],{"class":104},[91,1293,1294],{"class":115}," 42\n",[91,1296,1297,1300,1303,1305,1307,1310],{"class":93,"line":129},[91,1298,1299],{"class":108},"x.",[91,1301,1302],{"class":135},"toExponential",[91,1304,405],{"class":108},[91,1306,932],{"class":115},[91,1308,1309],{"class":108},")   ",[91,1311,1312],{"class":97},"\u002F\u002F 合法！any 上什么成员都允许\n",[91,1314,1315,1318],{"class":93,"line":453},[91,1316,1317],{"class":108},"x.somethingNotThere  ",[91,1319,1320],{"class":97},"\u002F\u002F 也不报错——这正是危险之处\n",[15,1322,1323,1325,1326,1329],{},[36,1324,513],{}," 只该用在「真的还不知道、且急着跑起来」的临时场合，以及迁移老 JS 代码的过渡期。它是",[19,1327,1328],{},"逃生舱","，不该是默认选择。",[28,1331,1333,1335],{"id":1332},"unknown安全的任意值",[36,1334,825],{},"：安全的任意值",[15,1337,1338,1340,1341,1343],{},[36,1339,825],{}," 才是「我不知道这是什么类型」的正确表达。它和 ",[36,1342,513],{}," 的区别，是这一章的核心：",[254,1345,1346,1360],{},[257,1347,1348],{},[260,1349,1350,1352,1356],{},[263,1351],{},[263,1353,1354],{},[36,1355,513],{},[263,1357,1358],{},[36,1359,825],{},[270,1361,1362,1376,1387],{},[260,1363,1364,1367,1373],{},[275,1365,1366],{},"能赋给别的类型吗",[275,1368,1369,1370,1372],{},"能（",[36,1371,513],{}," 赋值到哪都不报错，还会传染）",[275,1374,1375],{},"不能（必须先收窄）",[260,1377,1378,1381,1384],{},[275,1379,1380],{},"能直接调用\u002F取属性吗",[275,1382,1383],{},"能（不检查）",[275,1385,1386],{},"不能（报错，逼你判断）",[260,1388,1389,1392,1395],{},[275,1390,1391],{},"定位",[275,1393,1394],{},"逃生舱：关掉检查",[275,1396,1397,1398],{},"类型安全的 ",[36,1399,513],{},[82,1401,1403],{"className":84,"code":1402,"language":86,"meta":87,"style":87},"let u: unknown = 'hello'\nu.toUpperCase()          \u002F\u002F ✗ 报错：unknown 上什么都不能直接做\nconst s: string = u      \u002F\u002F ✗ 报错：unknown 不能赋给 string\nif (typeof u === 'string') {\n  u.toUpperCase()        \u002F\u002F ✓ 收窄后才能用\n}\nconst n: number = u as number   \u002F\u002F 也能用断言强行说它是 number（自担风险）\n",[36,1404,1405,1422,1435,1454,1475,1488,1492],{"__ignoreMap":87},[91,1406,1407,1409,1412,1414,1417,1419],{"class":93,"line":94},[91,1408,105],{"class":104},[91,1410,1411],{"class":108}," u",[91,1413,112],{"class":104},[91,1415,1416],{"class":115}," unknown",[91,1418,119],{"class":104},[91,1420,1421],{"class":122}," 'hello'\n",[91,1423,1424,1427,1429,1432],{"class":93,"line":101},[91,1425,1426],{"class":108},"u.",[91,1428,142],{"class":135},[91,1430,1431],{"class":108},"()          ",[91,1433,1434],{"class":97},"\u002F\u002F ✗ 报错：unknown 上什么都不能直接做\n",[91,1436,1437,1439,1442,1444,1446,1448,1451],{"class":93,"line":129},[91,1438,462],{"class":104},[91,1440,1441],{"class":115}," s",[91,1443,112],{"class":104},[91,1445,470],{"class":115},[91,1447,119],{"class":104},[91,1449,1450],{"class":108}," u      ",[91,1452,1453],{"class":97},"\u002F\u002F ✗ 报错：unknown 不能赋给 string\n",[91,1455,1456,1459,1461,1463,1466,1469,1472],{"class":93,"line":453},[91,1457,1458],{"class":104},"if",[91,1460,1220],{"class":108},[91,1462,1223],{"class":104},[91,1464,1465],{"class":108}," u ",[91,1467,1468],{"class":104},"===",[91,1470,1471],{"class":122}," 'string'",[91,1473,1474],{"class":108},") {\n",[91,1476,1477,1480,1482,1485],{"class":93,"line":459},[91,1478,1479],{"class":108},"  u.",[91,1481,142],{"class":135},[91,1483,1484],{"class":108},"()        ",[91,1486,1487],{"class":97},"\u002F\u002F ✓ 收窄后才能用\n",[91,1489,1490],{"class":93,"line":611},[91,1491,456],{"class":108},[91,1493,1495,1497,1499,1501,1503,1505,1507,1509,1511],{"class":93,"line":1494},7,[91,1496,462],{"class":104},[91,1498,109],{"class":115},[91,1500,112],{"class":104},[91,1502,116],{"class":115},[91,1504,119],{"class":104},[91,1506,1465],{"class":108},[91,1508,195],{"class":104},[91,1510,116],{"class":115},[91,1512,1513],{"class":97},"   \u002F\u002F 也能用断言强行说它是 number（自担风险）\n",[15,1515,1516,1517,1519,1520,1522,1523,504,1526,1529,1530,1532,1533,1535,1536,1539,1540],{},"任何值都能赋给 ",[36,1518,825],{},"，但 ",[36,1521,825],{}," 想再往下走得先「验明正身」。TS 4.4 起，",[36,1524,1525],{},"catch (err)",[36,1527,1528],{},"err"," 默认就是 ",[36,1531,825],{}," 而不是 ",[36,1534,513],{},"，逼你去判断捕获到的到底是什么——这是",[19,1537,1538],{},"把不确定性挡在使用点之前","的设计哲学。记住口诀：",[19,1541,1542,1544,1545,1547],{},[36,1543,513],{}," 是让编译器别管，",[36,1546,825],{}," 是让编译器管但先别信。",[28,1549,1551,1553],{"id":1550},"never不可能到达",[36,1552,828],{},"：不可能到达",[15,1555,1556,1558,1559,1562],{},[36,1557,828],{}," 是「",[19,1560,1561],{},"没有任何值","」的类型，出现在两种地方：",[82,1564,1566],{"className":84,"code":1565,"language":86,"meta":87,"style":87},"function fail(msg: string): never {    \u002F\u002F 永不正常返回：要么抛错要么死循环\n  throw new Error(msg)\n}\nfunction loop(): never { while (true) {} }\n\ntype Empty = number & string   \u002F\u002F 交集为空 → never\n",[36,1567,1568,1597,1611,1615,1642,1646],{"__ignoreMap":87},[91,1569,1570,1572,1575,1577,1580,1582,1584,1586,1588,1591,1594],{"class":93,"line":94},[91,1571,399],{"class":104},[91,1573,1574],{"class":135}," fail",[91,1576,405],{"class":108},[91,1578,1579],{"class":408},"msg",[91,1581,112],{"class":104},[91,1583,470],{"class":115},[91,1585,426],{"class":108},[91,1587,112],{"class":104},[91,1589,1590],{"class":115}," never",[91,1592,1593],{"class":108}," {    ",[91,1595,1596],{"class":97},"\u002F\u002F 永不正常返回：要么抛错要么死循环\n",[91,1598,1599,1602,1605,1608],{"class":93,"line":101},[91,1600,1601],{"class":104},"  throw",[91,1603,1604],{"class":104}," new",[91,1606,1607],{"class":135}," Error",[91,1609,1610],{"class":108},"(msg)\n",[91,1612,1613],{"class":93,"line":129},[91,1614,456],{"class":108},[91,1616,1617,1619,1622,1625,1627,1629,1631,1634,1636,1639],{"class":93,"line":453},[91,1618,399],{"class":104},[91,1620,1621],{"class":135}," loop",[91,1623,1624],{"class":108},"()",[91,1626,112],{"class":104},[91,1628,1590],{"class":115},[91,1630,1085],{"class":108},[91,1632,1633],{"class":104},"while",[91,1635,1220],{"class":108},[91,1637,1638],{"class":115},"true",[91,1640,1641],{"class":108},") {} }\n",[91,1643,1644],{"class":93,"line":459},[91,1645,567],{"emptyLinePlaceholder":566},[91,1647,1648,1650,1653,1655,1657,1660,1662],{"class":93,"line":611},[91,1649,1213],{"class":104},[91,1651,1652],{"class":135}," Empty",[91,1654,119],{"class":104},[91,1656,116],{"class":115},[91,1658,1659],{"class":104}," &",[91,1661,470],{"class":115},[91,1663,1664],{"class":97},"   \u002F\u002F 交集为空 → never\n",[15,1666,1667,1669,1670,1673,1674,1676],{},[36,1668,828],{}," 最重要的用法在第五章——",[19,1671,1672],{},"穷尽检查","：当联合类型的所有分支都被处理完，剩下的分支应当是 ",[36,1675,828],{},"，一旦哪天加了新分支没处理，编译器就会在这里报错。它是 TS 里少数「用来验证你写全了」的机制。",[28,1678,1680,814,1682,1685,1686],{"id":1679},"null-undefined-与-void",[36,1681,295],{},[36,1683,1684],{},"undefined"," 与 ",[36,1687,1688],{},"void",[15,1690,1691,1693],{},[36,1692,1688],{}," 表示「没有值」，主要用作函数返回类型，告诉编译器「别指望这里返回东西」：",[82,1695,1697],{"className":84,"code":1696,"language":86,"meta":87,"style":87},"function log(msg: string): void {\n  console.log(msg)     \u002F\u002F 没有 return\n}\nlet v: void = undefined   \u002F\u002F void 类型的变量只能装 undefined（严格模式下）\n",[36,1698,1699,1724,1737,1741],{"__ignoreMap":87},[91,1700,1701,1703,1706,1708,1710,1712,1714,1716,1718,1721],{"class":93,"line":94},[91,1702,399],{"class":104},[91,1704,1705],{"class":135}," log",[91,1707,405],{"class":108},[91,1709,1579],{"class":408},[91,1711,112],{"class":104},[91,1713,470],{"class":115},[91,1715,426],{"class":108},[91,1717,112],{"class":104},[91,1719,1720],{"class":115}," void",[91,1722,1723],{"class":108}," {\n",[91,1725,1726,1729,1731,1734],{"class":93,"line":101},[91,1727,1728],{"class":108},"  console.",[91,1730,136],{"class":135},[91,1732,1733],{"class":108},"(msg)     ",[91,1735,1736],{"class":97},"\u002F\u002F 没有 return\n",[91,1738,1739],{"class":93,"line":129},[91,1740,456],{"class":108},[91,1742,1743,1745,1748,1750,1752,1754,1757],{"class":93,"line":453},[91,1744,105],{"class":104},[91,1746,1747],{"class":108}," v",[91,1749,112],{"class":104},[91,1751,1720],{"class":115},[91,1753,119],{"class":104},[91,1755,1756],{"class":115}," undefined",[91,1758,1759],{"class":97},"   \u002F\u002F void 类型的变量只能装 undefined（严格模式下）\n",[15,1761,1762,881,1764,1766,1767,1773,1774,1777,1778,211,1793,1795,1796,1798,1799,1802],{},[36,1763,295],{},[36,1765,1684],{}," 在",[19,1768,1769,1770],{},"关掉 ",[36,1771,1772],{},"strictNullChecks"," 时可以被赋给任何类型，这是老代码里无数 ",[36,1775,1776],{},"Cannot read property of undefined"," 的根源。",[19,1779,1780,1781,1783,1784,1787,1788,729,1790,1792],{},"打开 ",[36,1782,1772],{},"（属于 ",[36,1785,1786],{},"strict"," 家族，第六章、十六章详谈）后，",[36,1789,295],{},[36,1791,1684],{}," 从各类型里独立出来",[36,1794,728],{}," 不再包含 ",[36,1797,295],{},"，你要表达「可能是空」必须显式写 ",[36,1800,1801],{},"string | null","。这是 TS 最有价值的一条开关——它把「这里会不会是 null」从运行时惊喜变成了编译期契约。",[789,1804],{"height":1805,"src":1806,"title":1807},600,"\u002Ftutorials\u002Fts\u002Fdemos\u002F03-basic-types.html","示例 03：基础类型与 any \u002F unknown \u002F never 的边界",[10,1809,1811,1812,1685,1814,1816],{"id":1810},"四对象类型与接口type-与-interface-的差别","四、对象类型与接口：",[36,1813,1213],{},[36,1815,189],{}," 的差别",[15,1818,1819,1820,881,1822,1824],{},"JS 教程第十章讲了对象在运行时怎么写；这一章讲 TS 怎么给对象「画形状」。核心是两种描述结构的方式 ",[36,1821,1213],{},[36,1823,189],{},"，以及围绕对象的四种修饰：可选、只读、索引签名、函数与对象混合。",[28,1826,1828],{"id":1827},"先看清结构相同即类型相同","先看清：结构相同即类型相同",[15,1830,1831,1832,1835],{},"TS 是",[19,1833,1834],{},"结构化类型（structural typing）","——名字不重要，形状重要。两个接口哪怕毫不相干、没继承关系，只要字段对得上，一个就能当另一个用：",[82,1837,1839],{"className":84,"code":1838,"language":86,"meta":87,"style":87},"interface Point { x: number; y: number }\nconst p: Point = { x: 1, y: 2, z: 3 }   \u002F\u002F ✗ 报错（多余属性检查，见下）\nfunction use(pt: { x: number }) { \u002F* 只用 x *\u002F }\nuse(p)   \u002F\u002F ✓ 只要 p 至少有 x 就行，不在乎它还有 y\n",[36,1840,1841,1869,1903,1933],{"__ignoreMap":87},[91,1842,1843,1845,1848,1850,1853,1855,1857,1860,1863,1865,1867],{"class":93,"line":94},[91,1844,189],{"class":104},[91,1846,1847],{"class":135}," Point",[91,1849,1085],{"class":108},[91,1851,1852],{"class":408},"x",[91,1854,112],{"class":104},[91,1856,116],{"class":115},[91,1858,1859],{"class":108},"; ",[91,1861,1862],{"class":408},"y",[91,1864,112],{"class":104},[91,1866,116],{"class":115},[91,1868,1124],{"class":108},[91,1870,1871,1873,1876,1878,1880,1882,1885,1887,1890,1892,1895,1897,1900],{"class":93,"line":101},[91,1872,462],{"class":104},[91,1874,1875],{"class":115}," p",[91,1877,112],{"class":104},[91,1879,1847],{"class":135},[91,1881,119],{"class":104},[91,1883,1884],{"class":108}," { x: ",[91,1886,927],{"class":115},[91,1888,1889],{"class":108},", y: ",[91,1891,932],{"class":115},[91,1893,1894],{"class":108},", z: ",[91,1896,937],{"class":115},[91,1898,1899],{"class":108}," }   ",[91,1901,1902],{"class":97},"\u002F\u002F ✗ 报错（多余属性检查，见下）\n",[91,1904,1905,1907,1910,1912,1915,1917,1919,1921,1923,1925,1928,1931],{"class":93,"line":129},[91,1906,399],{"class":104},[91,1908,1909],{"class":135}," use",[91,1911,405],{"class":108},[91,1913,1914],{"class":408},"pt",[91,1916,112],{"class":104},[91,1918,1085],{"class":108},[91,1920,1852],{"class":408},[91,1922,112],{"class":104},[91,1924,116],{"class":115},[91,1926,1927],{"class":108}," }) { ",[91,1929,1930],{"class":97},"\u002F* 只用 x *\u002F",[91,1932,1124],{"class":108},[91,1934,1935,1938,1941],{"class":93,"line":453},[91,1936,1937],{"class":135},"use",[91,1939,1940],{"class":108},"(p)   ",[91,1942,1943],{"class":97},"\u002F\u002F ✓ 只要 p 至少有 x 就行，不在乎它还有 y\n",[15,1945,1946],{},"这点和第 17 章「假兼容」的坑直接相关：结构化类型很灵活，但会让你以为「这两个类型不同所以不通用」，实际上编译器只看形状。",[28,1948,1950,1685,1952,1954],{"id":1949},"type-与-interface该怎么选",[36,1951,1213],{},[36,1953,189],{},"：该怎么选",[15,1956,1957],{},"两者大部分场景能互换，差别集中在几条：",[254,1959,1960,1975],{},[257,1961,1962],{},[260,1963,1964,1966,1970],{},[263,1965],{},[263,1967,1968],{},[36,1969,189],{},[263,1971,1972,1974],{},[36,1973,1213],{},"（类型别名）",[270,1976,1977,1995,2009,2020,2031],{},[260,1978,1979,1982,1988],{},[275,1980,1981],{},"扩展方式",[275,1983,1984,1987],{},[36,1985,1986],{},"extends","，可以继承多个",[275,1989,1990,1991,1994],{},"交叉 ",[36,1992,1993],{},"&","，也能拼",[260,1996,1997,2000,2006],{},[275,1998,1999],{},"同名声明",[275,2001,2002,2005],{},[19,2003,2004],{},"会自动合并","（声明合并）",[275,2007,2008],{},"同名再定义会报错",[260,2010,2011,2014,2017],{},[275,2012,2013],{},"能表达的范畴",[275,2015,2016],{},"只能描述对象\u002F函数\u002F构造这类「形状」",[275,2018,2019],{},"几乎什么都能：联合、元组、映射类型、条件类型",[260,2021,2022,2025,2028],{},[275,2023,2024],{},"报错信息",[275,2026,2027],{},"通常更简洁",[275,2029,2030],{},"复杂组合时更易读（保留了名字）",[260,2032,2033,2036,2039],{},[275,2034,2035],{},"惯例",[275,2037,2038],{},"描述对象契约、公共 API",[275,2040,2041],{},"需要联合\u002F工具\u002F高级类型时用",[15,2043,2044,2045,2053],{},"一句话经验：",[19,2046,2047,2048,2050,2051],{},"描述一个对象的形状优先 ",[36,2049,189],{},"；一旦需要联合、映射、条件这类「类型的运算」，只能用 ",[36,2052,1213],{},"。团队里两者都允许即可，不必教条。",[82,2055,2057],{"className":84,"code":2056,"language":86,"meta":87,"style":87},"type ID = string | number        \u002F\u002F 联合——interface 做不到\ntype Pair = [number, number]     \u002F\u002F 元组——type 更顺手\ntype Flags = { a: boolean } & { b: boolean }   \u002F\u002F 交叉，等价于 {a:boolean;b:boolean}\n",[36,2058,2059,2078,2101],{"__ignoreMap":87},[91,2060,2061,2063,2066,2068,2070,2073,2075],{"class":93,"line":94},[91,2062,1213],{"class":104},[91,2064,2065],{"class":135}," ID",[91,2067,119],{"class":104},[91,2069,470],{"class":115},[91,2071,2072],{"class":104}," |",[91,2074,116],{"class":115},[91,2076,2077],{"class":97},"        \u002F\u002F 联合——interface 做不到\n",[91,2079,2080,2082,2085,2087,2089,2091,2093,2095,2098],{"class":93,"line":101},[91,2081,1213],{"class":104},[91,2083,2084],{"class":135}," Pair",[91,2086,119],{"class":104},[91,2088,479],{"class":108},[91,2090,732],{"class":115},[91,2092,416],{"class":108},[91,2094,732],{"class":115},[91,2096,2097],{"class":108},"]     ",[91,2099,2100],{"class":97},"\u002F\u002F 元组——type 更顺手\n",[91,2102,2103,2105,2108,2110,2112,2114,2116,2119,2121,2123,2125,2127,2129,2131,2133],{"class":93,"line":129},[91,2104,1213],{"class":104},[91,2106,2107],{"class":135}," Flags",[91,2109,119],{"class":104},[91,2111,1085],{"class":108},[91,2113,409],{"class":408},[91,2115,112],{"class":104},[91,2117,2118],{"class":115}," boolean",[91,2120,1203],{"class":108},[91,2122,1993],{"class":104},[91,2124,1085],{"class":108},[91,2126,419],{"class":408},[91,2128,112],{"class":104},[91,2130,2118],{"class":115},[91,2132,1899],{"class":108},[91,2134,2135],{"class":97},"\u002F\u002F 交叉，等价于 {a:boolean;b:boolean}\n",[28,2137,2139,2140],{"id":2138},"可选属性","可选属性 ",[36,2141,2142],{},"?",[15,2144,2145,2146,2148,2149,211],{},"问号表示「这个键可以不存在」，一旦标了 ",[36,2147,2142],{},"，它的类型就自动变成 ",[36,2150,2151],{},"T | undefined",[82,2153,2155],{"className":84,"code":2154,"language":86,"meta":87,"style":87},"interface User { name: string; age?: number }\nconst u: User = { name: '阿蓝' }    \u002F\u002F 合法，age 省略\nu.age                               \u002F\u002F number | undefined\nu.age.toFixed(2)                    \u002F\u002F ✗ 报错：可能是 undefined\nu.age?.toFixed(2)                   \u002F\u002F ✓ 可选链（JS 教程第四章的语法，类型上同样有效）\nif (u.age !== undefined) u.age.toFixed(2)   \u002F\u002F ✓ 收窄后能用\n",[36,2156,2157,2185,2209,2217,2235,2252],{"__ignoreMap":87},[91,2158,2159,2161,2164,2166,2169,2171,2173,2175,2178,2181,2183],{"class":93,"line":94},[91,2160,189],{"class":104},[91,2162,2163],{"class":135}," User",[91,2165,1085],{"class":108},[91,2167,2168],{"class":408},"name",[91,2170,112],{"class":104},[91,2172,470],{"class":115},[91,2174,1859],{"class":108},[91,2176,2177],{"class":408},"age",[91,2179,2180],{"class":104},"?:",[91,2182,116],{"class":115},[91,2184,1124],{"class":108},[91,2186,2187,2189,2191,2193,2195,2197,2200,2203,2206],{"class":93,"line":101},[91,2188,462],{"class":104},[91,2190,1411],{"class":115},[91,2192,112],{"class":104},[91,2194,2163],{"class":135},[91,2196,119],{"class":104},[91,2198,2199],{"class":108}," { name: ",[91,2201,2202],{"class":122},"'阿蓝'",[91,2204,2205],{"class":108}," }    ",[91,2207,2208],{"class":97},"\u002F\u002F 合法，age 省略\n",[91,2210,2211,2214],{"class":93,"line":129},[91,2212,2213],{"class":108},"u.age                               ",[91,2215,2216],{"class":97},"\u002F\u002F number | undefined\n",[91,2218,2219,2222,2225,2227,2229,2232],{"class":93,"line":453},[91,2220,2221],{"class":108},"u.age.",[91,2223,2224],{"class":135},"toFixed",[91,2226,405],{"class":108},[91,2228,932],{"class":115},[91,2230,2231],{"class":108},")                    ",[91,2233,2234],{"class":97},"\u002F\u002F ✗ 报错：可能是 undefined\n",[91,2236,2237,2240,2242,2244,2246,2249],{"class":93,"line":459},[91,2238,2239],{"class":108},"u.age?.",[91,2241,2224],{"class":135},[91,2243,405],{"class":108},[91,2245,932],{"class":115},[91,2247,2248],{"class":108},")                   ",[91,2250,2251],{"class":97},"\u002F\u002F ✓ 可选链（JS 教程第四章的语法，类型上同样有效）\n",[91,2253,2254,2256,2259,2262,2264,2267,2269,2271,2273,2275],{"class":93,"line":611},[91,2255,1458],{"class":104},[91,2257,2258],{"class":108}," (u.age ",[91,2260,2261],{"class":104},"!==",[91,2263,1756],{"class":115},[91,2265,2266],{"class":108},") u.age.",[91,2268,2224],{"class":135},[91,2270,405],{"class":108},[91,2272,932],{"class":115},[91,2274,1309],{"class":108},[91,2276,2277],{"class":97},"\u002F\u002F ✓ 收窄后能用\n",[28,2279,2281,2282,1685,2285],{"id":2280},"只读-readonly-与-as-const","只读 ",[36,2283,2284],{},"readonly",[36,2286,1173],{},[15,2288,2289,2291],{},[36,2290,2284],{}," 让属性只能初始化时赋值一次，之后再改就报错：",[82,2293,2295],{"className":84,"code":2294,"language":86,"meta":87,"style":87},"interface Config { readonly api: string }\nconst c: Config = { api: '\u002Fv1' }\nc.api = '\u002Fv2'     \u002F\u002F ✗ 只读属性不能改\n",[36,2296,2297,2317,2338],{"__ignoreMap":87},[91,2298,2299,2301,2304,2306,2308,2311,2313,2315],{"class":93,"line":94},[91,2300,189],{"class":104},[91,2302,2303],{"class":135}," Config",[91,2305,1085],{"class":108},[91,2307,2284],{"class":104},[91,2309,2310],{"class":408}," api",[91,2312,112],{"class":104},[91,2314,470],{"class":115},[91,2316,1124],{"class":108},[91,2318,2319,2321,2324,2326,2328,2330,2333,2336],{"class":93,"line":101},[91,2320,462],{"class":104},[91,2322,2323],{"class":115}," c",[91,2325,112],{"class":104},[91,2327,2303],{"class":135},[91,2329,119],{"class":104},[91,2331,2332],{"class":108}," { api: ",[91,2334,2335],{"class":122},"'\u002Fv1'",[91,2337,1124],{"class":108},[91,2339,2340,2343,2345,2348],{"class":93,"line":129},[91,2341,2342],{"class":108},"c.api ",[91,2344,476],{"class":104},[91,2346,2347],{"class":122}," '\u002Fv2'",[91,2349,2350],{"class":97},"     \u002F\u002F ✗ 只读属性不能改\n",[15,2352,2353,2355,2356,2359,2360,2362,2363,2365],{},[36,2354,2284],{}," 是编译期约束，运行时值仍可改（除非 ",[36,2357,2358],{},"Object.freeze","）。第九章会看到 ",[36,2361,1173],{}," 把整个对象所有属性一次性都标成 ",[36,2364,2284],{}," 且窄化成字面量。",[28,2367,2369],{"id":2368},"索引签名描述键不固定的对象","索引签名：描述「键不固定的对象」",[15,2371,2372],{},"当对象的键你说不全（比如一个「字符串映射到数字」的字典），用索引签名：",[82,2374,2376],{"className":84,"code":2375,"language":86,"meta":87,"style":87},"interface StringNum { [key: string]: number }\nconst map: StringNum = { a: 1, b: 2 }\nmap.c = 3        \u002F\u002F 合法：任意字符串键 → number\nmap.d = 'x'      \u002F\u002F ✗ 值必须是 number\n\n\u002F\u002F 数字索引：数组本质上就是 { [index: number]: T } 的特例\n",[36,2377,2378,2404,2429,2441,2454,2458],{"__ignoreMap":87},[91,2379,2380,2382,2385,2388,2391,2393,2395,2398,2400,2402],{"class":93,"line":94},[91,2381,189],{"class":104},[91,2383,2384],{"class":135}," StringNum",[91,2386,2387],{"class":108}," { [",[91,2389,2390],{"class":408},"key",[91,2392,112],{"class":104},[91,2394,470],{"class":115},[91,2396,2397],{"class":108},"]",[91,2399,112],{"class":104},[91,2401,116],{"class":115},[91,2403,1124],{"class":108},[91,2405,2406,2408,2411,2413,2415,2417,2420,2422,2425,2427],{"class":93,"line":101},[91,2407,462],{"class":104},[91,2409,2410],{"class":115}," map",[91,2412,112],{"class":104},[91,2414,2384],{"class":135},[91,2416,119],{"class":104},[91,2418,2419],{"class":108}," { a: ",[91,2421,927],{"class":115},[91,2423,2424],{"class":108},", b: ",[91,2426,932],{"class":115},[91,2428,1124],{"class":108},[91,2430,2431,2434,2436,2438],{"class":93,"line":129},[91,2432,2433],{"class":108},"map.c ",[91,2435,476],{"class":104},[91,2437,1121],{"class":115},[91,2439,2440],{"class":97},"        \u002F\u002F 合法：任意字符串键 → number\n",[91,2442,2443,2446,2448,2451],{"class":93,"line":453},[91,2444,2445],{"class":108},"map.d ",[91,2447,476],{"class":104},[91,2449,2450],{"class":122}," 'x'",[91,2452,2453],{"class":97},"      \u002F\u002F ✗ 值必须是 number\n",[91,2455,2456],{"class":93,"line":459},[91,2457,567],{"emptyLinePlaceholder":566},[91,2459,2460],{"class":93,"line":611},[91,2461,2462],{"class":97},"\u002F\u002F 数字索引：数组本质上就是 { [index: number]: T } 的特例\n",[15,2464,2465,2466,2469],{},"数字键的索引签名比字符串键更严格：",[36,2467,2468],{},"[key: number]: T"," 只约束数字下标访问。",[28,2471,2472],{"id":2472},"函数类型与可调用对象混合",[15,2474,2475],{},"一个东西既能被调用、又带属性，用「调用签名 + 属性」混合写法：",[82,2477,2479],{"className":84,"code":2478,"language":86,"meta":87,"style":87},"interface Counter {\n  (start: number): string     \u002F\u002F 调用签名：它是个函数\n  interval: number            \u002F\u002F 同时又带属性\n  reset(): void\n}\nfunction getCounter(): Counter {\n  const c = ((start: number) => String(start)) as Counter\n  c.interval = 10\n  c.reset = () => {}\n  return c\n}\n",[36,2480,2481,2490,2511,2523,2535,2539,2554,2589,2600,2619,2627],{"__ignoreMap":87},[91,2482,2483,2485,2488],{"class":93,"line":94},[91,2484,189],{"class":104},[91,2486,2487],{"class":135}," Counter",[91,2489,1723],{"class":108},[91,2491,2492,2495,2498,2500,2502,2504,2506,2508],{"class":93,"line":101},[91,2493,2494],{"class":108},"  (",[91,2496,2497],{"class":408},"start",[91,2499,112],{"class":104},[91,2501,116],{"class":115},[91,2503,426],{"class":108},[91,2505,112],{"class":104},[91,2507,470],{"class":115},[91,2509,2510],{"class":97},"     \u002F\u002F 调用签名：它是个函数\n",[91,2512,2513,2516,2518,2520],{"class":93,"line":129},[91,2514,2515],{"class":408},"  interval",[91,2517,112],{"class":104},[91,2519,116],{"class":115},[91,2521,2522],{"class":97},"            \u002F\u002F 同时又带属性\n",[91,2524,2525,2528,2530,2532],{"class":93,"line":453},[91,2526,2527],{"class":135},"  reset",[91,2529,1624],{"class":108},[91,2531,112],{"class":104},[91,2533,2534],{"class":115}," void\n",[91,2536,2537],{"class":93,"line":459},[91,2538,456],{"class":108},[91,2540,2541,2543,2546,2548,2550,2552],{"class":93,"line":611},[91,2542,399],{"class":104},[91,2544,2545],{"class":135}," getCounter",[91,2547,1624],{"class":108},[91,2549,112],{"class":104},[91,2551,2487],{"class":135},[91,2553,1723],{"class":108},[91,2555,2556,2559,2561,2563,2566,2568,2570,2572,2575,2578,2581,2584,2586],{"class":93,"line":1494},[91,2557,2558],{"class":104},"  const",[91,2560,2323],{"class":115},[91,2562,119],{"class":104},[91,2564,2565],{"class":108}," ((",[91,2567,2497],{"class":408},[91,2569,112],{"class":104},[91,2571,116],{"class":115},[91,2573,2574],{"class":108},") ",[91,2576,2577],{"class":104},"=>",[91,2579,2580],{"class":135}," String",[91,2582,2583],{"class":108},"(start)) ",[91,2585,195],{"class":104},[91,2587,2588],{"class":135}," Counter\n",[91,2590,2592,2595,2597],{"class":93,"line":2591},8,[91,2593,2594],{"class":108},"  c.interval ",[91,2596,476],{"class":104},[91,2598,2599],{"class":115}," 10\n",[91,2601,2603,2606,2609,2611,2614,2616],{"class":93,"line":2602},9,[91,2604,2605],{"class":108},"  c.",[91,2607,2608],{"class":135},"reset",[91,2610,119],{"class":104},[91,2612,2613],{"class":108}," () ",[91,2615,2577],{"class":104},[91,2617,2618],{"class":108}," {}\n",[91,2620,2622,2624],{"class":93,"line":2621},10,[91,2623,441],{"class":104},[91,2625,2626],{"class":108}," c\n",[91,2628,2630],{"class":93,"line":2629},11,[91,2631,456],{"class":108},[15,2633,2634,2635,2638],{},"这类写法在描述「函数上挂了些元信息」的库 API 时很常见。函数本身的类型还可以直接写成 ",[36,2636,2637],{},"type Fn = (a: number) => number","，第六章细讲函数类型。",[10,2640,2642],{"id":2641},"五联合类型交叉类型与可辨识联合","五、联合类型、交叉类型与可辨识联合",[15,2644,2645,2646,2649],{},"联合和交叉是 TS 类型系统里最像「代数运算」的两个东西：",[19,2647,2648],{},"联合是「或」，交叉是「且」","。掌握它们，尤其是联合的收窄玩法，是从「会用 TS」到「用得好 TS」的分水岭。",[28,2651,2653,2654,2657],{"id":2652},"联合类型-a-b值要么是-a-要么是-b","联合类型 ",[36,2655,2656],{},"A | B","：值要么是 A 要么是 B",[82,2659,2661],{"className":84,"code":2660,"language":86,"meta":87,"style":87},"type ID = string | number\nfunction render(id: ID) {\n  id.toString()        \u002F\u002F ✓ string 和 number 都有 toString，取交集成员\n  id.length            \u002F\u002F ✗ number 上没有 length，联合里不能直接用某一分支独有的成员\n  id.toUpperCase()     \u002F\u002F ✗ 同理，未收窄前只能用「所有分支都保证」的成员\n}\n",[36,2662,2663,2678,2696,2709,2719,2731],{"__ignoreMap":87},[91,2664,2665,2667,2669,2671,2673,2675],{"class":93,"line":94},[91,2666,1213],{"class":104},[91,2668,2065],{"class":135},[91,2670,119],{"class":104},[91,2672,470],{"class":115},[91,2674,2072],{"class":104},[91,2676,2677],{"class":115}," number\n",[91,2679,2680,2682,2685,2687,2690,2692,2694],{"class":93,"line":101},[91,2681,399],{"class":104},[91,2683,2684],{"class":135}," render",[91,2686,405],{"class":108},[91,2688,2689],{"class":408},"id",[91,2691,112],{"class":104},[91,2693,2065],{"class":135},[91,2695,1474],{"class":108},[91,2697,2698,2701,2704,2706],{"class":93,"line":129},[91,2699,2700],{"class":108},"  id.",[91,2702,2703],{"class":135},"toString",[91,2705,1484],{"class":108},[91,2707,2708],{"class":97},"\u002F\u002F ✓ string 和 number 都有 toString，取交集成员\n",[91,2710,2711,2713,2716],{"class":93,"line":453},[91,2712,2700],{"class":108},[91,2714,2715],{"class":115},"length",[91,2717,2718],{"class":97},"            \u002F\u002F ✗ number 上没有 length，联合里不能直接用某一分支独有的成员\n",[91,2720,2721,2723,2725,2728],{"class":93,"line":459},[91,2722,2700],{"class":108},[91,2724,142],{"class":135},[91,2726,2727],{"class":108},"()     ",[91,2729,2730],{"class":97},"\u002F\u002F ✗ 同理，未收窄前只能用「所有分支都保证」的成员\n",[91,2732,2733],{"class":93,"line":611},[91,2734,456],{"class":108},[15,2736,2737,2738,2741,2742,2745],{},"联合类型在",[19,2739,2740],{},"没收窄之前","，只允许访问所有分支",[19,2743,2744],{},"共有","的成员（是各分支成员的交集）。要访问某一分支独有的，得先把它收窄成那个分支——第七章会系统讲收窄，这里先看联合的基本脾气。",[15,2747,2748,2749,186,2752,2755,2756,2759],{},"给联合赋值时反过来：任何一支的值都能赋进去，",[36,2750,2751],{},"id = 'a'",[36,2753,2754],{},"id = 1"," 都合法；但从联合",[19,2757,2758],{},"取用","时受限。这就是它的核心张力。",[28,2761,2763,2764,2767],{"id":2762},"交叉类型-a-b同时满足两边","交叉类型 ",[36,2765,2766],{},"A & B","：同时满足两边",[82,2769,2771],{"className":84,"code":2770,"language":86,"meta":87,"style":87},"type HasName = { name: string }\ntype HasAge = { age: number }\ntype Person = HasName & HasAge     \u002F\u002F { name: string; age: number }\nconst p: Person = { name: '阿蓝', age: 18 }\n",[36,2772,2773,2792,2811,2829],{"__ignoreMap":87},[91,2774,2775,2777,2780,2782,2784,2786,2788,2790],{"class":93,"line":94},[91,2776,1213],{"class":104},[91,2778,2779],{"class":135}," HasName",[91,2781,119],{"class":104},[91,2783,1085],{"class":108},[91,2785,2168],{"class":408},[91,2787,112],{"class":104},[91,2789,470],{"class":115},[91,2791,1124],{"class":108},[91,2793,2794,2796,2799,2801,2803,2805,2807,2809],{"class":93,"line":101},[91,2795,1213],{"class":104},[91,2797,2798],{"class":135}," HasAge",[91,2800,119],{"class":104},[91,2802,1085],{"class":108},[91,2804,2177],{"class":408},[91,2806,112],{"class":104},[91,2808,116],{"class":115},[91,2810,1124],{"class":108},[91,2812,2813,2815,2818,2820,2822,2824,2826],{"class":93,"line":129},[91,2814,1213],{"class":104},[91,2816,2817],{"class":135}," Person",[91,2819,119],{"class":104},[91,2821,2779],{"class":135},[91,2823,1659],{"class":104},[91,2825,2798],{"class":135},[91,2827,2828],{"class":97},"     \u002F\u002F { name: string; age: number }\n",[91,2830,2831,2833,2835,2837,2839,2841,2843,2845,2848,2851],{"class":93,"line":453},[91,2832,462],{"class":104},[91,2834,1875],{"class":115},[91,2836,112],{"class":104},[91,2838,2817],{"class":135},[91,2840,119],{"class":104},[91,2842,2199],{"class":108},[91,2844,2202],{"class":122},[91,2846,2847],{"class":108},", age: ",[91,2849,2850],{"class":115},"18",[91,2852,1124],{"class":108},[15,2854,2855,2856,2858,2859,2861,2862,2864],{},"交叉是把多个形状「叠加」成一个更具体的形状，字段全都要满足。接口之间用 ",[36,2857,1986],{},"、type 之间用 ",[36,2860,1993],{},"，效果对对象而言基本一致。当两支有同名不同类型字段时会得到 ",[36,2863,828],{},"，这也是「类型层面的空集」的一个来源。",[789,2866],{"height":1805,"src":2867,"title":2868},"\u002Ftutorials\u002Fts\u002Fdemos\u002F05-union-discriminated.html","示例 05：联合、交叉与可辨识联合的穷尽检查",[28,2870,2872],{"id":2871},"可辨识联合ts-里最好用的模式","可辨识联合：TS 里最好用的模式",[15,2874,2875,2876,2879,2880,2883],{},"如果联合的每一支都带一个",[19,2877,2878],{},"字面量类型的「标签」字段","，TS 就能凭这个标签把分支自动区分开——这叫 ",[19,2881,2882],{},"discriminated union","，是整个 TS 里最被推崇的建模手法：",[82,2885,2887],{"className":84,"code":2886,"language":86,"meta":87,"style":87},"type Shape =\n  | { kind: 'circle'; r: number }\n  | { kind: 'rect'; w: number; h: number }\n  | { kind: 'triangle'; base: number; height: number }\n\nfunction area(s: Shape): number {\n  switch (s.kind) {\n    case 'circle':   return Math.PI * s.r ** 2          \u002F\u002F 这支里 s 收窄成 circle\n    case 'rect':     return s.w * s.h                   \u002F\u002F 只能访问 w\u002Fh\n    case 'triangle': return (s.base * s.height) \u002F 2     \u002F\u002F 只能访问 base\u002Fheight\n  }\n}\n",[36,2888,2889,2899,2925,2958,2991,2995,3019,3027,3059,3082,3108,3113],{"__ignoreMap":87},[91,2890,2891,2893,2896],{"class":93,"line":94},[91,2892,1213],{"class":104},[91,2894,2895],{"class":135}," Shape",[91,2897,2898],{"class":104}," =\n",[91,2900,2901,2904,2906,2909,2911,2914,2916,2919,2921,2923],{"class":93,"line":101},[91,2902,2903],{"class":104},"  |",[91,2905,1085],{"class":108},[91,2907,2908],{"class":408},"kind",[91,2910,112],{"class":104},[91,2912,2913],{"class":122}," 'circle'",[91,2915,1859],{"class":108},[91,2917,2918],{"class":408},"r",[91,2920,112],{"class":104},[91,2922,116],{"class":115},[91,2924,1124],{"class":108},[91,2926,2927,2929,2931,2933,2935,2938,2940,2943,2945,2947,2949,2952,2954,2956],{"class":93,"line":129},[91,2928,2903],{"class":104},[91,2930,1085],{"class":108},[91,2932,2908],{"class":408},[91,2934,112],{"class":104},[91,2936,2937],{"class":122}," 'rect'",[91,2939,1859],{"class":108},[91,2941,2942],{"class":408},"w",[91,2944,112],{"class":104},[91,2946,116],{"class":115},[91,2948,1859],{"class":108},[91,2950,2951],{"class":408},"h",[91,2953,112],{"class":104},[91,2955,116],{"class":115},[91,2957,1124],{"class":108},[91,2959,2960,2962,2964,2966,2968,2971,2973,2976,2978,2980,2982,2985,2987,2989],{"class":93,"line":453},[91,2961,2903],{"class":104},[91,2963,1085],{"class":108},[91,2965,2908],{"class":408},[91,2967,112],{"class":104},[91,2969,2970],{"class":122}," 'triangle'",[91,2972,1859],{"class":108},[91,2974,2975],{"class":408},"base",[91,2977,112],{"class":104},[91,2979,116],{"class":115},[91,2981,1859],{"class":108},[91,2983,2984],{"class":408},"height",[91,2986,112],{"class":104},[91,2988,116],{"class":115},[91,2990,1124],{"class":108},[91,2992,2993],{"class":93,"line":459},[91,2994,567],{"emptyLinePlaceholder":566},[91,2996,2997,2999,3002,3004,3007,3009,3011,3013,3015,3017],{"class":93,"line":611},[91,2998,399],{"class":104},[91,3000,3001],{"class":135}," area",[91,3003,405],{"class":108},[91,3005,3006],{"class":408},"s",[91,3008,112],{"class":104},[91,3010,2895],{"class":135},[91,3012,426],{"class":108},[91,3014,112],{"class":104},[91,3016,116],{"class":115},[91,3018,1723],{"class":108},[91,3020,3021,3024],{"class":93,"line":1494},[91,3022,3023],{"class":104},"  switch",[91,3025,3026],{"class":108}," (s.kind) {\n",[91,3028,3029,3032,3034,3037,3039,3042,3045,3048,3051,3054,3056],{"class":93,"line":2591},[91,3030,3031],{"class":104},"    case",[91,3033,2913],{"class":122},[91,3035,3036],{"class":108},":   ",[91,3038,802],{"class":104},[91,3040,3041],{"class":108}," Math.",[91,3043,3044],{"class":115},"PI",[91,3046,3047],{"class":104}," *",[91,3049,3050],{"class":108}," s.r ",[91,3052,3053],{"class":104},"**",[91,3055,1111],{"class":115},[91,3057,3058],{"class":97},"          \u002F\u002F 这支里 s 收窄成 circle\n",[91,3060,3061,3063,3065,3068,3070,3073,3076,3079],{"class":93,"line":2602},[91,3062,3031],{"class":104},[91,3064,2937],{"class":122},[91,3066,3067],{"class":108},":     ",[91,3069,802],{"class":104},[91,3071,3072],{"class":108}," s.w ",[91,3074,3075],{"class":104},"*",[91,3077,3078],{"class":108}," s.h                   ",[91,3080,3081],{"class":97},"\u002F\u002F 只能访问 w\u002Fh\n",[91,3083,3084,3086,3088,3091,3093,3096,3098,3101,3103,3105],{"class":93,"line":2621},[91,3085,3031],{"class":104},[91,3087,2970],{"class":122},[91,3089,3090],{"class":108},": ",[91,3092,802],{"class":104},[91,3094,3095],{"class":108}," (s.base ",[91,3097,3075],{"class":104},[91,3099,3100],{"class":108}," s.height) ",[91,3102,729],{"class":104},[91,3104,1111],{"class":115},[91,3106,3107],{"class":97},"     \u002F\u002F 只能访问 base\u002Fheight\n",[91,3109,3110],{"class":93,"line":2629},[91,3111,3112],{"class":108},"  }\n",[91,3114,3116],{"class":93,"line":3115},12,[91,3117,456],{"class":108},[15,3119,3120,3121,3123,3124,3127,3128,3131,3132,3135,3136,3139,3140,3142,3143,186,3146,3149,3150,3157,3158,3160],{},"关键在于 ",[36,3122,2908],{}," 是字面量类型（",[36,3125,3126],{},"'circle'"," 而非 ",[36,3129,3130],{},"'string'","）。在 ",[36,3133,3134],{},"switch (s.kind)"," 的每个 ",[36,3137,3138],{},"case"," 里，",[36,3141,3006],{}," 会自动收窄到对应那一支，于是 ",[36,3144,3145],{},"s.r",[36,3147,3148],{},"s.w"," 这些专属字段都变得类型安全，不需要任何断言。",[19,3151,3152,3153,3156],{},"没有标签字段（",[36,3154,3155],{},"kind: string","）这一整套就全塌了","——这就是第九章「把值当类型用」和第二章 ",[36,3159,462],{}," 窄化真正兑现价值的地方。",[28,3162,3164,3165,3167],{"id":3163},"穷尽检查用-never-兜底","穷尽检查：用 ",[36,3166,828],{}," 兜底",[15,3169,3170,3171,3174,3175,3178,3179,3182,3183,3186,3187,3189],{},"上面的 ",[36,3172,3173],{},"area"," 还差一步：万一以后给 ",[36,3176,3177],{},"Shape"," 加了 ",[36,3180,3181],{},"case 'polygon'"," 却忘了在 ",[36,3184,3185],{},"switch"," 里处理怎么办？用 ",[36,3188,828],{}," 兜底：",[82,3191,3193],{"className":84,"code":3192,"language":86,"meta":87,"style":87},"function area(s: Shape): number {\n  switch (s.kind) {\n    case 'circle': return Math.PI * s.r ** 2\n    case 'rect': return s.w * s.h\n    case 'triangle': return (s.base * s.height) \u002F 2\n    default:\n      const _exhaustive: never = s   \u002F\u002F 若上面没覆盖全，s 还有剩余类型，赋给 never 就报错\n      return _exhaustive\n  }\n}\n",[36,3194,3195,3217,3223,3246,3263,3283,3291,3311,3319,3323],{"__ignoreMap":87},[91,3196,3197,3199,3201,3203,3205,3207,3209,3211,3213,3215],{"class":93,"line":94},[91,3198,399],{"class":104},[91,3200,3001],{"class":135},[91,3202,405],{"class":108},[91,3204,3006],{"class":408},[91,3206,112],{"class":104},[91,3208,2895],{"class":135},[91,3210,426],{"class":108},[91,3212,112],{"class":104},[91,3214,116],{"class":115},[91,3216,1723],{"class":108},[91,3218,3219,3221],{"class":93,"line":101},[91,3220,3023],{"class":104},[91,3222,3026],{"class":108},[91,3224,3225,3227,3229,3231,3233,3235,3237,3239,3241,3243],{"class":93,"line":129},[91,3226,3031],{"class":104},[91,3228,2913],{"class":122},[91,3230,3090],{"class":108},[91,3232,802],{"class":104},[91,3234,3041],{"class":108},[91,3236,3044],{"class":115},[91,3238,3047],{"class":104},[91,3240,3050],{"class":108},[91,3242,3053],{"class":104},[91,3244,3245],{"class":115}," 2\n",[91,3247,3248,3250,3252,3254,3256,3258,3260],{"class":93,"line":453},[91,3249,3031],{"class":104},[91,3251,2937],{"class":122},[91,3253,3090],{"class":108},[91,3255,802],{"class":104},[91,3257,3072],{"class":108},[91,3259,3075],{"class":104},[91,3261,3262],{"class":108}," s.h\n",[91,3264,3265,3267,3269,3271,3273,3275,3277,3279,3281],{"class":93,"line":459},[91,3266,3031],{"class":104},[91,3268,2970],{"class":122},[91,3270,3090],{"class":108},[91,3272,802],{"class":104},[91,3274,3095],{"class":108},[91,3276,3075],{"class":104},[91,3278,3100],{"class":108},[91,3280,729],{"class":104},[91,3282,3245],{"class":115},[91,3284,3285,3288],{"class":93,"line":611},[91,3286,3287],{"class":104},"    default",[91,3289,3290],{"class":108},":\n",[91,3292,3293,3296,3299,3301,3303,3305,3308],{"class":93,"line":1494},[91,3294,3295],{"class":104},"      const",[91,3297,3298],{"class":115}," _exhaustive",[91,3300,112],{"class":104},[91,3302,1590],{"class":115},[91,3304,119],{"class":104},[91,3306,3307],{"class":108}," s   ",[91,3309,3310],{"class":97},"\u002F\u002F 若上面没覆盖全，s 还有剩余类型，赋给 never 就报错\n",[91,3312,3313,3316],{"class":93,"line":2591},[91,3314,3315],{"class":104},"      return",[91,3317,3318],{"class":108}," _exhaustive\n",[91,3320,3321],{"class":93,"line":2602},[91,3322,3112],{"class":108},[91,3324,3325],{"class":93,"line":2621},[91,3326,456],{"class":108},[15,3328,3329,3330,504,3333,3335,3336,3338,3339,3341,3342,3351,3352,3354],{},"处理完全部分支后，",[36,3331,3332],{},"default",[36,3334,3006],{}," 类型应当是 ",[36,3337,828],{},"（所有可能都被 case 吃掉了），赋给一个 ",[36,3340,828],{}," 变量正好成立。",[19,3343,3344,3345,3347,3348,3350],{},"一旦你漏了某一支，",[36,3346,3006],{}," 就不是 ",[36,3349,828],{}," 而是那个没处理的具体分支类型，赋值报错","——你等于给编译器留了个哨兵，专门盯着「未来加了新分支但你忘了改这里」。业务里凡是 switch 一个可辨识联合，都建议加上这条 ",[36,3353,828],{}," 兜底。",[10,3356,3358,3359],{"id":3357},"六函数类型参数可选剩余重载与-this","六、函数类型：参数、可选、剩余、重载与 ",[36,3360,3361],{},"this",[15,3363,3364,3365,1685,3367,3369],{},"函数是 TS 里类型信息最密集的地方，也是注解写得最多的一处。JS 教程第六章讲过 JS 函数的行为（默认值、剩余参数、闭包捕获），这里不重复那些运行时语义，只讲类型怎么描述它们，以及几个 TS 特有的点：重载签名、",[36,3366,1688],{},[36,3368,1684],{}," 作为返回类型的差别。",[28,3371,3372],{"id":3372},"可选参数与默认值",[15,3374,3375,3376,3378,3379,3381],{},"参数上加 ",[36,3377,2142],{}," 表示可省略，它的类型随即变成 ",[36,3380,2151],{},"；给了默认值的参数自动是可选的，而且类型从默认值推断，通常不用再标：",[82,3383,3385],{"className":84,"code":3384,"language":86,"meta":87,"style":87},"function greet(name: string, title?: string) {\n  \u002F\u002F title: string | undefined\n  return title ? `${title} ${name}` : name\n}\nfunction greet2(name: string, title = '先生') {   \u002F\u002F title 推断为 string，且可选\n  return `${title} ${name}`\n}\n",[36,3386,3387,3413,3418,3446,3450,3479,3494],{"__ignoreMap":87},[91,3388,3389,3391,3394,3396,3398,3400,3402,3404,3407,3409,3411],{"class":93,"line":94},[91,3390,399],{"class":104},[91,3392,3393],{"class":135}," greet",[91,3395,405],{"class":108},[91,3397,2168],{"class":408},[91,3399,112],{"class":104},[91,3401,470],{"class":115},[91,3403,416],{"class":108},[91,3405,3406],{"class":408},"title",[91,3408,2180],{"class":104},[91,3410,470],{"class":115},[91,3412,1474],{"class":108},[91,3414,3415],{"class":93,"line":101},[91,3416,3417],{"class":97},"  \u002F\u002F title: string | undefined\n",[91,3419,3420,3422,3425,3427,3430,3432,3435,3437,3440,3443],{"class":93,"line":129},[91,3421,441],{"class":104},[91,3423,3424],{"class":108}," title ",[91,3426,2142],{"class":104},[91,3428,3429],{"class":122}," `${",[91,3431,3406],{"class":108},[91,3433,3434],{"class":122},"} ${",[91,3436,2168],{"class":108},[91,3438,3439],{"class":122},"}`",[91,3441,3442],{"class":104}," :",[91,3444,3445],{"class":108}," name\n",[91,3447,3448],{"class":93,"line":453},[91,3449,456],{"class":108},[91,3451,3452,3454,3457,3459,3461,3463,3465,3467,3469,3471,3474,3476],{"class":93,"line":459},[91,3453,399],{"class":104},[91,3455,3456],{"class":135}," greet2",[91,3458,405],{"class":108},[91,3460,2168],{"class":408},[91,3462,112],{"class":104},[91,3464,470],{"class":115},[91,3466,416],{"class":108},[91,3468,3406],{"class":408},[91,3470,119],{"class":104},[91,3472,3473],{"class":122}," '先生'",[91,3475,586],{"class":108},[91,3477,3478],{"class":97},"\u002F\u002F title 推断为 string，且可选\n",[91,3480,3481,3483,3485,3487,3489,3491],{"class":93,"line":611},[91,3482,441],{"class":104},[91,3484,3429],{"class":122},[91,3486,3406],{"class":108},[91,3488,3434],{"class":122},[91,3490,2168],{"class":108},[91,3492,3493],{"class":122},"}`\n",[91,3495,3496],{"class":93,"line":1494},[91,3497,456],{"class":108},[15,3499,3500,3501,3504],{},"注意一个顺序规则：",[19,3502,3503],{},"可选参数和带默认值的参数要排在必选参数后面","，否则调用时没法靠位置区分。",[28,3506,3507],{"id":3507},"剩余参数",[15,3509,3510],{},"剩余参数在类型上写成一个数组类型，泛型写法更常见：",[82,3512,3514],{"className":84,"code":3513,"language":86,"meta":87,"style":87},"function sum(...nums: number[]): number {\n  return nums.reduce((a, b) => a + b, 0)\n}\nfunction join(sep: string, ...parts: string[]) { \u002F* parts: string[] *\u002F }\n",[36,3515,3516,3544,3579,3583],{"__ignoreMap":87},[91,3517,3518,3520,3523,3525,3528,3531,3533,3535,3538,3540,3542],{"class":93,"line":94},[91,3519,399],{"class":104},[91,3521,3522],{"class":135}," sum",[91,3524,405],{"class":108},[91,3526,3527],{"class":104},"...",[91,3529,3530],{"class":408},"nums",[91,3532,112],{"class":104},[91,3534,116],{"class":115},[91,3536,3537],{"class":108},"[])",[91,3539,112],{"class":104},[91,3541,116],{"class":115},[91,3543,1723],{"class":108},[91,3545,3546,3548,3551,3554,3557,3559,3561,3563,3565,3567,3569,3571,3574,3576],{"class":93,"line":101},[91,3547,441],{"class":104},[91,3549,3550],{"class":108}," nums.",[91,3552,3553],{"class":135},"reduce",[91,3555,3556],{"class":108},"((",[91,3558,409],{"class":408},[91,3560,416],{"class":108},[91,3562,419],{"class":408},[91,3564,2574],{"class":108},[91,3566,2577],{"class":104},[91,3568,444],{"class":108},[91,3570,447],{"class":104},[91,3572,3573],{"class":108}," b, ",[91,3575,1147],{"class":115},[91,3577,3578],{"class":108},")\n",[91,3580,3581],{"class":93,"line":129},[91,3582,456],{"class":108},[91,3584,3585,3587,3590,3592,3595,3597,3599,3601,3603,3606,3608,3610,3613,3616],{"class":93,"line":453},[91,3586,399],{"class":104},[91,3588,3589],{"class":135}," join",[91,3591,405],{"class":108},[91,3593,3594],{"class":408},"sep",[91,3596,112],{"class":104},[91,3598,470],{"class":115},[91,3600,416],{"class":108},[91,3602,3527],{"class":104},[91,3604,3605],{"class":408},"parts",[91,3607,112],{"class":104},[91,3609,470],{"class":115},[91,3611,3612],{"class":108},"[]) { ",[91,3614,3615],{"class":97},"\u002F* parts: string[] *\u002F",[91,3617,1124],{"class":108},[15,3619,3620,3621,74],{},"元组也能描述「固定前缀 + 可变后缀」的签名：",[36,3622,3623],{},"(...args: [string, ...number[]]) => void",[28,3625,3626],{"id":3626},"函数类型表达式",[15,3628,3629,3630,211],{},"函数本身可以当成一个类型来命名和传递，用 ",[36,3631,3632],{},"(参数: 类型) => 返回",[82,3634,3636],{"className":84,"code":3635,"language":86,"meta":87,"style":87},"type Comparator = (a: string, b: string) => number\nfunction sortBy(arr: string[], cmp: Comparator) { \u002F* ... *\u002F }\n\nconst byLen: Comparator = (a, b) => a.length - b.length   \u002F\u002F cmp 的类型已知，a\u002Fb 自动推断\n",[36,3637,3638,3669,3703,3707],{"__ignoreMap":87},[91,3639,3640,3642,3645,3647,3649,3651,3653,3655,3657,3659,3661,3663,3665,3667],{"class":93,"line":94},[91,3641,1213],{"class":104},[91,3643,3644],{"class":135}," Comparator",[91,3646,119],{"class":104},[91,3648,1220],{"class":108},[91,3650,409],{"class":408},[91,3652,112],{"class":104},[91,3654,470],{"class":115},[91,3656,416],{"class":108},[91,3658,419],{"class":408},[91,3660,112],{"class":104},[91,3662,470],{"class":115},[91,3664,2574],{"class":108},[91,3666,2577],{"class":104},[91,3668,2677],{"class":115},[91,3670,3671,3673,3676,3678,3681,3683,3685,3688,3691,3693,3695,3698,3701],{"class":93,"line":101},[91,3672,399],{"class":104},[91,3674,3675],{"class":135}," sortBy",[91,3677,405],{"class":108},[91,3679,3680],{"class":408},"arr",[91,3682,112],{"class":104},[91,3684,470],{"class":115},[91,3686,3687],{"class":108},"[], ",[91,3689,3690],{"class":408},"cmp",[91,3692,112],{"class":104},[91,3694,3644],{"class":135},[91,3696,3697],{"class":108},") { ",[91,3699,3700],{"class":97},"\u002F* ... *\u002F",[91,3702,1124],{"class":108},[91,3704,3705],{"class":93,"line":129},[91,3706,567],{"emptyLinePlaceholder":566},[91,3708,3709,3711,3714,3716,3718,3720,3722,3724,3726,3728,3730,3732,3735,3737,3740,3743,3745],{"class":93,"line":453},[91,3710,462],{"class":104},[91,3712,3713],{"class":135}," byLen",[91,3715,112],{"class":104},[91,3717,3644],{"class":135},[91,3719,119],{"class":104},[91,3721,1220],{"class":108},[91,3723,409],{"class":408},[91,3725,416],{"class":108},[91,3727,419],{"class":408},[91,3729,2574],{"class":108},[91,3731,2577],{"class":104},[91,3733,3734],{"class":108}," a.",[91,3736,2715],{"class":115},[91,3738,3739],{"class":104}," -",[91,3741,3742],{"class":108}," b.",[91,3744,2715],{"class":115},[91,3746,3747],{"class":97},"   \u002F\u002F cmp 的类型已知，a\u002Fb 自动推断\n",[15,3749,3750,3751,3753,3754,3757],{},"把函数签名抽成 ",[36,3752,1213],{}," 别名，是回调型 API（",[36,3755,3756],{},"Array.prototype.map"," 的 callback、事件处理器）反复用到的写法，也让「传进去的函数长什么样」一目了然。",[789,3759],{"height":791,"src":3760,"title":3761},"\u002Ftutorials\u002Fts\u002Fdemos\u002F06-function-types.html","示例 06：函数类型——参数、返回与推断的边界",[28,3763,3765],{"id":3764},"重载多个签名-一个实现","重载：多个签名 + 一个实现",[15,3767,3768,3769,211],{},"当一个函数「传不同类型进去、返回不同类型出来」，用重载。语法是",[19,3770,3771],{},"先列多个对外签名，再写一个对内的实现签名",[82,3773,3775],{"className":84,"code":3774,"language":86,"meta":87,"style":87},"function pick(x: string): string\nfunction pick(x: number): number\nfunction pick(x: string | number): string | number {   \u002F\u002F 实现签名不对外暴露\n  return x\n}\npick('a')   \u002F\u002F 返回 string\npick(1)     \u002F\u002F 返回 number\n",[36,3776,3777,3799,3819,3852,3859,3863,3877],{"__ignoreMap":87},[91,3778,3779,3781,3784,3786,3788,3790,3792,3794,3796],{"class":93,"line":94},[91,3780,399],{"class":104},[91,3782,3783],{"class":135}," pick",[91,3785,405],{"class":108},[91,3787,1852],{"class":408},[91,3789,112],{"class":104},[91,3791,470],{"class":115},[91,3793,426],{"class":108},[91,3795,112],{"class":104},[91,3797,3798],{"class":115}," string\n",[91,3800,3801,3803,3805,3807,3809,3811,3813,3815,3817],{"class":93,"line":101},[91,3802,399],{"class":104},[91,3804,3783],{"class":135},[91,3806,405],{"class":108},[91,3808,1852],{"class":408},[91,3810,112],{"class":104},[91,3812,116],{"class":115},[91,3814,426],{"class":108},[91,3816,112],{"class":104},[91,3818,2677],{"class":115},[91,3820,3821,3823,3825,3827,3829,3831,3833,3835,3837,3839,3841,3843,3845,3847,3849],{"class":93,"line":129},[91,3822,399],{"class":104},[91,3824,3783],{"class":135},[91,3826,405],{"class":108},[91,3828,1852],{"class":408},[91,3830,112],{"class":104},[91,3832,470],{"class":115},[91,3834,2072],{"class":104},[91,3836,116],{"class":115},[91,3838,426],{"class":108},[91,3840,112],{"class":104},[91,3842,470],{"class":115},[91,3844,2072],{"class":104},[91,3846,116],{"class":115},[91,3848,433],{"class":108},[91,3850,3851],{"class":97},"\u002F\u002F 实现签名不对外暴露\n",[91,3853,3854,3856],{"class":93,"line":453},[91,3855,441],{"class":104},[91,3857,3858],{"class":108}," x\n",[91,3860,3861],{"class":93,"line":459},[91,3862,456],{"class":108},[91,3864,3865,3868,3870,3872,3874],{"class":93,"line":611},[91,3866,3867],{"class":135},"pick",[91,3869,405],{"class":108},[91,3871,482],{"class":122},[91,3873,1309],{"class":108},[91,3875,3876],{"class":97},"\u002F\u002F 返回 string\n",[91,3878,3879,3881,3883,3885,3888],{"class":93,"line":1494},[91,3880,3867],{"class":135},[91,3882,405],{"class":108},[91,3884,927],{"class":115},[91,3886,3887],{"class":108},")     ",[91,3889,3890],{"class":97},"\u002F\u002F 返回 number\n",[15,3892,3893,3894,3897],{},"要点：",[19,3895,3896],{},"实现签名必须能兼容所有对外签名","，且调用方只能看见对外的那几个签名，看不见实现签名。外部调用如果落不进任何对外签名，就报错。重载写起来啰嗦，现代 TS 里很多场景可以用「泛型 + 条件返回类型」（第十一、十四章）替代，更简洁也更不容易出错。",[28,3899,3901,3902],{"id":3900},"箭头函数与-this","箭头函数与 ",[36,3903,3361],{},[15,3905,3906,3907,3912,3913,3915,3916,3918,3919,3921,3922,3925,3926,3929,3930,3932],{},"箭头函数",[19,3908,3909,3910],{},"没有自己的 ",[36,3911,3361],{},"，它捕获定义时外层的 ",[36,3914,3361],{},"。这在类型上表现为：普通函数的 ",[36,3917,3361],{}," 默认是 ",[36,3920,513],{},"（严格模式下需要 ",[36,3923,3924],{},"ThisType"," 或显式 ",[36,3927,3928],{},"this: T"," 参数），而箭头函数的 ",[36,3931,3361],{}," 从上下文来，无需注解：",[82,3934,3936],{"className":84,"code":3935,"language":86,"meta":87,"style":87},"const timer = {\n  seconds: 0,\n  start() {\n    setInterval(function () {\n      this.seconds++     \u002F\u002F ✗ 这里 this 是全局\u002Fwindow，不是 timer——经典坑\n    }, 1000)\n    setInterval(() => {\n      this.seconds++     \u002F\u002F ✓ 箭头函数捕获 timer 的 this，正确\n    }, 1000)\n  }\n}\n",[36,3937,3938,3949,3959,3967,3979,3993,4003,4014,4025,4033,4037],{"__ignoreMap":87},[91,3939,3940,3942,3945,3947],{"class":93,"line":94},[91,3941,462],{"class":104},[91,3943,3944],{"class":115}," timer",[91,3946,119],{"class":104},[91,3948,1723],{"class":108},[91,3950,3951,3954,3956],{"class":93,"line":101},[91,3952,3953],{"class":108},"  seconds: ",[91,3955,1147],{"class":115},[91,3957,3958],{"class":108},",\n",[91,3960,3961,3964],{"class":93,"line":129},[91,3962,3963],{"class":135},"  start",[91,3965,3966],{"class":108},"() {\n",[91,3968,3969,3972,3974,3976],{"class":93,"line":453},[91,3970,3971],{"class":135},"    setInterval",[91,3973,405],{"class":108},[91,3975,399],{"class":104},[91,3977,3978],{"class":108}," () {\n",[91,3980,3981,3984,3987,3990],{"class":93,"line":459},[91,3982,3983],{"class":115},"      this",[91,3985,3986],{"class":108},".seconds",[91,3988,3989],{"class":104},"++",[91,3991,3992],{"class":97},"     \u002F\u002F ✗ 这里 this 是全局\u002Fwindow，不是 timer——经典坑\n",[91,3994,3995,3998,4001],{"class":93,"line":611},[91,3996,3997],{"class":108},"    }, ",[91,3999,4000],{"class":115},"1000",[91,4002,3578],{"class":108},[91,4004,4005,4007,4010,4012],{"class":93,"line":1494},[91,4006,3971],{"class":135},[91,4008,4009],{"class":108},"(() ",[91,4011,2577],{"class":104},[91,4013,1723],{"class":108},[91,4015,4016,4018,4020,4022],{"class":93,"line":2591},[91,4017,3983],{"class":115},[91,4019,3986],{"class":108},[91,4021,3989],{"class":104},[91,4023,4024],{"class":97},"     \u002F\u002F ✓ 箭头函数捕获 timer 的 this，正确\n",[91,4026,4027,4029,4031],{"class":93,"line":2602},[91,4028,3997],{"class":108},[91,4030,4000],{"class":115},[91,4032,3578],{"class":108},[91,4034,4035],{"class":93,"line":2621},[91,4036,3112],{"class":108},[91,4038,4039],{"class":93,"line":2629},[91,4040,456],{"class":108},[15,4042,4043,4044,4046,4047,4049],{},"普通函数若要显式约束 ",[36,4045,3361],{},"，在参数表最前面加一个 ",[36,4048,3928],{},"（它不是真参数，调用时不占位）。这是 TS 提供的一个「只给 this 打类型」的特殊语法。",[28,4051,4053,1685,4055,4057],{"id":4052},"void-与-undefined-作返回类型的差别",[36,4054,1688],{},[36,4056,1684],{}," 作返回类型的差别",[15,4059,4060,4061,4063,4064,4066,4067,4070,4071,4074,4075,4077,4078,4081,4082,4084,4085,4087,4088,74],{},"两者细微不同，但会咬人：",[36,4062,1688],{}," 表示「调用方不关心返回值」，TS 对返回 ",[36,4065,1688],{}," 的函数",[19,4068,4069],{},"很宽容","——它可以返回任何东西，反正调用方承诺不读（比如 ",[36,4072,4073],{},"arr.forEach(() => x())","，回调返回什么都行）。而 ",[36,4076,1684],{}," 作返回类型则",[19,4079,4080],{},"严格","要求真的只返回 ",[36,4083,1684],{},"。所以当你想表达「随便返回啥但我不看」时，用 ",[36,4086,1688],{},"；想表达「它必须什么都不返回」时，才用 ",[36,4089,1684],{},[10,4091,4093],{"id":4092},"七类型收窄编译器怎么一步步相信你的判断","七、类型收窄：编译器怎么一步步相信你的判断",[15,4095,4096,4097,4100,4101,4103],{},"如果说这门课只能精读一章，就是这一章。",[19,4098,4099],{},"收窄（narrowing）是 TS 类型系统的发动机","：类型不会自己变精确，是你在代码里做的判断，让编译器在某个分支内把一个宽类型「收窄」成具体的窄类型。第五章的可辨识联合、第三章的 ",[36,4102,825],{}," 使用点，全都靠收窄落地。",[28,4105,4107],{"id":4106},"什么是收窄一个直观例子","什么是收窄：一个直观例子",[82,4109,4111],{"className":84,"code":4110,"language":86,"meta":87,"style":87},"function len(x: string | number) {\n  \u002F\u002F 这里 x 是 string | number，length\u002FtoFixed 都不能直接用\n  if (typeof x === 'string') {\n    return x.length       \u002F\u002F ✓ 这个分支里 x 被收窄成 string\n  }\n  return x.toFixed(2)     \u002F\u002F ✓ 走到 else，x 收窄成 number\n}\n",[36,4112,4113,4134,4139,4156,4169,4173,4190],{"__ignoreMap":87},[91,4114,4115,4117,4120,4122,4124,4126,4128,4130,4132],{"class":93,"line":94},[91,4116,399],{"class":104},[91,4118,4119],{"class":135}," len",[91,4121,405],{"class":108},[91,4123,1852],{"class":408},[91,4125,112],{"class":104},[91,4127,470],{"class":115},[91,4129,2072],{"class":104},[91,4131,116],{"class":115},[91,4133,1474],{"class":108},[91,4135,4136],{"class":93,"line":101},[91,4137,4138],{"class":97},"  \u002F\u002F 这里 x 是 string | number，length\u002FtoFixed 都不能直接用\n",[91,4140,4141,4144,4146,4148,4150,4152,4154],{"class":93,"line":129},[91,4142,4143],{"class":104},"  if",[91,4145,1220],{"class":108},[91,4147,1223],{"class":104},[91,4149,540],{"class":108},[91,4151,1468],{"class":104},[91,4153,1471],{"class":122},[91,4155,1474],{"class":108},[91,4157,4158,4161,4164,4166],{"class":93,"line":453},[91,4159,4160],{"class":104},"    return",[91,4162,4163],{"class":108}," x.",[91,4165,2715],{"class":115},[91,4167,4168],{"class":97},"       \u002F\u002F ✓ 这个分支里 x 被收窄成 string\n",[91,4170,4171],{"class":93,"line":459},[91,4172,3112],{"class":108},[91,4174,4175,4177,4179,4181,4183,4185,4187],{"class":93,"line":611},[91,4176,441],{"class":104},[91,4178,4163],{"class":108},[91,4180,2224],{"class":135},[91,4182,405],{"class":108},[91,4184,932],{"class":115},[91,4186,3887],{"class":108},[91,4188,4189],{"class":97},"\u002F\u002F ✓ 走到 else，x 收窄成 number\n",[91,4191,4192],{"class":93,"line":1494},[91,4193,456],{"class":108},[15,4195,4196,4198,4199,4202,4203,4206,4207,4209,4210,74],{},[36,4197,1223],{}," 这个判断本身在 JS 里就有，TS 只是",[19,4200,4201],{},"读懂了它的含义","：在 ",[36,4204,4205],{},"=== 'string'"," 为真的分支里，把 ",[36,4208,1852],{}," 的类型从联合里剩下的分支收窄。收窄不改变运行时的值，只改变",[19,4211,4212],{},"编译器在这一段里对它的看法",[28,4214,4216,4218],{"id":4215},"typeof-收窄",[36,4217,1223],{}," 收窄",[15,4220,4221,4222,186,4225,186,4228,186,4231,186,4234,186,4237,186,4240,4243,4244,4247,4248,4251,4252,74],{},"支持的字面量：",[36,4223,4224],{},"\"string\"",[36,4226,4227],{},"\"number\"",[36,4229,4230],{},"\"boolean\"",[36,4232,4233],{},"\"function\"",[36,4235,4236],{},"\"object\"",[36,4238,4239],{},"\"symbol\"",[36,4241,4242],{},"\"undefined\"","，以及 ",[36,4245,4246],{},"\"bigint\"","。注意 ",[36,4249,4250],{},"typeof null === 'object'"," 这条 JS 老 bug 也影响收窄——判对象为真还要再 ",[36,4253,4254],{},"x !== null",[28,4256,4258,4218],{"id":4257},"in-收窄",[36,4259,4260],{},"in",[15,4262,4263,4264,211],{},"用一个「只有某支才有」的属性名来区分，靠 ",[36,4265,4260],{},[82,4267,4269],{"className":84,"code":4268,"language":86,"meta":87,"style":87},"type Fish = { swim: () => void }\ntype Bird = { fly: () => void }\nfunction move(a: Fish | Bird) {\n  if ('fly' in a) a.fly()    \u002F\u002F ✓ 'fly' in a 为真时 a 收窄成 Bird\n  else a.swim()\n}\n",[36,4270,4271,4295,4319,4340,4363,4375],{"__ignoreMap":87},[91,4272,4273,4275,4278,4280,4282,4285,4287,4289,4291,4293],{"class":93,"line":94},[91,4274,1213],{"class":104},[91,4276,4277],{"class":135}," Fish",[91,4279,119],{"class":104},[91,4281,1085],{"class":108},[91,4283,4284],{"class":135},"swim",[91,4286,112],{"class":104},[91,4288,2613],{"class":108},[91,4290,2577],{"class":104},[91,4292,1720],{"class":115},[91,4294,1124],{"class":108},[91,4296,4297,4299,4302,4304,4306,4309,4311,4313,4315,4317],{"class":93,"line":101},[91,4298,1213],{"class":104},[91,4300,4301],{"class":135}," Bird",[91,4303,119],{"class":104},[91,4305,1085],{"class":108},[91,4307,4308],{"class":135},"fly",[91,4310,112],{"class":104},[91,4312,2613],{"class":108},[91,4314,2577],{"class":104},[91,4316,1720],{"class":115},[91,4318,1124],{"class":108},[91,4320,4321,4323,4326,4328,4330,4332,4334,4336,4338],{"class":93,"line":129},[91,4322,399],{"class":104},[91,4324,4325],{"class":135}," move",[91,4327,405],{"class":108},[91,4329,409],{"class":408},[91,4331,112],{"class":104},[91,4333,4277],{"class":135},[91,4335,2072],{"class":104},[91,4337,4301],{"class":135},[91,4339,1474],{"class":108},[91,4341,4342,4344,4346,4349,4352,4355,4357,4360],{"class":93,"line":453},[91,4343,4143],{"class":104},[91,4345,1220],{"class":108},[91,4347,4348],{"class":122},"'fly'",[91,4350,4351],{"class":104}," in",[91,4353,4354],{"class":108}," a) a.",[91,4356,4308],{"class":135},[91,4358,4359],{"class":108},"()    ",[91,4361,4362],{"class":97},"\u002F\u002F ✓ 'fly' in a 为真时 a 收窄成 Bird\n",[91,4364,4365,4368,4370,4372],{"class":93,"line":459},[91,4366,4367],{"class":104},"  else",[91,4369,3734],{"class":108},[91,4371,4284],{"class":135},[91,4373,4374],{"class":108},"()\n",[91,4376,4377],{"class":93,"line":611},[91,4378,456],{"class":108},[15,4380,4381,4383],{},[36,4382,4260],{}," 收窄的正是那种「没设计标签字段、但各支属性不同」的场合——不过更好的做法通常是回去把它改成可辨识联合。",[28,4385,4387,4218],{"id":4386},"instanceof-收窄",[36,4388,234],{},[15,4390,4391,4392,4394,4395,4398,4399,729,4401,4403,4404,4406],{},"对 class 实例，用 ",[36,4393,234],{}," 判断，收窄到对应类。因为 ",[36,4396,4397],{},"class"," 是运行时真实存在的，所以这条能用；而纯 ",[36,4400,189],{},[36,4402,1213],{}," 运行时不存在，",[36,4405,234],{}," 帮不上忙（第七章的这条限制，正是第一章「类型擦除」的直接后果）。",[82,4408,4410],{"className":84,"code":4409,"language":86,"meta":87,"style":87},"function describe(e: Error | Date) {\n  if (e instanceof Error) return e.message   \u002F\u002F 收窄成 Error\n  return e.getTime()                          \u002F\u002F 收窄成 Date\n}\n",[36,4411,4412,4435,4456,4472],{"__ignoreMap":87},[91,4413,4414,4416,4419,4421,4424,4426,4428,4430,4433],{"class":93,"line":94},[91,4415,399],{"class":104},[91,4417,4418],{"class":135}," describe",[91,4420,405],{"class":108},[91,4422,4423],{"class":408},"e",[91,4425,112],{"class":104},[91,4427,1607],{"class":135},[91,4429,2072],{"class":104},[91,4431,4432],{"class":135}," Date",[91,4434,1474],{"class":108},[91,4436,4437,4439,4442,4444,4446,4448,4450,4453],{"class":93,"line":101},[91,4438,4143],{"class":104},[91,4440,4441],{"class":108}," (e ",[91,4443,234],{"class":104},[91,4445,1607],{"class":135},[91,4447,2574],{"class":108},[91,4449,802],{"class":104},[91,4451,4452],{"class":108}," e.message   ",[91,4454,4455],{"class":97},"\u002F\u002F 收窄成 Error\n",[91,4457,4458,4460,4463,4466,4469],{"class":93,"line":129},[91,4459,441],{"class":104},[91,4461,4462],{"class":108}," e.",[91,4464,4465],{"class":135},"getTime",[91,4467,4468],{"class":108},"()                          ",[91,4470,4471],{"class":97},"\u002F\u002F 收窄成 Date\n",[91,4473,4474],{"class":93,"line":453},[91,4475,456],{"class":108},[28,4477,4478],{"id":4478},"真值收窄与相等收窄",[15,4480,4481,186,4484,186,4487,186,4490,4493,4494,186,4496,186,4499,186,4501,186,4503,4506],{},[36,4482,4483],{},"if (x)",[36,4485,4486],{},"x && ...",[36,4488,4489],{},"x === value",[36,4491,4492],{},"x !== value"," 这些都能收窄。真值收窄会把 ",[36,4495,1147],{},[36,4497,4498],{},"''",[36,4500,295],{},[36,4502,1684],{},[36,4504,4505],{},"NaN"," 这些假值从类型里剔除；相等收窄则配合字面量类型精确命中：",[82,4508,4510],{"className":84,"code":4509,"language":86,"meta":87,"style":87},"function f(s?: string) {\n  if (s) {\n    s.toUpperCase()   \u002F\u002F ✓ 排除了 undefined 和空串（string 仍在）\n  }\n}\ntype Status = 'a' | 'b' | 'c'\nfunction g(x: Status) {\n  if (x === 'a') { \u002F* x: 'a' *\u002F }\n  else { \u002F* x: 'b' | 'c' *\u002F }   \u002F\u002F 排除 'a' 后剩两支\n}\n",[36,4511,4512,4529,4536,4549,4553,4557,4579,4596,4614,4628],{"__ignoreMap":87},[91,4513,4514,4516,4519,4521,4523,4525,4527],{"class":93,"line":94},[91,4515,399],{"class":104},[91,4517,4518],{"class":135}," f",[91,4520,405],{"class":108},[91,4522,3006],{"class":408},[91,4524,2180],{"class":104},[91,4526,470],{"class":115},[91,4528,1474],{"class":108},[91,4530,4531,4533],{"class":93,"line":101},[91,4532,4143],{"class":104},[91,4534,4535],{"class":108}," (s) {\n",[91,4537,4538,4541,4543,4546],{"class":93,"line":129},[91,4539,4540],{"class":108},"    s.",[91,4542,142],{"class":135},[91,4544,4545],{"class":108},"()   ",[91,4547,4548],{"class":97},"\u002F\u002F ✓ 排除了 undefined 和空串（string 仍在）\n",[91,4550,4551],{"class":93,"line":453},[91,4552,3112],{"class":108},[91,4554,4555],{"class":93,"line":459},[91,4556,456],{"class":108},[91,4558,4559,4561,4564,4566,4569,4571,4574,4576],{"class":93,"line":611},[91,4560,1213],{"class":104},[91,4562,4563],{"class":135}," Status",[91,4565,119],{"class":104},[91,4567,4568],{"class":122}," 'a'",[91,4570,2072],{"class":104},[91,4572,4573],{"class":122}," 'b'",[91,4575,2072],{"class":104},[91,4577,4578],{"class":122}," 'c'\n",[91,4580,4581,4583,4586,4588,4590,4592,4594],{"class":93,"line":1494},[91,4582,399],{"class":104},[91,4584,4585],{"class":135}," g",[91,4587,405],{"class":108},[91,4589,1852],{"class":408},[91,4591,112],{"class":104},[91,4593,4563],{"class":135},[91,4595,1474],{"class":108},[91,4597,4598,4600,4603,4605,4607,4609,4612],{"class":93,"line":2591},[91,4599,4143],{"class":104},[91,4601,4602],{"class":108}," (x ",[91,4604,1468],{"class":104},[91,4606,4568],{"class":122},[91,4608,3697],{"class":108},[91,4610,4611],{"class":97},"\u002F* x: 'a' *\u002F",[91,4613,1124],{"class":108},[91,4615,4616,4618,4620,4623,4625],{"class":93,"line":2602},[91,4617,4367],{"class":104},[91,4619,1085],{"class":108},[91,4621,4622],{"class":97},"\u002F* x: 'b' | 'c' *\u002F",[91,4624,1899],{"class":108},[91,4626,4627],{"class":97},"\u002F\u002F 排除 'a' 后剩两支\n",[91,4629,4630],{"class":93,"line":2621},[91,4631,456],{"class":108},[15,4633,4634,4635,881,4637,4639,4640,4643,4644,74],{},"真值收窄有个陷阱：",[36,4636,1147],{},[36,4638,4498],{}," 也会被一起剔掉，如果你本意只是「判空」，用 ",[36,4641,4642],{},"x !== undefined"," 更精确，别误用 ",[36,4645,4483],{},[28,4647,4649,4650,4653],{"id":4648},"类型谓词-x-is-t把你的判断经验交给编译器","类型谓词 ",[36,4651,4652],{},"x is T","：把你的判断经验交给编译器",[15,4655,4656,4657,4659,4660,4662,4663,4666],{},"标准判断不够用时，你写一个返回 ",[36,4658,819],{}," 的函数，但用 ",[36,4661,4652],{}," 告诉编译器「返回真即说明 x 是 T」——这就是",[19,4664,4665],{},"类型谓词（type predicate）","，用户自定义收窄的核心工具：",[82,4668,4670],{"className":84,"code":4669,"language":86,"meta":87,"style":87},"function isString(x: unknown): x is string {\n  return typeof x === 'string'\n}\nfunction use(v: unknown) {\n  if (isString(v)) {\n    v.toUpperCase()   \u002F\u002F ✓ 因为 isString 声明了 x is string\n  }\n}\n\n\u002F\u002F 配合 filter 极其好用：过滤后的数组类型会自动窄化\nconst nums = ['a', 1, 'b', 2].filter(isString)   \u002F\u002F nums: string[]\n",[36,4671,4672,4700,4713,4717,4734,4746,4758,4762,4766,4770,4775],{"__ignoreMap":87},[91,4673,4674,4676,4679,4681,4683,4685,4687,4689,4691,4693,4696,4698],{"class":93,"line":94},[91,4675,399],{"class":104},[91,4677,4678],{"class":135}," isString",[91,4680,405],{"class":108},[91,4682,1852],{"class":408},[91,4684,112],{"class":104},[91,4686,1416],{"class":115},[91,4688,426],{"class":108},[91,4690,112],{"class":104},[91,4692,1274],{"class":408},[91,4694,4695],{"class":104}," is",[91,4697,470],{"class":115},[91,4699,1723],{"class":108},[91,4701,4702,4704,4706,4708,4710],{"class":93,"line":101},[91,4703,441],{"class":104},[91,4705,1232],{"class":104},[91,4707,540],{"class":108},[91,4709,1468],{"class":104},[91,4711,4712],{"class":122}," 'string'\n",[91,4714,4715],{"class":93,"line":129},[91,4716,456],{"class":108},[91,4718,4719,4721,4723,4725,4728,4730,4732],{"class":93,"line":453},[91,4720,399],{"class":104},[91,4722,1909],{"class":135},[91,4724,405],{"class":108},[91,4726,4727],{"class":408},"v",[91,4729,112],{"class":104},[91,4731,1416],{"class":115},[91,4733,1474],{"class":108},[91,4735,4736,4738,4740,4743],{"class":93,"line":459},[91,4737,4143],{"class":104},[91,4739,1220],{"class":108},[91,4741,4742],{"class":135},"isString",[91,4744,4745],{"class":108},"(v)) {\n",[91,4747,4748,4751,4753,4755],{"class":93,"line":611},[91,4749,4750],{"class":108},"    v.",[91,4752,142],{"class":135},[91,4754,4545],{"class":108},[91,4756,4757],{"class":97},"\u002F\u002F ✓ 因为 isString 声明了 x is string\n",[91,4759,4760],{"class":93,"line":1494},[91,4761,3112],{"class":108},[91,4763,4764],{"class":93,"line":2591},[91,4765,456],{"class":108},[91,4767,4768],{"class":93,"line":2602},[91,4769,567],{"emptyLinePlaceholder":566},[91,4771,4772],{"class":93,"line":2621},[91,4773,4774],{"class":97},"\u002F\u002F 配合 filter 极其好用：过滤后的数组类型会自动窄化\n",[91,4776,4777,4779,4782,4784,4786,4788,4790,4792,4794,4796,4798,4800,4803,4806,4809],{"class":93,"line":2629},[91,4778,462],{"class":104},[91,4780,4781],{"class":115}," nums",[91,4783,119],{"class":104},[91,4785,479],{"class":108},[91,4787,482],{"class":122},[91,4789,416],{"class":108},[91,4791,927],{"class":115},[91,4793,416],{"class":108},[91,4795,487],{"class":122},[91,4797,416],{"class":108},[91,4799,932],{"class":115},[91,4801,4802],{"class":108},"].",[91,4804,4805],{"class":135},"filter",[91,4807,4808],{"class":108},"(isString)   ",[91,4810,4811],{"class":97},"\u002F\u002F nums: string[]\n",[15,4813,4814,4817,4818,514,4821,4823,4824,4826],{},[36,4815,4816],{},"is"," 前面必须是",[19,4819,4820],{},"函数参数本身",[36,4822,4652],{},"），TS 只信任「对这个变量做的判断」。这个写法把「我确信它是这个类型」的经验，安全地注入编译器，而不必用 ",[36,4825,195],{}," 硬断言。",[28,4828,4830,4833],{"id":4829},"asserts-x-is-t断言函数",[36,4831,4832],{},"asserts x is T","：断言函数",[15,4835,4836,4837,4840],{},"类型谓词的「加强版」——它不返回布尔，而是",[19,4838,4839],{},"不满足就抛错","，从而在调用点之后把变量收窄。适合做参数校验：",[82,4842,4844],{"className":84,"code":4843,"language":86,"meta":87,"style":87},"function assertIsNumber(v: unknown): asserts v is number {\n  if (typeof v !== 'number') throw new Error('not a number')\n}\nfunction compute(v: unknown) {\n  assertIsNumber(v)\n  v.toFixed(2)   \u002F\u002F ✓ 调用之后，编译器认定 v 是 number\n}\n",[36,4845,4846,4876,4908,4912,4929,4937,4953],{"__ignoreMap":87},[91,4847,4848,4850,4853,4855,4857,4859,4861,4863,4865,4868,4870,4872,4874],{"class":93,"line":94},[91,4849,399],{"class":104},[91,4851,4852],{"class":135}," assertIsNumber",[91,4854,405],{"class":108},[91,4856,4727],{"class":408},[91,4858,112],{"class":104},[91,4860,1416],{"class":115},[91,4862,426],{"class":108},[91,4864,112],{"class":104},[91,4866,4867],{"class":104}," asserts",[91,4869,1747],{"class":408},[91,4871,4695],{"class":104},[91,4873,116],{"class":115},[91,4875,1723],{"class":108},[91,4877,4878,4880,4882,4884,4887,4889,4892,4894,4897,4899,4901,4903,4906],{"class":93,"line":101},[91,4879,4143],{"class":104},[91,4881,1220],{"class":108},[91,4883,1223],{"class":104},[91,4885,4886],{"class":108}," v ",[91,4888,2261],{"class":104},[91,4890,4891],{"class":122}," 'number'",[91,4893,2574],{"class":108},[91,4895,4896],{"class":104},"throw",[91,4898,1604],{"class":104},[91,4900,1607],{"class":135},[91,4902,405],{"class":108},[91,4904,4905],{"class":122},"'not a number'",[91,4907,3578],{"class":108},[91,4909,4910],{"class":93,"line":129},[91,4911,456],{"class":108},[91,4913,4914,4916,4919,4921,4923,4925,4927],{"class":93,"line":453},[91,4915,399],{"class":104},[91,4917,4918],{"class":135}," compute",[91,4920,405],{"class":108},[91,4922,4727],{"class":408},[91,4924,112],{"class":104},[91,4926,1416],{"class":115},[91,4928,1474],{"class":108},[91,4930,4931,4934],{"class":93,"line":459},[91,4932,4933],{"class":135},"  assertIsNumber",[91,4935,4936],{"class":108},"(v)\n",[91,4938,4939,4942,4944,4946,4948,4950],{"class":93,"line":611},[91,4940,4941],{"class":108},"  v.",[91,4943,2224],{"class":135},[91,4945,405],{"class":108},[91,4947,932],{"class":115},[91,4949,1309],{"class":108},[91,4951,4952],{"class":97},"\u002F\u002F ✓ 调用之后，编译器认定 v 是 number\n",[91,4954,4955],{"class":93,"line":1494},[91,4956,456],{"class":108},[15,4958,4959],{},"断言函数对调用方是「隐形收窄」：它没有返回值可用，副作用是改变了后续代码里变量的类型。校验外部输入时特别顺手。",[789,4961],{"height":4962,"src":4963,"title":4964},620,"\u002Ftutorials\u002Fts\u002Fdemos\u002F07-narrowing.html","示例 07：类型收窄——编译器怎么逐步相信你的判断",[28,4966,4967],{"id":4967},"收窄不是运行时保护",[15,4969,4970,4971,74,4974,4977,4978,729,4980,4982,4983,4986],{},"最后再敲一次黑板：收窄只影响",[19,4972,4973],{},"编译期看法",[36,4975,4976],{},"typeof x === 'string'"," 这个分支运行时确实会执行对应 JS，但那是 JS 的判断在起作用，不是类型系统。外部数据你没法靠类型「声明」它是 string——必须靠 ",[36,4979,1223],{},[36,4981,4260],{},"\u002F校验库在运行时",[19,4984,4985],{},"真的验一遍","（第十五、十七章）。收窄是「你在代码里做出判断，编译器据此更新它的看法」这个协作过程，两半缺一不可。",[10,4988,4990,4991,4993,4994,1685,4997],{"id":4989},"八断言as双重断言非空断言以及-satisfies-与-as-const","八、断言：",[36,4992,195],{},"、双重断言、非空断言，以及 ",[36,4995,4996],{},"satisfies",[36,4998,1173],{},[15,5000,5001,5002,5005],{},"断言是 TS 里最强大也最容易被滥用的一组工具。它的本质一句话：",[19,5003,5004],{},"你对编译器说「相信我，这个值其实是某个类型」，编译器照办，且不再检查——出了错你自己担","。本章把四种断言工具各自的分工讲清楚。",[28,5007,5009,5012],{"id":5008},"as-t普通断言",[36,5010,5011],{},"as T","：普通断言",[82,5014,5016],{"className":84,"code":5015,"language":86,"meta":87,"style":87},"const el = document.getElementById('app') as HTMLElement\nconst input = el.querySelector('#name') as HTMLInputElement\nconst data = response.body as User\n",[36,5017,5018,5045,5072],{"__ignoreMap":87},[91,5019,5020,5022,5025,5027,5030,5033,5035,5038,5040,5042],{"class":93,"line":94},[91,5021,462],{"class":104},[91,5023,5024],{"class":115}," el",[91,5026,119],{"class":104},[91,5028,5029],{"class":108}," document.",[91,5031,5032],{"class":135},"getElementById",[91,5034,405],{"class":108},[91,5036,5037],{"class":122},"'app'",[91,5039,2574],{"class":108},[91,5041,195],{"class":104},[91,5043,5044],{"class":135}," HTMLElement\n",[91,5046,5047,5049,5052,5054,5057,5060,5062,5065,5067,5069],{"class":93,"line":101},[91,5048,462],{"class":104},[91,5050,5051],{"class":115}," input",[91,5053,119],{"class":104},[91,5055,5056],{"class":108}," el.",[91,5058,5059],{"class":135},"querySelector",[91,5061,405],{"class":108},[91,5063,5064],{"class":122},"'#name'",[91,5066,2574],{"class":108},[91,5068,195],{"class":104},[91,5070,5071],{"class":135}," HTMLInputElement\n",[91,5073,5074,5076,5079,5081,5084,5086],{"class":93,"line":129},[91,5075,462],{"class":104},[91,5077,5078],{"class":115}," data",[91,5080,119],{"class":104},[91,5082,5083],{"class":108}," response.body ",[91,5085,195],{"class":104},[91,5087,5088],{"class":135}," User\n",[15,5090,5091,5093,5094,5096,5097,5100,5101,5103,5104,5106,5107,5110,5111,5114],{},[36,5092,195],{}," 用来在「编译器知道的类型」和「你知道的实际类型」之间搭桥，常见于 DOM 查询（",[36,5095,5059],{}," 默认返回宽泛的 ",[36,5098,5099],{},"Element","）、以及类型断言从 ",[36,5102,825],{}," 收窄。但 ",[36,5105,195],{}," ",[19,5108,5109],{},"不做运行时转换","，",[36,5112,5113],{},"x as number"," 不会把字符串转成数字，它只是改编译器看法。",[15,5116,5117,5118,5121,5122,5124,5125,74],{},"这里引用本课程反复强调的一句话，正好给断言下个定义：",[19,5119,5120],{},"断言是你写给编译器的情书——你告诉它该怎么想，它全盘接受，不做任何核实。"," 情书写得越满，运行时被打脸越疼。所以能用收窄（",[36,5123,4816],{}," 谓词）解决的地方，就别用 ",[36,5126,195],{},[28,5128,5130,5131],{"id":5129},"双重断言-as-unknown-as-t","双重断言 ",[36,5132,5133],{},"as unknown as T",[15,5135,5136,5137,5140,5141,5144,5145,5147],{},"直接从 ",[36,5138,5139],{},"A"," 断言到八竿子打不着的 ",[36,5142,5143],{},"B"," 会被拒（「两种类型重叠不足」），此时先绕到 ",[36,5146,825],{}," 再断言就通得了：",[82,5149,5151],{"className":84,"code":5150,"language":86,"meta":87,"style":87},"const el = document as unknown as HTMLElement   \u002F\u002F 直断报错，借 unknown 中转\nconst v = someValue as unknown as TargetType\n",[36,5152,5153,5177],{"__ignoreMap":87},[91,5154,5155,5157,5159,5161,5164,5166,5168,5171,5174],{"class":93,"line":94},[91,5156,462],{"class":104},[91,5158,5024],{"class":115},[91,5160,119],{"class":104},[91,5162,5163],{"class":108}," document ",[91,5165,195],{"class":104},[91,5167,1416],{"class":115},[91,5169,5170],{"class":104}," as",[91,5172,5173],{"class":135}," HTMLElement",[91,5175,5176],{"class":97},"   \u002F\u002F 直断报错，借 unknown 中转\n",[91,5178,5179,5181,5183,5185,5188,5190,5192,5194],{"class":93,"line":101},[91,5180,462],{"class":104},[91,5182,1747],{"class":115},[91,5184,119],{"class":104},[91,5186,5187],{"class":108}," someValue ",[91,5189,195],{"class":104},[91,5191,1416],{"class":115},[91,5193,5170],{"class":104},[91,5195,5196],{"class":135}," TargetType\n",[15,5198,5199],{},"双重断言是「我最清楚，编译器你别拦」的终极表达，几乎放弃了所有安全检查。它合理的时候很少——比如把 DOM 元素硬当某个自定义类型、老库类型写错时——但每次用都该停下来想想：是不是该用校验代替。",[789,5201],{"height":1805,"src":5202,"title":5203},"\u002Ftutorials\u002Fts\u002Fdemos\u002F08-assertions.html","示例 08：断言的四种工具与它们的分工",[28,5205,5207,5208],{"id":5206},"非空断言","非空断言 ",[36,5209,198],{},[15,5211,5212,5213,5215,5216,729,5218,5220,5221,5223,5224,211],{},"后缀 ",[36,5214,198],{}," 告诉编译器「这个值这里一定不是 ",[36,5217,295],{},[36,5219,1684],{},"」，把 ",[36,5222,2151],{}," 收成 ",[36,5225,5226],{},"T",[82,5228,5230],{"className":84,"code":5229,"language":86,"meta":87,"style":87},"function f(s?: string) {\n  const len = s!.length    \u002F\u002F 你保证 s 一定有值；真给了 undefined 就运行时崩\n}\nconst el = document.getElementById('root')!    \u002F\u002F 断言这个 id 一定存在\n",[36,5231,5232,5248,5267,5271],{"__ignoreMap":87},[91,5233,5234,5236,5238,5240,5242,5244,5246],{"class":93,"line":94},[91,5235,399],{"class":104},[91,5237,4518],{"class":135},[91,5239,405],{"class":108},[91,5241,3006],{"class":408},[91,5243,2180],{"class":104},[91,5245,470],{"class":115},[91,5247,1474],{"class":108},[91,5249,5250,5252,5254,5256,5258,5260,5262,5264],{"class":93,"line":101},[91,5251,2558],{"class":104},[91,5253,4119],{"class":115},[91,5255,119],{"class":104},[91,5257,1441],{"class":108},[91,5259,198],{"class":104},[91,5261,599],{"class":108},[91,5263,2715],{"class":115},[91,5265,5266],{"class":97},"    \u002F\u002F 你保证 s 一定有值；真给了 undefined 就运行时崩\n",[91,5268,5269],{"class":93,"line":129},[91,5270,456],{"class":108},[91,5272,5273,5275,5277,5279,5281,5283,5285,5288,5290,5292],{"class":93,"line":453},[91,5274,462],{"class":104},[91,5276,5024],{"class":115},[91,5278,119],{"class":104},[91,5280,5029],{"class":108},[91,5282,5032],{"class":135},[91,5284,405],{"class":108},[91,5286,5287],{"class":122},"'root'",[91,5289,426],{"class":108},[91,5291,198],{"class":104},[91,5293,5294],{"class":97},"    \u002F\u002F 断言这个 id 一定存在\n",[15,5296,5297,5299,5300,5303,5304,5307,5308,5311,5312,5314,5315,5318,5319,74],{},[36,5298,198],{}," 是「我检查过了，编译器别念叨」的速记。风险显而易见：如果你",[19,5301,5302],{},"没","真检查过，",[36,5305,5306],{},"s!.length"," 就是 ",[36,5309,5310],{},"Cannot read properties of undefined","。它是 ",[36,5313,1772],{}," 下的止痛药，不该变成主食——能用可选链 ",[36,5316,5317],{},"?.","、能用收窄分支解决的，都别用 ",[36,5320,198],{},[28,5322,5324,5326],{"id":5323},"as-const把字面量彻底钉死",[36,5325,1173],{},"：把字面量彻底钉死",[15,5328,5329,5331,5332,5335,5336,5338],{},[36,5330,1173],{}," 不是断言类型，而是",[19,5333,5334],{},"断言「别再宽化了」","：加在字面量后面，所有属性变 ",[36,5337,2284],{},"，所有值收成最具体的字面量类型：",[82,5340,5342],{"className":84,"code":5341,"language":86,"meta":87,"style":87},"const a = { dir: 'up' }         \u002F\u002F dir: string\nconst b = { dir: 'up' } as const \u002F\u002F dir: 'up'（且整个对象只读）\n\nconst dirs = ['up', 'down'] as const   \u002F\u002F readonly ['up', 'down']，可当联合 'up'|'down' 用\n",[36,5343,5344,5365,5388,5392],{"__ignoreMap":87},[91,5345,5346,5348,5351,5353,5356,5359,5362],{"class":93,"line":94},[91,5347,462],{"class":104},[91,5349,5350],{"class":115}," a",[91,5352,119],{"class":104},[91,5354,5355],{"class":108}," { dir: ",[91,5357,5358],{"class":122},"'up'",[91,5360,5361],{"class":108}," }         ",[91,5363,5364],{"class":97},"\u002F\u002F dir: string\n",[91,5366,5367,5369,5372,5374,5376,5378,5380,5382,5385],{"class":93,"line":101},[91,5368,462],{"class":104},[91,5370,5371],{"class":115}," b",[91,5373,119],{"class":104},[91,5375,5355],{"class":108},[91,5377,5358],{"class":122},[91,5379,1203],{"class":108},[91,5381,195],{"class":104},[91,5383,5384],{"class":135}," const",[91,5386,5387],{"class":97}," \u002F\u002F dir: 'up'（且整个对象只读）\n",[91,5389,5390],{"class":93,"line":129},[91,5391,567],{"emptyLinePlaceholder":566},[91,5393,5394,5396,5399,5401,5403,5405,5407,5410,5412,5414,5416],{"class":93,"line":453},[91,5395,462],{"class":104},[91,5397,5398],{"class":115}," dirs",[91,5400,119],{"class":104},[91,5402,479],{"class":108},[91,5404,5358],{"class":122},[91,5406,416],{"class":108},[91,5408,5409],{"class":122},"'down'",[91,5411,1004],{"class":108},[91,5413,195],{"class":104},[91,5415,5384],{"class":135},[91,5417,5418],{"class":97},"   \u002F\u002F readonly ['up', 'down']，可当联合 'up'|'down' 用\n",[15,5420,5421,5422,5424],{},"第九章会专门讲它解决「字面量宽化」的价值，这里先记住：",[36,5423,1173],{}," 帮你把「值」冻成「类型」。",[28,5426,5428,5430],{"id":5427},"satisfies校验形状但不丢窄化",[36,5429,4996],{},"：校验形状但不丢窄化",[15,5432,5433,5434,5436,5437,5439],{},"TS 4.9 新增的 ",[36,5435,4996],{}," 解决一个 ",[36,5438,195],{}," 解决不好的两难。你想让一个值符合某类型、又不想因此丢掉它精确的字面量推断：",[82,5441,5443],{"className":84,"code":5442,"language":86,"meta":87,"style":87},"type Route = string | (() => string)\nconst routes = {\n  home: '\u002F',\n  about: () => '\u002Fabout',\n} satisfies Record\u003Cstring, Route>   \u002F\u002F 校验：值必须是 Route\n\nroutes.home     \u002F\u002F 类型是 '\u002F'（字面量保住），而不是被 route 的注解宽化\nroutes.about()  \u002F\u002F 能调用，且知道存在\n",[36,5444,5445,5467,5478,5488,5503,5528,5532,5540],{"__ignoreMap":87},[91,5446,5447,5449,5452,5454,5456,5458,5461,5463,5465],{"class":93,"line":94},[91,5448,1213],{"class":104},[91,5450,5451],{"class":135}," Route",[91,5453,119],{"class":104},[91,5455,470],{"class":115},[91,5457,2072],{"class":104},[91,5459,5460],{"class":108}," (() ",[91,5462,2577],{"class":104},[91,5464,470],{"class":115},[91,5466,3578],{"class":108},[91,5468,5469,5471,5474,5476],{"class":93,"line":101},[91,5470,462],{"class":104},[91,5472,5473],{"class":115}," routes",[91,5475,119],{"class":104},[91,5477,1723],{"class":108},[91,5479,5480,5483,5486],{"class":93,"line":129},[91,5481,5482],{"class":108},"  home: ",[91,5484,5485],{"class":122},"'\u002F'",[91,5487,3958],{"class":108},[91,5489,5490,5493,5496,5498,5501],{"class":93,"line":453},[91,5491,5492],{"class":135},"  about",[91,5494,5495],{"class":108},": () ",[91,5497,2577],{"class":104},[91,5499,5500],{"class":122}," '\u002Fabout'",[91,5502,3958],{"class":108},[91,5504,5505,5508,5510,5513,5515,5517,5519,5522,5525],{"class":93,"line":459},[91,5506,5507],{"class":108},"} ",[91,5509,4996],{"class":104},[91,5511,5512],{"class":135}," Record",[91,5514,955],{"class":108},[91,5516,728],{"class":115},[91,5518,416],{"class":108},[91,5520,5521],{"class":135},"Route",[91,5523,5524],{"class":108},">   ",[91,5526,5527],{"class":97},"\u002F\u002F 校验：值必须是 Route\n",[91,5529,5530],{"class":93,"line":611},[91,5531,567],{"emptyLinePlaceholder":566},[91,5533,5534,5537],{"class":93,"line":1494},[91,5535,5536],{"class":108},"routes.home     ",[91,5538,5539],{"class":97},"\u002F\u002F 类型是 '\u002F'（字面量保住），而不是被 route 的注解宽化\n",[91,5541,5542,5545,5548,5551],{"class":93,"line":2591},[91,5543,5544],{"class":108},"routes.",[91,5546,5547],{"class":135},"about",[91,5549,5550],{"class":108},"()  ",[91,5552,5553],{"class":97},"\u002F\u002F 能调用，且知道存在\n",[15,5555,5556,5557,5560,5561,5564,5565,5568,5569,5571,5572,5575,5576,5578,5579,5581,5582,5584],{},"对比 ",[36,5558,5559],{},"const routes: Record\u003Cstring, Route> = {...}","——那样 ",[36,5562,5563],{},"routes.home"," 会变成宽泛的 ",[36,5566,5567],{},"string | (() => string)","，丢掉「它其实就是 '\u002F'」的信息。",[36,5570,4996],{}," 的分工就是：",[19,5573,5574],{},"做「类型检查」但保留「更精确的推断结果」","，还能在使用点发现多余\u002F缺失属性。三者记住一句话——",[36,5577,195],{}," 改编译器看法、",[36,5580,1173],{}," 钉死字面量、",[36,5583,4996],{}," 校验又不丢精度。",[10,5586,5588,5589,5591],{"id":5587},"九字面量类型与-as-const把值当类型用","九、字面量类型与 ",[36,5590,1173],{},"：把「值」当「类型」用",[15,5593,5594,5595,74,5598,5600,5601,5603],{},"这一章解释一件很多人后知后觉的事：在 TS 里，",[19,5596,5597],{},"一个具体的值本身就是一种类型",[36,5599,5358],{}," 不只是字符串的一个实例，它还是一个「只可能是 up 这个字符串」的类型。理解这点，第五、七章的字面量窄化、",[36,5602,3185],{}," 穷尽才真正说得通。",[28,5605,5606],{"id":5606},"字面量类型家族",[15,5608,5609,5610,186,5612,5614],{},"字符串、数字、布尔、",[36,5611,295],{},[36,5613,1684],{}," 都能各自成为「只包含这一个值」的字面量类型：",[82,5616,5618],{"className":84,"code":5617,"language":86,"meta":87,"style":87},"let a: 'hello' = 'hello'     \u002F\u002F 只能是 hello\nlet n: 42 = 42               \u002F\u002F 只能是 42\nlet t: true = true           \u002F\u002F 只能是 true（注意不是 boolean）\n",[36,5619,5620,5637,5654],{"__ignoreMap":87},[91,5621,5622,5624,5626,5628,5630,5632,5634],{"class":93,"line":94},[91,5623,105],{"class":104},[91,5625,5350],{"class":108},[91,5627,112],{"class":104},[91,5629,123],{"class":122},[91,5631,119],{"class":104},[91,5633,123],{"class":122},[91,5635,5636],{"class":97},"     \u002F\u002F 只能是 hello\n",[91,5638,5639,5641,5643,5645,5647,5649,5651],{"class":93,"line":101},[91,5640,105],{"class":104},[91,5642,109],{"class":108},[91,5644,112],{"class":104},[91,5646,778],{"class":115},[91,5648,119],{"class":104},[91,5650,778],{"class":115},[91,5652,5653],{"class":97},"               \u002F\u002F 只能是 42\n",[91,5655,5656,5658,5661,5663,5665,5667,5669],{"class":93,"line":129},[91,5657,105],{"class":104},[91,5659,5660],{"class":108}," t",[91,5662,112],{"class":104},[91,5664,674],{"class":115},[91,5666,119],{"class":104},[91,5668,674],{"class":115},[91,5670,5671],{"class":97},"           \u002F\u002F 只能是 true（注意不是 boolean）\n",[15,5673,5674,5675,5678],{},"它们的价值全在",[19,5676,5677],{},"联合","里：把一堆允许值并成一个联合类型，等于给参数列了白名单：",[82,5680,5682],{"className":84,"code":5681,"language":86,"meta":87,"style":87},"type Dir = 'up' | 'down' | 'left' | 'right'\nfunction move(d: Dir) { \u002F* 只认这四个 *\u002F }\nmove('up')     \u002F\u002F ✓\nmove('diagonal')   \u002F\u002F ✗ 不在白名单里，直接报错\n",[36,5683,5684,5710,5732,5746],{"__ignoreMap":87},[91,5685,5686,5688,5691,5693,5695,5697,5700,5702,5705,5707],{"class":93,"line":94},[91,5687,1213],{"class":104},[91,5689,5690],{"class":135}," Dir",[91,5692,119],{"class":104},[91,5694,749],{"class":122},[91,5696,2072],{"class":104},[91,5698,5699],{"class":122}," 'down'",[91,5701,2072],{"class":104},[91,5703,5704],{"class":122}," 'left'",[91,5706,2072],{"class":104},[91,5708,5709],{"class":122}," 'right'\n",[91,5711,5712,5714,5716,5718,5721,5723,5725,5727,5730],{"class":93,"line":101},[91,5713,399],{"class":104},[91,5715,4325],{"class":135},[91,5717,405],{"class":108},[91,5719,5720],{"class":408},"d",[91,5722,112],{"class":104},[91,5724,5690],{"class":135},[91,5726,3697],{"class":108},[91,5728,5729],{"class":97},"\u002F* 只认这四个 *\u002F",[91,5731,1124],{"class":108},[91,5733,5734,5737,5739,5741,5743],{"class":93,"line":129},[91,5735,5736],{"class":135},"move",[91,5738,405],{"class":108},[91,5740,5358],{"class":122},[91,5742,3887],{"class":108},[91,5744,5745],{"class":97},"\u002F\u002F ✓\n",[91,5747,5748,5750,5752,5755,5757],{"class":93,"line":453},[91,5749,5736],{"class":135},[91,5751,405],{"class":108},[91,5753,5754],{"class":122},"'diagonal'",[91,5756,1309],{"class":108},[91,5758,5759],{"class":97},"\u002F\u002F ✗ 不在白名单里，直接报错\n",[15,5761,5762,5763,5765,5766,5768,5769,5772],{},"这比用 ",[36,5764,728],{}," 宽泛接参、运行时再 ",[36,5767,1458],{}," 判断安全太多——白名单错误在",[19,5770,5771],{},"写的时候","就暴露了。",[28,5774,5775],{"id":5775},"字面量宽化问题",[15,5777,5778,5779,504,5782,5784,5785,1532,5787,5789,5790,5792,5793,5796,5797,5800],{},"TS 默认会把字面量「往宽里推」：",[36,5780,5781],{},"let x = 'up'",[36,5783,1852],{}," 被推断成 ",[36,5786,728],{},[36,5788,5358],{},"，因为 ",[36,5791,105],{}," 意味着以后可能改成别的字符串。这个「从具体值放宽到所属基本类型」就叫",[19,5794,5795],{},"宽化（widening）","。多数时候它合理，但当你",[19,5798,5799],{},"想要那个具体类型","时就成了障碍。",[15,5802,5803,5804,5807,5808,5810,5811,74],{},"三条把类型「钉在字面量」的手段：显式注解 ",[36,5805,5806],{},"const d: Dir = 'up'","；",[36,5809,462],{}," 声明（第二章讲过的窄化）；",[36,5812,1173],{},[789,5814],{"height":791,"src":5815,"title":5816},"\u002Ftutorials\u002Fts\u002Fdemos\u002F09-literal-const.html","示例 09：字面量类型与 as const——对抗宽化",[28,5818,5820,5822],{"id":5819},"as-const-的三种落点",[36,5821,1173],{}," 的三种落点",[82,5824,5826],{"className":84,"code":5825,"language":86,"meta":87,"style":87},"const s = 'hi' as const          \u002F\u002F 'hi'\nconst arr = [1, 2, 3] as const   \u002F\u002F readonly [1, 2, 3]（元组 + 只读）\nconst obj = { x: 1, y: 'a' } as const  \u002F\u002F { readonly x: 1; readonly y: 'a' }\n",[36,5827,5828,5846,5876],{"__ignoreMap":87},[91,5829,5830,5832,5834,5836,5839,5841,5843],{"class":93,"line":94},[91,5831,462],{"class":104},[91,5833,1441],{"class":115},[91,5835,119],{"class":104},[91,5837,5838],{"class":122}," 'hi'",[91,5840,5170],{"class":104},[91,5842,5384],{"class":135},[91,5844,5845],{"class":97},"          \u002F\u002F 'hi'\n",[91,5847,5848,5850,5853,5855,5857,5859,5861,5863,5865,5867,5869,5871,5873],{"class":93,"line":101},[91,5849,462],{"class":104},[91,5851,5852],{"class":115}," arr",[91,5854,119],{"class":104},[91,5856,479],{"class":108},[91,5858,927],{"class":115},[91,5860,416],{"class":108},[91,5862,932],{"class":115},[91,5864,416],{"class":108},[91,5866,937],{"class":115},[91,5868,1004],{"class":108},[91,5870,195],{"class":104},[91,5872,5384],{"class":135},[91,5874,5875],{"class":97},"   \u002F\u002F readonly [1, 2, 3]（元组 + 只读）\n",[91,5877,5878,5880,5883,5885,5887,5889,5891,5893,5895,5897,5899],{"class":93,"line":129},[91,5879,462],{"class":104},[91,5881,5882],{"class":115}," obj",[91,5884,119],{"class":104},[91,5886,1884],{"class":108},[91,5888,927],{"class":115},[91,5890,1889],{"class":108},[91,5892,482],{"class":122},[91,5894,1203],{"class":108},[91,5896,195],{"class":104},[91,5898,5384],{"class":135},[91,5900,5901],{"class":97},"  \u002F\u002F { readonly x: 1; readonly y: 'a' }\n",[15,5903,5904,5905,5907,5908,5910,5911,5913,5914,74],{},"对象用了 ",[36,5906,1173],{},"，所有属性都变 ",[36,5909,2284],{}," 且值收成字面量；数组变成只读元组。它常和 ",[36,5912,4996],{},"、可辨识联合、枚举替代方案搭配——第五章那个「联合字面量替代 enum」的写法就靠 ",[36,5915,1173],{},[28,5917,5919,5920,5922],{"id":5918},"把值当类型用typeof-类型查询","把值当类型用：",[36,5921,1223],{}," 类型查询",[15,5924,5925,5928,5929,5932],{},[36,5926,5927],{},"typeof x"," 出现在",[19,5930,5931],{},"类型位置","时，是「取 x 的类型」，配合索引访问能从一组常量里提炼出联合类型：",[82,5934,5936],{"className":84,"code":5935,"language":86,"meta":87,"style":87},"const CONFIG = { api: '\u002Fv1', retry: 3 } as const\ntype ConfigKey = keyof typeof CONFIG          \u002F\u002F 'api' | 'retry'\ntype RetryVal = (typeof CONFIG)['retry']      \u002F\u002F 3\n",[36,5937,5938,5962,5981],{"__ignoreMap":87},[91,5939,5940,5942,5945,5947,5949,5951,5954,5956,5958,5960],{"class":93,"line":94},[91,5941,462],{"class":104},[91,5943,5944],{"class":115}," CONFIG",[91,5946,119],{"class":104},[91,5948,2332],{"class":108},[91,5950,2335],{"class":122},[91,5952,5953],{"class":108},", retry: ",[91,5955,937],{"class":115},[91,5957,1203],{"class":108},[91,5959,195],{"class":104},[91,5961,1208],{"class":104},[91,5963,5964,5966,5969,5971,5974,5976,5978],{"class":93,"line":101},[91,5965,1213],{"class":104},[91,5967,5968],{"class":135}," ConfigKey",[91,5970,119],{"class":104},[91,5972,5973],{"class":104}," keyof",[91,5975,1232],{"class":104},[91,5977,5944],{"class":115},[91,5979,5980],{"class":97},"          \u002F\u002F 'api' | 'retry'\n",[91,5982,5983,5985,5988,5990,5992,5994,5996,5999,6002,6005],{"class":93,"line":129},[91,5984,1213],{"class":104},[91,5986,5987],{"class":135}," RetryVal",[91,5989,119],{"class":104},[91,5991,1220],{"class":108},[91,5993,1223],{"class":104},[91,5995,5944],{"class":115},[91,5997,5998],{"class":108},")[",[91,6000,6001],{"class":122},"'retry'",[91,6003,6004],{"class":108},"]      ",[91,6006,6007],{"class":97},"\u002F\u002F 3\n",[15,6009,6010],{},"这是「值」和「类型」两个世界之间的一扇后门：常量对象一处维护，它的键、它的值类型都能反查出来，不用手写两遍。",[28,6012,6014,6016],{"id":6013},"const-类型参数简提",[36,6015,462],{}," 类型参数（简提）",[15,6018,6019,6020,6023,6024,6026],{},"TS 5.0 引入 ",[36,6021,6022],{},"\u003Cconst T>","，让泛型函数在接收实参时自动对它做 ",[36,6025,1173],{}," 式窄化，免去调用方手打：",[82,6028,6030],{"className":84,"code":6029,"language":86,"meta":87,"style":87},"function keep\u003Cconst T>(x: T): T { return x }\nkeep({ a: 1 })   \u002F\u002F 推断为 { readonly a: 1 }，而不是 { a: number }\n",[36,6031,6032,6068],{"__ignoreMap":87},[91,6033,6034,6036,6039,6041,6043,6046,6049,6051,6053,6055,6057,6059,6061,6063,6065],{"class":93,"line":94},[91,6035,399],{"class":104},[91,6037,6038],{"class":135}," keep",[91,6040,955],{"class":108},[91,6042,462],{"class":104},[91,6044,6045],{"class":135}," T",[91,6047,6048],{"class":108},">(",[91,6050,1852],{"class":408},[91,6052,112],{"class":104},[91,6054,6045],{"class":135},[91,6056,426],{"class":108},[91,6058,112],{"class":104},[91,6060,6045],{"class":135},[91,6062,1085],{"class":108},[91,6064,802],{"class":104},[91,6066,6067],{"class":108}," x }\n",[91,6069,6070,6073,6076,6078,6081],{"class":93,"line":101},[91,6071,6072],{"class":135},"keep",[91,6074,6075],{"class":108},"({ a: ",[91,6077,927],{"class":115},[91,6079,6080],{"class":108}," })   ",[91,6082,6083],{"class":97},"\u002F\u002F 推断为 { readonly a: 1 }，而不是 { a: number }\n",[15,6085,6086],{},"日常用得不多，知道它存在、能理解库作者为什么有时用它即可，不深入。",[10,6088,6090],{"id":6089},"十类修饰符参数属性抽象与真正的私有","十、类：修饰符、参数属性、抽象与真正的私有",[15,6092,6093,6094,6096,6097,186,6099,6102,6103,6106,6107,6110],{},"JS 教程第十一章讲过 ",[36,6095,4397],{}," 的运行时行为（",[36,6098,1986],{},[36,6100,6101],{},"super","、方法、静态成员）。这一章只看 TS 在类上",[19,6104,6105],{},"加了哪些类型层面的东西","：可见性修饰符、参数属性、",[36,6108,6109],{},"implements","、抽象类，以及「类型的类」和「实例的类」这一组容易混的概念。",[28,6112,6114,814,6117,814,6120],{"id":6113},"public-private-protected",[36,6115,6116],{},"public",[36,6118,6119],{},"private",[36,6121,6122],{},"protected",[15,6124,6125,6126,6129],{},"TS 给成员加了三种可见性，只在",[19,6127,6128],{},"编译期","生效——擦除后 JS 里根本没有私有：",[82,6131,6133],{"className":84,"code":6132,"language":86,"meta":87,"style":87},"class Account {\n  public owner: string      \u002F\u002F public 可省略，默认就是\n  private balance: number   \u002F\u002F 只能在类内部访问\n  protected level: number   \u002F\u002F 类内部 + 子类可访问\n\n  constructor(owner: string) {\n    this.owner = owner\n    this.balance = 0\n    this.level = 1\n  }\n  deposit(n: number) { this.balance += n }\n  getBalance() { return this.balance }\n}\nnew Account('a').balance   \u002F\u002F ✗ 报错：private 不能外部访问\n",[36,6134,6135,6144,6159,6174,6189,6193,6209,6222,6234,6246,6250,6276,6292,6297],{"__ignoreMap":87},[91,6136,6137,6139,6142],{"class":93,"line":94},[91,6138,4397],{"class":104},[91,6140,6141],{"class":135}," Account",[91,6143,1723],{"class":108},[91,6145,6146,6149,6152,6154,6156],{"class":93,"line":101},[91,6147,6148],{"class":104},"  public",[91,6150,6151],{"class":408}," owner",[91,6153,112],{"class":104},[91,6155,470],{"class":115},[91,6157,6158],{"class":97},"      \u002F\u002F public 可省略，默认就是\n",[91,6160,6161,6164,6167,6169,6171],{"class":93,"line":129},[91,6162,6163],{"class":104},"  private",[91,6165,6166],{"class":408}," balance",[91,6168,112],{"class":104},[91,6170,116],{"class":115},[91,6172,6173],{"class":97},"   \u002F\u002F 只能在类内部访问\n",[91,6175,6176,6179,6182,6184,6186],{"class":93,"line":453},[91,6177,6178],{"class":104},"  protected",[91,6180,6181],{"class":408}," level",[91,6183,112],{"class":104},[91,6185,116],{"class":115},[91,6187,6188],{"class":97},"   \u002F\u002F 类内部 + 子类可访问\n",[91,6190,6191],{"class":93,"line":459},[91,6192,567],{"emptyLinePlaceholder":566},[91,6194,6195,6198,6200,6203,6205,6207],{"class":93,"line":611},[91,6196,6197],{"class":104},"  constructor",[91,6199,405],{"class":108},[91,6201,6202],{"class":408},"owner",[91,6204,112],{"class":104},[91,6206,470],{"class":115},[91,6208,1474],{"class":108},[91,6210,6211,6214,6217,6219],{"class":93,"line":1494},[91,6212,6213],{"class":115},"    this",[91,6215,6216],{"class":108},".owner ",[91,6218,476],{"class":104},[91,6220,6221],{"class":108}," owner\n",[91,6223,6224,6226,6229,6231],{"class":93,"line":2591},[91,6225,6213],{"class":115},[91,6227,6228],{"class":108},".balance ",[91,6230,476],{"class":104},[91,6232,6233],{"class":115}," 0\n",[91,6235,6236,6238,6241,6243],{"class":93,"line":2602},[91,6237,6213],{"class":115},[91,6239,6240],{"class":108},".level ",[91,6242,476],{"class":104},[91,6244,6245],{"class":115}," 1\n",[91,6247,6248],{"class":93,"line":2621},[91,6249,3112],{"class":108},[91,6251,6252,6255,6257,6260,6262,6264,6266,6268,6270,6273],{"class":93,"line":2629},[91,6253,6254],{"class":135},"  deposit",[91,6256,405],{"class":108},[91,6258,6259],{"class":408},"n",[91,6261,112],{"class":104},[91,6263,116],{"class":115},[91,6265,3697],{"class":108},[91,6267,3361],{"class":115},[91,6269,6228],{"class":108},[91,6271,6272],{"class":104},"+=",[91,6274,6275],{"class":108}," n }\n",[91,6277,6278,6281,6284,6286,6289],{"class":93,"line":3115},[91,6279,6280],{"class":135},"  getBalance",[91,6282,6283],{"class":108},"() { ",[91,6285,802],{"class":104},[91,6287,6288],{"class":115}," this",[91,6290,6291],{"class":108},".balance }\n",[91,6293,6295],{"class":93,"line":6294},13,[91,6296,456],{"class":108},[91,6298,6300,6303,6305,6307,6309,6312],{"class":93,"line":6299},14,[91,6301,6302],{"class":104},"new",[91,6304,6141],{"class":135},[91,6306,405],{"class":108},[91,6308,482],{"class":122},[91,6310,6311],{"class":108},").balance   ",[91,6313,6314],{"class":97},"\u002F\u002F ✗ 报错：private 不能外部访问\n",[15,6316,6317,6318,6320,6321,6324,6325,6328,6329,74],{},"要认清：",[36,6319,6119],{}," 挡的是",[19,6322,6323],{},"编译器","，不是铁了心的运行时攻击——",[36,6326,6327],{},"account['balance']"," 用字符串索引能绕开（TS 层面也会红，但值确实读得到）。它是「契约」不是「保险箱」。真要运行时私有见下面的 ",[36,6330,6331],{},"#",[28,6333,6335],{"id":6334},"参数属性构造函数的样板消除","参数属性：构造函数的样板消除",[15,6337,6338,6339,6341],{},"在 constructor 参数前面直接加修饰符和 ",[36,6340,2284],{},"，TS 会自动帮你声明字段并赋值：",[82,6343,6345],{"className":84,"code":6344,"language":86,"meta":87,"style":87},"class Point {\n  constructor(\n    public readonly x: number,\n    public readonly y: number\n  ) {}          \u002F\u002F 不用手写 this.x = x 了\n}\n",[36,6346,6347,6355,6362,6378,6390,6398],{"__ignoreMap":87},[91,6348,6349,6351,6353],{"class":93,"line":94},[91,6350,4397],{"class":104},[91,6352,1847],{"class":135},[91,6354,1723],{"class":108},[91,6356,6357,6359],{"class":93,"line":101},[91,6358,6197],{"class":104},[91,6360,6361],{"class":108},"(\n",[91,6363,6364,6367,6370,6372,6374,6376],{"class":93,"line":129},[91,6365,6366],{"class":104},"    public",[91,6368,6369],{"class":104}," readonly",[91,6371,1274],{"class":408},[91,6373,112],{"class":104},[91,6375,116],{"class":115},[91,6377,3958],{"class":108},[91,6379,6380,6382,6384,6386,6388],{"class":93,"line":453},[91,6381,6366],{"class":104},[91,6383,6369],{"class":104},[91,6385,554],{"class":408},[91,6387,112],{"class":104},[91,6389,2677],{"class":115},[91,6391,6392,6395],{"class":93,"line":459},[91,6393,6394],{"class":108},"  ) {}          ",[91,6396,6397],{"class":97},"\u002F\u002F 不用手写 this.x = x 了\n",[91,6399,6400],{"class":93,"line":611},[91,6401,456],{"class":108},[15,6403,6404,6405,6408],{},"这一条能把「声明字段 + 构造函数赋值」的重复代码砍掉大半，日常写数据类时高频使用。注意它是 TS 独有语法，擦除后仍会还原成 ",[36,6406,6407],{},"this.x = x"," 的普通赋值。",[28,6410,6412,6415],{"id":6411},"static-与抽象类",[36,6413,6414],{},"static"," 与抽象类",[15,6417,6418,6420,6421,6424,6425,6427],{},[36,6419,6414],{}," 是 JS 就有的（挂在类本身而非实例），TS 只是给静态成员也加上类型注解。",[36,6422,6423],{},"abstract"," 是纯 TS 的：抽象类不能直接 ",[36,6426,6302],{},"，它的抽象方法没有实现、必须由子类补全，是「强制子类实现某些方法」的手段：",[82,6429,6431],{"className":84,"code":6430,"language":86,"meta":87,"style":87},"abstract class Shape {\n  abstract area(): number         \u002F\u002F 只声明不实现\n  describe() { return `area=${this.area()}` }   \u002F\u002F 可以有普通实现\n}\nclass Square extends Shape {\n  constructor(private side: number) { super() }\n  area() { return this.side ** 2 }   \u002F\u002F 必须实现，否则报错\n}\nnew Shape()   \u002F\u002F ✗ 抽象类不能实例化\n",[36,6432,6433,6444,6460,6487,6491,6505,6527,6550,6554],{"__ignoreMap":87},[91,6434,6435,6437,6440,6442],{"class":93,"line":94},[91,6436,6423],{"class":104},[91,6438,6439],{"class":104}," class",[91,6441,2895],{"class":135},[91,6443,1723],{"class":108},[91,6445,6446,6449,6451,6453,6455,6457],{"class":93,"line":101},[91,6447,6448],{"class":104},"  abstract",[91,6450,3001],{"class":135},[91,6452,1624],{"class":108},[91,6454,112],{"class":104},[91,6456,116],{"class":115},[91,6458,6459],{"class":97},"         \u002F\u002F 只声明不实现\n",[91,6461,6462,6465,6467,6469,6472,6474,6476,6478,6480,6482,6484],{"class":93,"line":129},[91,6463,6464],{"class":135},"  describe",[91,6466,6283],{"class":108},[91,6468,802],{"class":104},[91,6470,6471],{"class":122}," `area=${",[91,6473,3361],{"class":115},[91,6475,599],{"class":122},[91,6477,3173],{"class":135},[91,6479,1624],{"class":122},[91,6481,3439],{"class":122},[91,6483,1899],{"class":108},[91,6485,6486],{"class":97},"\u002F\u002F 可以有普通实现\n",[91,6488,6489],{"class":93,"line":453},[91,6490,456],{"class":108},[91,6492,6493,6495,6498,6501,6503],{"class":93,"line":459},[91,6494,4397],{"class":104},[91,6496,6497],{"class":135}," Square",[91,6499,6500],{"class":104}," extends",[91,6502,2895],{"class":135},[91,6504,1723],{"class":108},[91,6506,6507,6509,6511,6513,6516,6518,6520,6522,6524],{"class":93,"line":611},[91,6508,6197],{"class":104},[91,6510,405],{"class":108},[91,6512,6119],{"class":104},[91,6514,6515],{"class":408}," side",[91,6517,112],{"class":104},[91,6519,116],{"class":115},[91,6521,3697],{"class":108},[91,6523,6101],{"class":115},[91,6525,6526],{"class":108},"() }\n",[91,6528,6529,6532,6534,6536,6538,6541,6543,6545,6547],{"class":93,"line":1494},[91,6530,6531],{"class":135},"  area",[91,6533,6283],{"class":108},[91,6535,802],{"class":104},[91,6537,6288],{"class":115},[91,6539,6540],{"class":108},".side ",[91,6542,3053],{"class":104},[91,6544,1111],{"class":115},[91,6546,1899],{"class":108},[91,6548,6549],{"class":97},"\u002F\u002F 必须实现，否则报错\n",[91,6551,6552],{"class":93,"line":2591},[91,6553,456],{"class":108},[91,6555,6556,6558,6560,6562],{"class":93,"line":2602},[91,6557,6302],{"class":104},[91,6559,2895],{"class":135},[91,6561,4545],{"class":108},[91,6563,6564],{"class":97},"\u002F\u002F ✗ 抽象类不能实例化\n",[28,6566,6568,6570],{"id":6567},"implements声明我满足某个契约",[36,6569,6109],{},"：声明「我满足某个契约」",[15,6572,6573,6575],{},[36,6574,6109],{}," 让类显式承诺它符合某个接口。它只检查「类有没有接口的成员」，不改变类型本身：",[82,6577,6579],{"className":84,"code":6578,"language":86,"meta":87,"style":87},"interface Drivable { steer(angle: number): void }\nclass Car implements Drivable {\n  steer(angle: number) { \u002F* ... *\u002F }   \u002F\u002F 漏了 steer 就编译报错\n}\n",[36,6580,6581,6610,6624,6646],{"__ignoreMap":87},[91,6582,6583,6585,6588,6590,6593,6595,6598,6600,6602,6604,6606,6608],{"class":93,"line":94},[91,6584,189],{"class":104},[91,6586,6587],{"class":135}," Drivable",[91,6589,1085],{"class":108},[91,6591,6592],{"class":135},"steer",[91,6594,405],{"class":108},[91,6596,6597],{"class":408},"angle",[91,6599,112],{"class":104},[91,6601,116],{"class":115},[91,6603,426],{"class":108},[91,6605,112],{"class":104},[91,6607,1720],{"class":115},[91,6609,1124],{"class":108},[91,6611,6612,6614,6617,6620,6622],{"class":93,"line":101},[91,6613,4397],{"class":104},[91,6615,6616],{"class":135}," Car",[91,6618,6619],{"class":104}," implements",[91,6621,6587],{"class":135},[91,6623,1723],{"class":108},[91,6625,6626,6629,6631,6633,6635,6637,6639,6641,6643],{"class":93,"line":129},[91,6627,6628],{"class":135},"  steer",[91,6630,405],{"class":108},[91,6632,6597],{"class":408},[91,6634,112],{"class":104},[91,6636,116],{"class":115},[91,6638,3697],{"class":108},[91,6640,3700],{"class":97},[91,6642,1899],{"class":108},[91,6644,6645],{"class":97},"\u002F\u002F 漏了 steer 就编译报错\n",[91,6647,6648],{"class":93,"line":453},[91,6649,456],{"class":108},[15,6651,6652,6653,6655],{},"一个常见误解：",[36,6654,6109],{}," 不是运行时校验，也不像 Java 那样参与多态判定——它只是编译期的一张检查清单。忘了实现某方法时报错，仅此而已。",[28,6657,6659,6660,6662],{"id":6658},"真正的私有-字段","真正的私有：",[36,6661,6331],{}," 字段",[15,6664,6665,6666,6669,6670,6673],{},"ES 标准的私有字段 ",[36,6667,6668],{},"#name"," 才是运行时真正私有的，外部",[19,6671,6672],{},"任何方式","都摸不到：",[82,6675,6677],{"className":84,"code":6676,"language":86,"meta":87,"style":87},"class Vault {\n  #secret = 'abc'              \u002F\u002F 运行时私有，编译后是真私有\n  private hint = 'x'           \u002F\u002F 仅编译期私有\n  reveal() { return this.#secret }\n}\nnew Vault().#secret            \u002F\u002F ✗ 语法直接报错\n(new Vault() as any).secret    \u002F\u002F 也拿不到，运行时确实没有这个公开属性\n",[36,6678,6679,6688,6701,6715,6729,6733,6745],{"__ignoreMap":87},[91,6680,6681,6683,6686],{"class":93,"line":94},[91,6682,4397],{"class":104},[91,6684,6685],{"class":135}," Vault",[91,6687,1723],{"class":108},[91,6689,6690,6693,6695,6698],{"class":93,"line":101},[91,6691,6692],{"class":408},"  #secret",[91,6694,119],{"class":104},[91,6696,6697],{"class":122}," 'abc'",[91,6699,6700],{"class":97},"              \u002F\u002F 运行时私有，编译后是真私有\n",[91,6702,6703,6705,6708,6710,6712],{"class":93,"line":129},[91,6704,6163],{"class":104},[91,6706,6707],{"class":408}," hint",[91,6709,119],{"class":104},[91,6711,2450],{"class":122},[91,6713,6714],{"class":97},"           \u002F\u002F 仅编译期私有\n",[91,6716,6717,6720,6722,6724,6726],{"class":93,"line":453},[91,6718,6719],{"class":135},"  reveal",[91,6721,6283],{"class":108},[91,6723,802],{"class":104},[91,6725,6288],{"class":115},[91,6727,6728],{"class":108},".#secret }\n",[91,6730,6731],{"class":93,"line":459},[91,6732,456],{"class":108},[91,6734,6735,6737,6739,6742],{"class":93,"line":611},[91,6736,6302],{"class":104},[91,6738,6685],{"class":135},[91,6740,6741],{"class":108},"().#secret            ",[91,6743,6744],{"class":97},"\u002F\u002F ✗ 语法直接报错\n",[91,6746,6747,6749,6751,6753,6756,6758,6760,6763],{"class":93,"line":1494},[91,6748,405],{"class":108},[91,6750,6302],{"class":104},[91,6752,6685],{"class":135},[91,6754,6755],{"class":108},"() ",[91,6757,195],{"class":104},[91,6759,1279],{"class":115},[91,6761,6762],{"class":108},").secret    ",[91,6764,6765],{"class":97},"\u002F\u002F 也拿不到，运行时确实没有这个公开属性\n",[15,6767,6768,1685,6770,6772,6773,6775,6776,6778,6779,6781,6782,74],{},[36,6769,6331],{},[36,6771,6119],{}," 别混用概念：",[36,6774,6119],{}," 是 TS 的类型层约束（会被擦除），",[36,6777,6331],{}," 是 JS 的运行层约束（会保留）。老打包目标不支持 ",[36,6780,6331],{}," 时 TS 会把它降级成 ",[36,6783,6784],{},"WeakMap",[28,6786,6788,6789,814,6792],{"id":6787},"访问器-get-set","访问器 ",[36,6790,6791],{},"get",[36,6793,6794],{},"set",[15,6796,6797],{},"访问器和 JS 一样，TS 只是给读写分别加类型：",[82,6799,6801],{"className":84,"code":6800,"language":86,"meta":87,"style":87},"class Temp {\n  #c = 0\n  get celsius() { return this.#c }\n  set celsius(v: number) { this.#c = v }\n}\n",[36,6802,6803,6812,6821,6838,6865],{"__ignoreMap":87},[91,6804,6805,6807,6810],{"class":93,"line":94},[91,6806,4397],{"class":104},[91,6808,6809],{"class":135}," Temp",[91,6811,1723],{"class":108},[91,6813,6814,6817,6819],{"class":93,"line":101},[91,6815,6816],{"class":408},"  #c",[91,6818,119],{"class":104},[91,6820,6233],{"class":115},[91,6822,6823,6826,6829,6831,6833,6835],{"class":93,"line":129},[91,6824,6825],{"class":104},"  get",[91,6827,6828],{"class":135}," celsius",[91,6830,6283],{"class":108},[91,6832,802],{"class":104},[91,6834,6288],{"class":115},[91,6836,6837],{"class":108},".#c }\n",[91,6839,6840,6843,6845,6847,6849,6851,6853,6855,6857,6860,6862],{"class":93,"line":453},[91,6841,6842],{"class":104},"  set",[91,6844,6828],{"class":135},[91,6846,405],{"class":108},[91,6848,4727],{"class":408},[91,6850,112],{"class":104},[91,6852,116],{"class":115},[91,6854,3697],{"class":108},[91,6856,3361],{"class":115},[91,6858,6859],{"class":108},".#c ",[91,6861,476],{"class":104},[91,6863,6864],{"class":108}," v }\n",[91,6866,6867],{"class":93,"line":459},[91,6868,456],{"class":108},[15,6870,6871,6872,6874,6875,6877],{},"只写 ",[36,6873,6791],{}," 不写 ",[36,6876,6794],{}," 会读得到、写不进（严格模式下赋值报错），等价于一个「计算得出的只读属性」。",[28,6879,6881],{"id":6880},"类的类型-vs-实例的类型","类的类型 vs 实例的类型",[15,6883,6884,6885,6887,6888,6891,6892,6894],{},"一个 ",[36,6886,4397],{}," 名字在 TS 里同时是",[19,6889,6890],{},"两个类型","：作为「实例的形状」和作为「构造器本身」。区分它们是理解 ",[36,6893,6302],{}," 和泛型约束的关键：",[82,6896,6898],{"className":84,"code":6897,"language":86,"meta":87,"style":87},"class User { name = '' }\nlet u: User = new User()          \u002F\u002F User = 实例类型\nlet C: typeof User = User         \u002F\u002F typeof User = 构造器类型（能 new 的那个）\nlet C2: new () => User = User     \u002F\u002F 也可以直接写构造签名\n",[36,6899,6900,6917,6938,6960],{"__ignoreMap":87},[91,6901,6902,6904,6906,6908,6910,6912,6915],{"class":93,"line":94},[91,6903,4397],{"class":104},[91,6905,2163],{"class":135},[91,6907,1085],{"class":108},[91,6909,2168],{"class":408},[91,6911,119],{"class":104},[91,6913,6914],{"class":122}," ''",[91,6916,1124],{"class":108},[91,6918,6919,6921,6923,6925,6927,6929,6931,6933,6935],{"class":93,"line":101},[91,6920,105],{"class":104},[91,6922,1411],{"class":108},[91,6924,112],{"class":104},[91,6926,2163],{"class":135},[91,6928,119],{"class":104},[91,6930,1604],{"class":104},[91,6932,2163],{"class":135},[91,6934,1431],{"class":108},[91,6936,6937],{"class":97},"\u002F\u002F User = 实例类型\n",[91,6939,6940,6942,6945,6947,6949,6952,6954,6957],{"class":93,"line":129},[91,6941,105],{"class":104},[91,6943,6944],{"class":115}," C",[91,6946,112],{"class":104},[91,6948,1232],{"class":104},[91,6950,6951],{"class":108}," User ",[91,6953,476],{"class":104},[91,6955,6956],{"class":108}," User         ",[91,6958,6959],{"class":97},"\u002F\u002F typeof User = 构造器类型（能 new 的那个）\n",[91,6961,6962,6964,6967,6969,6971,6973,6975,6977,6979,6982],{"class":93,"line":453},[91,6963,105],{"class":104},[91,6965,6966],{"class":115}," C2",[91,6968,112],{"class":104},[91,6970,1604],{"class":104},[91,6972,2613],{"class":108},[91,6974,2577],{"class":104},[91,6976,2163],{"class":135},[91,6978,119],{"class":104},[91,6980,6981],{"class":108}," User     ",[91,6983,6984],{"class":97},"\u002F\u002F 也可以直接写构造签名\n",[15,6986,6987,6989,6990,6993],{},[36,6988,230],{}," 指实例的形状，",[36,6991,6992],{},"typeof User"," 指「这个类本身」。写「接收一个类、内部 new 它」的泛型函数时（第十一章），这个区别会立刻用上。",[10,6995,6997,6998],{"id":6996},"十一泛型入门为什么需要泛型而不是-any","十一、泛型入门：为什么需要泛型而不是 ",[36,6999,513],{},[15,7001,7002,7003,7006,7007,7009],{},"泛型是 TS 的灵魂。这一章先建立直觉：",[19,7004,7005],{},"泛型是「类型的参数」","——函数接收值作为参数，泛型函数额外接收「类型」作为参数，让返回类型能跟着输入类型的形状变化。先看不用泛型、用 ",[36,7008,513],{}," 的失败，再看泛型怎么救场。",[28,7011,7013,7014,7016],{"id":7012},"同一个例子先写成-any再写成泛型","同一个例子：先写成 ",[36,7015,513],{},"，再写成泛型",[15,7018,7019,7020,211],{},"需求：写一个函数，返回数组的第一个元素。用 ",[36,7021,513],{},[82,7023,7025],{"className":84,"code":7024,"language":86,"meta":87,"style":87},"function firstAny(arr: any[]): any {\n  return arr[0]\n}\nconst x = firstAny([1, 2, 3])   \u002F\u002F x: any —— 丢了元素类型！\nx.toFixed(2)                     \u002F\u002F 不报错，但可能运行时崩\nfirstAny(['a']).toUpperCase()    \u002F\u002F 也不能安全地知道是 string\n",[36,7026,7027,7050,7061,7065,7094,7110],{"__ignoreMap":87},[91,7028,7029,7031,7034,7036,7038,7040,7042,7044,7046,7048],{"class":93,"line":94},[91,7030,399],{"class":104},[91,7032,7033],{"class":135}," firstAny",[91,7035,405],{"class":108},[91,7037,3680],{"class":408},[91,7039,112],{"class":104},[91,7041,1279],{"class":115},[91,7043,3537],{"class":108},[91,7045,112],{"class":104},[91,7047,1279],{"class":115},[91,7049,1723],{"class":108},[91,7051,7052,7054,7057,7059],{"class":93,"line":101},[91,7053,441],{"class":104},[91,7055,7056],{"class":108}," arr[",[91,7058,1147],{"class":115},[91,7060,940],{"class":108},[91,7062,7063],{"class":93,"line":129},[91,7064,456],{"class":108},[91,7066,7067,7069,7071,7073,7075,7078,7080,7082,7084,7086,7088,7091],{"class":93,"line":453},[91,7068,462],{"class":104},[91,7070,1274],{"class":115},[91,7072,119],{"class":104},[91,7074,7033],{"class":135},[91,7076,7077],{"class":108},"([",[91,7079,927],{"class":115},[91,7081,416],{"class":108},[91,7083,932],{"class":115},[91,7085,416],{"class":108},[91,7087,937],{"class":115},[91,7089,7090],{"class":108},"])   ",[91,7092,7093],{"class":97},"\u002F\u002F x: any —— 丢了元素类型！\n",[91,7095,7096,7098,7100,7102,7104,7107],{"class":93,"line":459},[91,7097,1299],{"class":108},[91,7099,2224],{"class":135},[91,7101,405],{"class":108},[91,7103,932],{"class":115},[91,7105,7106],{"class":108},")                     ",[91,7108,7109],{"class":97},"\u002F\u002F 不报错，但可能运行时崩\n",[91,7111,7112,7115,7117,7119,7122,7124,7126],{"class":93,"line":611},[91,7113,7114],{"class":135},"firstAny",[91,7116,7077],{"class":108},[91,7118,482],{"class":122},[91,7120,7121],{"class":108},"]).",[91,7123,142],{"class":135},[91,7125,4359],{"class":108},[91,7127,7128],{"class":97},"\u002F\u002F 也不能安全地知道是 string\n",[15,7130,7131,7133,7134,7137,7138,7144],{},[36,7132,513],{}," 版本致命缺陷：进去的时候你知道是 ",[36,7135,7136],{},"number[]","，出来的时候啥都不知道了——",[19,7139,7140,7141,7143],{},"信息在 ",[36,7142,513],{}," 处漏光了","。现在用泛型：",[82,7146,7148],{"className":84,"code":7147,"language":86,"meta":87,"style":87},"function first\u003CT>(arr: T[]): T {\n  return arr[0]\n}\nconst a = first([1, 2, 3])   \u002F\u002F T 被推断成 number，a: number\nconst b = first(['x', 'y'])  \u002F\u002F T 推断成 string，b: string\na.toFixed(2)                  \u002F\u002F ✓ 知道是 number\nb.toUpperCase()               \u002F\u002F ✓ 知道是 string\n",[36,7149,7150,7177,7187,7191,7218,7244,7261],{"__ignoreMap":87},[91,7151,7152,7154,7157,7159,7161,7163,7165,7167,7169,7171,7173,7175],{"class":93,"line":94},[91,7153,399],{"class":104},[91,7155,7156],{"class":135}," first",[91,7158,955],{"class":108},[91,7160,5226],{"class":135},[91,7162,6048],{"class":108},[91,7164,3680],{"class":408},[91,7166,112],{"class":104},[91,7168,6045],{"class":135},[91,7170,3537],{"class":108},[91,7172,112],{"class":104},[91,7174,6045],{"class":135},[91,7176,1723],{"class":108},[91,7178,7179,7181,7183,7185],{"class":93,"line":101},[91,7180,441],{"class":104},[91,7182,7056],{"class":108},[91,7184,1147],{"class":115},[91,7186,940],{"class":108},[91,7188,7189],{"class":93,"line":129},[91,7190,456],{"class":108},[91,7192,7193,7195,7197,7199,7201,7203,7205,7207,7209,7211,7213,7215],{"class":93,"line":453},[91,7194,462],{"class":104},[91,7196,5350],{"class":115},[91,7198,119],{"class":104},[91,7200,7156],{"class":135},[91,7202,7077],{"class":108},[91,7204,927],{"class":115},[91,7206,416],{"class":108},[91,7208,932],{"class":115},[91,7210,416],{"class":108},[91,7212,937],{"class":115},[91,7214,7090],{"class":108},[91,7216,7217],{"class":97},"\u002F\u002F T 被推断成 number，a: number\n",[91,7219,7220,7222,7224,7226,7228,7230,7233,7235,7238,7241],{"class":93,"line":459},[91,7221,462],{"class":104},[91,7223,5371],{"class":115},[91,7225,119],{"class":104},[91,7227,7156],{"class":135},[91,7229,7077],{"class":108},[91,7231,7232],{"class":122},"'x'",[91,7234,416],{"class":108},[91,7236,7237],{"class":122},"'y'",[91,7239,7240],{"class":108},"])  ",[91,7242,7243],{"class":97},"\u002F\u002F T 推断成 string，b: string\n",[91,7245,7246,7249,7251,7253,7255,7258],{"class":93,"line":611},[91,7247,7248],{"class":108},"a.",[91,7250,2224],{"class":135},[91,7252,405],{"class":108},[91,7254,932],{"class":115},[91,7256,7257],{"class":108},")                  ",[91,7259,7260],{"class":97},"\u002F\u002F ✓ 知道是 number\n",[91,7262,7263,7266,7268,7271],{"class":93,"line":1494},[91,7264,7265],{"class":108},"b.",[91,7267,142],{"class":135},[91,7269,7270],{"class":108},"()               ",[91,7272,7273],{"class":97},"\u002F\u002F ✓ 知道是 string\n",[15,7275,7276,7278,7279,7281,7282,7284,7285,7287,7288,74,7290,7293,7294,7296],{},[36,7277,192],{}," 是一个「类型变量」：调用时 TS 看实参 ",[36,7280,7136],{},"，把 ",[36,7283,5226],{}," 填成 ",[36,7286,732],{},"，于是返回值也就是 ",[36,7289,732],{},[19,7291,7292],{},"类型进了函数又活着出来了","——这就是泛型相对 ",[36,7295,513],{}," 的全部价值：既复用代码，又不牺牲信息。",[789,7298],{"height":1805,"src":7299,"title":7300},"\u002Ftutorials\u002Fts\u002Fdemos\u002F11-generics-intro.html","示例 11：泛型入门——同一个函数用 any 与用泛型的差距",[28,7302,7304,7305,7307],{"id":7303},"泛型约束-extends给类型变量划范围","泛型约束 ",[36,7306,1986],{},"：给类型变量划范围",[15,7309,7310,7312,7313,7315],{},[36,7311,192],{}," 天真的地方是「T 上什么成员都不能访问」（因为编译器不知道 T 长啥样）。想访问某成员，就得用 ",[36,7314,1986],{}," 约束它至少具备什么：",[82,7317,7319],{"className":84,"code":7318,"language":86,"meta":87,"style":87},"function longest\u003CT extends { length: number }>(a: T, b: T): T {\n  return a.length >= b.length ? a : b    \u002F\u002F ✓ 约束保证 a\u002Fb 一定有 length\n}\nlongest('abc', 'a')        \u002F\u002F string 有 length，通过\nlongest([1, 2], [])        \u002F\u002F 数组也有 length，通过\nlongest(1, 2)              \u002F\u002F ✗ number 没有 length，被约束挡住\n",[36,7320,7321,7367,7395,7399,7419,7437],{"__ignoreMap":87},[91,7322,7323,7325,7328,7330,7332,7334,7336,7338,7340,7342,7345,7347,7349,7351,7353,7355,7357,7359,7361,7363,7365],{"class":93,"line":94},[91,7324,399],{"class":104},[91,7326,7327],{"class":135}," longest",[91,7329,955],{"class":108},[91,7331,5226],{"class":135},[91,7333,6500],{"class":104},[91,7335,1085],{"class":108},[91,7337,2715],{"class":408},[91,7339,112],{"class":104},[91,7341,116],{"class":115},[91,7343,7344],{"class":108}," }>(",[91,7346,409],{"class":408},[91,7348,112],{"class":104},[91,7350,6045],{"class":135},[91,7352,416],{"class":108},[91,7354,419],{"class":408},[91,7356,112],{"class":104},[91,7358,6045],{"class":135},[91,7360,426],{"class":108},[91,7362,112],{"class":104},[91,7364,6045],{"class":135},[91,7366,1723],{"class":108},[91,7368,7369,7371,7373,7375,7378,7380,7382,7385,7387,7389,7392],{"class":93,"line":101},[91,7370,441],{"class":104},[91,7372,3734],{"class":108},[91,7374,2715],{"class":115},[91,7376,7377],{"class":104}," >=",[91,7379,3742],{"class":108},[91,7381,2715],{"class":115},[91,7383,7384],{"class":104}," ?",[91,7386,444],{"class":108},[91,7388,112],{"class":104},[91,7390,7391],{"class":108}," b    ",[91,7393,7394],{"class":97},"\u002F\u002F ✓ 约束保证 a\u002Fb 一定有 length\n",[91,7396,7397],{"class":93,"line":129},[91,7398,456],{"class":108},[91,7400,7401,7404,7406,7409,7411,7413,7416],{"class":93,"line":453},[91,7402,7403],{"class":135},"longest",[91,7405,405],{"class":108},[91,7407,7408],{"class":122},"'abc'",[91,7410,416],{"class":108},[91,7412,482],{"class":122},[91,7414,7415],{"class":108},")        ",[91,7417,7418],{"class":97},"\u002F\u002F string 有 length，通过\n",[91,7420,7421,7423,7425,7427,7429,7431,7434],{"class":93,"line":459},[91,7422,7403],{"class":135},[91,7424,7077],{"class":108},[91,7426,927],{"class":115},[91,7428,416],{"class":108},[91,7430,932],{"class":115},[91,7432,7433],{"class":108},"], [])        ",[91,7435,7436],{"class":97},"\u002F\u002F 数组也有 length，通过\n",[91,7438,7439,7441,7443,7445,7447,7449,7452],{"class":93,"line":611},[91,7440,7403],{"class":135},[91,7442,405],{"class":108},[91,7444,927],{"class":115},[91,7446,416],{"class":108},[91,7448,932],{"class":115},[91,7450,7451],{"class":108},")              ",[91,7453,7454],{"class":97},"\u002F\u002F ✗ number 没有 length，被约束挡住\n",[15,7456,7457,7460],{},[36,7458,7459],{},"T extends U"," 读作「T 至少是 U 的形状」。约束既让函数内部能安全访问成员，又在调用点挡住了不合规格的实参——这是泛型「有边界」的两面。",[28,7462,7463],{"id":7463},"默认类型参数",[15,7465,7466,7469],{},[36,7467,7468],{},"\u003CT = 某类型>"," 给类型变量一个默认，调用时不显式传就用默认：",[82,7471,7473],{"className":84,"code":7472,"language":86,"meta":87,"style":87},"function box\u003CT = string>(v: T): { value: T } { return { value: v } }\nbox('a')       \u002F\u002F T = string\nbox(42)        \u002F\u002F T = number（实参优先于默认）\nbox\u003Cstring>(42)   \u002F\u002F 显式指定 T = string，此时 42 反而报错\n",[36,7474,7475,7519,7534,7548],{"__ignoreMap":87},[91,7476,7477,7479,7482,7484,7486,7488,7490,7492,7494,7496,7498,7500,7502,7504,7507,7509,7511,7514,7516],{"class":93,"line":94},[91,7478,399],{"class":104},[91,7480,7481],{"class":135}," box",[91,7483,955],{"class":108},[91,7485,5226],{"class":135},[91,7487,119],{"class":104},[91,7489,470],{"class":115},[91,7491,6048],{"class":108},[91,7493,4727],{"class":408},[91,7495,112],{"class":104},[91,7497,6045],{"class":135},[91,7499,426],{"class":108},[91,7501,112],{"class":104},[91,7503,1085],{"class":108},[91,7505,7506],{"class":408},"value",[91,7508,112],{"class":104},[91,7510,6045],{"class":135},[91,7512,7513],{"class":108}," } { ",[91,7515,802],{"class":104},[91,7517,7518],{"class":108}," { value: v } }\n",[91,7520,7521,7524,7526,7528,7531],{"class":93,"line":101},[91,7522,7523],{"class":135},"box",[91,7525,405],{"class":108},[91,7527,482],{"class":122},[91,7529,7530],{"class":108},")       ",[91,7532,7533],{"class":97},"\u002F\u002F T = string\n",[91,7535,7536,7538,7540,7543,7545],{"class":93,"line":129},[91,7537,7523],{"class":135},[91,7539,405],{"class":108},[91,7541,7542],{"class":115},"42",[91,7544,7415],{"class":108},[91,7546,7547],{"class":97},"\u002F\u002F T = number（实参优先于默认）\n",[91,7549,7550,7552,7554,7556,7558,7560,7562],{"class":93,"line":453},[91,7551,7523],{"class":135},[91,7553,955],{"class":108},[91,7555,728],{"class":115},[91,7557,6048],{"class":108},[91,7559,7542],{"class":115},[91,7561,1309],{"class":108},[91,7563,7564],{"class":97},"\u002F\u002F 显式指定 T = string，此时 42 反而报错\n",[15,7566,7567],{},"默认类型参数常用于「给个合理缺省、但允许覆盖」的通用容器和工具函数。",[28,7569,7571],{"id":7570},"什么时候用泛型什么时候别","什么时候用泛型，什么时候别",[15,7573,7574,7575,7578,7579,186,7582,186,7585,186,7588,7591],{},"经验法则：",[19,7576,7577],{},"当「输入类型」和「输出类型」之间存在形状依赖时，用泛型","。像 ",[36,7580,7581],{},"first",[36,7583,7584],{},"map",[36,7586,7587],{},"identity",[36,7589,7590],{},"Record\u003CK,V>"," 这种「你给啥类型、我按同样形状返回」的，泛型最合适。如果函数的返回类型和入参类型毫无关系，或者你只想要「多种固定情况之一」，用联合类型（第五章）而不是泛型——别为了看起来高级而把简单联合写成复杂泛型，可读性会付代价。",[10,7593,7595,7596,7598],{"id":7594},"十二泛型进阶接口类keyof索引访问与映射类型入门","十二、泛型进阶：接口、类、",[36,7597,1229],{},"、索引访问与映射类型入门",[15,7600,7601,7602,7604,7605,7608],{},"泛型不只活在函数上。这一章把泛型铺到接口、类型别名、类上，并介绍三件「类型的运算」工具：",[36,7603,1229],{},"、索引访问 ",[36,7606,7607],{},"T[K]","、以及映射类型的雏形——它们是下一章工具类型的地基。",[28,7610,7611],{"id":7611},"泛型接口与泛型类型别名",[15,7613,7614],{},"把类型变量放进接口\u002F别名的尖括号，就得到「可参数化的形状」：",[82,7616,7618],{"className":84,"code":7617,"language":86,"meta":87,"style":87},"interface Box\u003CT> { value: T }\ninterface Pair\u003CA, B> { first: A; second: B }\n\nlet b: Box\u003Cnumber> = { value: 1 }\nlet p: Pair\u003Cstring, boolean> = { first: 'a', second: true }\n\ntype Result\u003CT> = { ok: true; data: T } | { ok: false; error: string }\nfunction unwrap\u003CT>(r: Result\u003CT>): T | undefined {\n  return r.ok ? r.data : undefined\n}\n",[36,7619,7620,7642,7677,7681,7706,7740,7744,7802,7838,7855],{"__ignoreMap":87},[91,7621,7622,7624,7627,7629,7631,7634,7636,7638,7640],{"class":93,"line":94},[91,7623,189],{"class":104},[91,7625,7626],{"class":135}," Box",[91,7628,955],{"class":108},[91,7630,5226],{"class":135},[91,7632,7633],{"class":108},"> { ",[91,7635,7506],{"class":408},[91,7637,112],{"class":104},[91,7639,6045],{"class":135},[91,7641,1124],{"class":108},[91,7643,7644,7646,7648,7650,7652,7654,7656,7658,7660,7662,7665,7667,7670,7672,7675],{"class":93,"line":101},[91,7645,189],{"class":104},[91,7647,2084],{"class":135},[91,7649,955],{"class":108},[91,7651,5139],{"class":135},[91,7653,416],{"class":108},[91,7655,5143],{"class":135},[91,7657,7633],{"class":108},[91,7659,7581],{"class":408},[91,7661,112],{"class":104},[91,7663,7664],{"class":135}," A",[91,7666,1859],{"class":108},[91,7668,7669],{"class":408},"second",[91,7671,112],{"class":104},[91,7673,7674],{"class":135}," B",[91,7676,1124],{"class":108},[91,7678,7679],{"class":93,"line":129},[91,7680,567],{"emptyLinePlaceholder":566},[91,7682,7683,7685,7687,7689,7691,7693,7695,7697,7699,7702,7704],{"class":93,"line":453},[91,7684,105],{"class":104},[91,7686,5371],{"class":108},[91,7688,112],{"class":104},[91,7690,7626],{"class":135},[91,7692,955],{"class":108},[91,7694,732],{"class":115},[91,7696,960],{"class":108},[91,7698,476],{"class":104},[91,7700,7701],{"class":108}," { value: ",[91,7703,927],{"class":115},[91,7705,1124],{"class":108},[91,7707,7708,7710,7712,7714,7716,7718,7720,7722,7724,7726,7728,7731,7733,7736,7738],{"class":93,"line":459},[91,7709,105],{"class":104},[91,7711,1875],{"class":108},[91,7713,112],{"class":104},[91,7715,2084],{"class":135},[91,7717,955],{"class":108},[91,7719,728],{"class":115},[91,7721,416],{"class":108},[91,7723,819],{"class":115},[91,7725,960],{"class":108},[91,7727,476],{"class":104},[91,7729,7730],{"class":108}," { first: ",[91,7732,482],{"class":122},[91,7734,7735],{"class":108},", second: ",[91,7737,1638],{"class":115},[91,7739,1124],{"class":108},[91,7741,7742],{"class":93,"line":611},[91,7743,567],{"emptyLinePlaceholder":566},[91,7745,7746,7748,7751,7753,7755,7757,7759,7761,7764,7766,7768,7770,7773,7775,7777,7779,7782,7784,7786,7788,7791,7793,7796,7798,7800],{"class":93,"line":1494},[91,7747,1213],{"class":104},[91,7749,7750],{"class":135}," Result",[91,7752,955],{"class":108},[91,7754,5226],{"class":135},[91,7756,960],{"class":108},[91,7758,476],{"class":104},[91,7760,1085],{"class":108},[91,7762,7763],{"class":408},"ok",[91,7765,112],{"class":104},[91,7767,674],{"class":115},[91,7769,1859],{"class":108},[91,7771,7772],{"class":408},"data",[91,7774,112],{"class":104},[91,7776,6045],{"class":135},[91,7778,1203],{"class":108},[91,7780,7781],{"class":104},"|",[91,7783,1085],{"class":108},[91,7785,7763],{"class":408},[91,7787,112],{"class":104},[91,7789,7790],{"class":115}," false",[91,7792,1859],{"class":108},[91,7794,7795],{"class":408},"error",[91,7797,112],{"class":104},[91,7799,470],{"class":115},[91,7801,1124],{"class":108},[91,7803,7804,7806,7809,7811,7813,7815,7817,7819,7821,7823,7825,7828,7830,7832,7834,7836],{"class":93,"line":2591},[91,7805,399],{"class":104},[91,7807,7808],{"class":135}," unwrap",[91,7810,955],{"class":108},[91,7812,5226],{"class":135},[91,7814,6048],{"class":108},[91,7816,2918],{"class":408},[91,7818,112],{"class":104},[91,7820,7750],{"class":135},[91,7822,955],{"class":108},[91,7824,5226],{"class":135},[91,7826,7827],{"class":108},">)",[91,7829,112],{"class":104},[91,7831,6045],{"class":135},[91,7833,2072],{"class":104},[91,7835,1756],{"class":115},[91,7837,1723],{"class":108},[91,7839,7840,7842,7845,7847,7850,7852],{"class":93,"line":2602},[91,7841,441],{"class":104},[91,7843,7844],{"class":108}," r.ok ",[91,7846,2142],{"class":104},[91,7848,7849],{"class":108}," r.data ",[91,7851,112],{"class":104},[91,7853,7854],{"class":115}," undefined\n",[91,7856,7857],{"class":93,"line":2621},[91,7858,456],{"class":108},[15,7860,7861,7862,186,7865,186,7868,7870,7871,7874],{},"泛型接口是 API 建模的主力：",[36,7863,7864],{},"Result\u003CT>",[36,7866,7867],{},"Promise\u003CT>",[36,7869,898],{},"、React 的 ",[36,7872,7873],{},"Props\u003CT>"," 全是它。",[28,7876,7877],{"id":7877},"泛型类",[15,7879,7880],{},"类同样能带类型参数，实例化时确定：",[82,7882,7884],{"className":84,"code":7883,"language":86,"meta":87,"style":87},"class Stack\u003CT> {\n  private items: T[] = []\n  push(v: T) { this.items.push(v) }\n  pop(): T | undefined { return this.items.pop() }\n}\nconst s = new Stack\u003Cnumber>()   \u002F\u002F 只能 push number\n",[36,7885,7886,7900,7918,7944,7972,7976],{"__ignoreMap":87},[91,7887,7888,7890,7893,7895,7897],{"class":93,"line":94},[91,7889,4397],{"class":104},[91,7891,7892],{"class":135}," Stack",[91,7894,955],{"class":108},[91,7896,5226],{"class":135},[91,7898,7899],{"class":108},"> {\n",[91,7901,7902,7904,7907,7909,7911,7913,7915],{"class":93,"line":101},[91,7903,6163],{"class":104},[91,7905,7906],{"class":408}," items",[91,7908,112],{"class":104},[91,7910,6045],{"class":135},[91,7912,473],{"class":108},[91,7914,476],{"class":104},[91,7916,7917],{"class":108}," []\n",[91,7919,7920,7923,7925,7927,7929,7931,7933,7935,7938,7941],{"class":93,"line":129},[91,7921,7922],{"class":135},"  push",[91,7924,405],{"class":108},[91,7926,4727],{"class":408},[91,7928,112],{"class":104},[91,7930,6045],{"class":135},[91,7932,3697],{"class":108},[91,7934,3361],{"class":115},[91,7936,7937],{"class":108},".items.",[91,7939,7940],{"class":135},"push",[91,7942,7943],{"class":108},"(v) }\n",[91,7945,7946,7949,7951,7953,7955,7957,7959,7961,7963,7965,7967,7970],{"class":93,"line":453},[91,7947,7948],{"class":135},"  pop",[91,7950,1624],{"class":108},[91,7952,112],{"class":104},[91,7954,6045],{"class":135},[91,7956,2072],{"class":104},[91,7958,1756],{"class":115},[91,7960,1085],{"class":108},[91,7962,802],{"class":104},[91,7964,6288],{"class":115},[91,7966,7937],{"class":108},[91,7968,7969],{"class":135},"pop",[91,7971,6526],{"class":108},[91,7973,7974],{"class":93,"line":459},[91,7975,456],{"class":108},[91,7977,7978,7980,7982,7984,7986,7988,7990,7992,7995],{"class":93,"line":611},[91,7979,462],{"class":104},[91,7981,1441],{"class":115},[91,7983,119],{"class":104},[91,7985,1604],{"class":104},[91,7987,7892],{"class":135},[91,7989,955],{"class":108},[91,7991,732],{"class":115},[91,7993,7994],{"class":108},">()   ",[91,7996,7997],{"class":97},"\u002F\u002F 只能 push number\n",[15,7999,8000,8001,8003,8004,8007,8008,8010,8011,74],{},"上一节 ",[36,8002,6992],{}," 那对概念在这里闭环：写一个「接收某个类的实例并返回同类」的泛型方法时，靠 ",[36,8005,8006],{},"new () => T"," 这样的构造签名约束，TS 才知道你能 ",[36,8009,6302],{}," 它、且 new 出来的就是 ",[36,8012,5226],{},[28,8014,8016,8018],{"id":8015},"keyof取一个类型所有键的联合",[36,8017,1229],{},"：取一个类型所有键的联合",[15,8020,8021,8024],{},[36,8022,8023],{},"keyof T"," 返回 T 全部属性名组成的字符串\u002F数字联合：",[82,8026,8028],{"className":84,"code":8027,"language":86,"meta":87,"style":87},"interface User { name: string; age: number }\ntype K = keyof User        \u002F\u002F 'name' | 'age'\n\nfunction get\u003CT, K extends keyof T>(obj: T, key: K): T[K] {\n  return obj[key]\n}\nget({ name: 'a', age: 1 }, 'name')   \u002F\u002F ✓ 返回类型是 string\nget({ name: 'a' }, 'nope')           \u002F\u002F ✗ 'nope' 不在 keyof 里\n",[36,8029,8030,8054,8070,8074,8127,8134,8138,8162],{"__ignoreMap":87},[91,8031,8032,8034,8036,8038,8040,8042,8044,8046,8048,8050,8052],{"class":93,"line":94},[91,8033,189],{"class":104},[91,8035,2163],{"class":135},[91,8037,1085],{"class":108},[91,8039,2168],{"class":408},[91,8041,112],{"class":104},[91,8043,470],{"class":115},[91,8045,1859],{"class":108},[91,8047,2177],{"class":408},[91,8049,112],{"class":104},[91,8051,116],{"class":115},[91,8053,1124],{"class":108},[91,8055,8056,8058,8061,8063,8065,8067],{"class":93,"line":101},[91,8057,1213],{"class":104},[91,8059,8060],{"class":135}," K",[91,8062,119],{"class":104},[91,8064,5973],{"class":104},[91,8066,2163],{"class":135},[91,8068,8069],{"class":97},"        \u002F\u002F 'name' | 'age'\n",[91,8071,8072],{"class":93,"line":129},[91,8073,567],{"emptyLinePlaceholder":566},[91,8075,8076,8078,8081,8083,8085,8087,8090,8092,8094,8096,8098,8101,8103,8105,8107,8109,8111,8113,8115,8117,8119,8122,8124],{"class":93,"line":453},[91,8077,399],{"class":104},[91,8079,8080],{"class":135}," get",[91,8082,955],{"class":108},[91,8084,5226],{"class":135},[91,8086,416],{"class":108},[91,8088,8089],{"class":135},"K",[91,8091,6500],{"class":104},[91,8093,5973],{"class":104},[91,8095,6045],{"class":135},[91,8097,6048],{"class":108},[91,8099,8100],{"class":408},"obj",[91,8102,112],{"class":104},[91,8104,6045],{"class":135},[91,8106,416],{"class":108},[91,8108,2390],{"class":408},[91,8110,112],{"class":104},[91,8112,8060],{"class":135},[91,8114,426],{"class":108},[91,8116,112],{"class":104},[91,8118,6045],{"class":135},[91,8120,8121],{"class":108},"[",[91,8123,8089],{"class":135},[91,8125,8126],{"class":108},"] {\n",[91,8128,8129,8131],{"class":93,"line":459},[91,8130,441],{"class":104},[91,8132,8133],{"class":108}," obj[key]\n",[91,8135,8136],{"class":93,"line":611},[91,8137,456],{"class":108},[91,8139,8140,8142,8145,8147,8149,8151,8154,8157,8159],{"class":93,"line":1494},[91,8141,6791],{"class":135},[91,8143,8144],{"class":108},"({ name: ",[91,8146,482],{"class":122},[91,8148,2847],{"class":108},[91,8150,927],{"class":115},[91,8152,8153],{"class":108}," }, ",[91,8155,8156],{"class":122},"'name'",[91,8158,1309],{"class":108},[91,8160,8161],{"class":97},"\u002F\u002F ✓ 返回类型是 string\n",[91,8163,8164,8166,8168,8170,8172,8175,8178],{"class":93,"line":2591},[91,8165,6791],{"class":135},[91,8167,8144],{"class":108},[91,8169,482],{"class":122},[91,8171,8153],{"class":108},[91,8173,8174],{"class":122},"'nope'",[91,8176,8177],{"class":108},")           ",[91,8179,8180],{"class":97},"\u002F\u002F ✗ 'nope' 不在 keyof 里\n",[15,8182,8183,8185,8186,8189,8190,8192],{},[36,8184,1229],{}," + ",[36,8187,8188],{},"extends keyof T"," 约束，让「按名字安全取属性」成为可能——",[36,8191,2390],{}," 只能是真实存在的键名，返回类型还能精确到那个键的值类型。",[28,8194,8196,8197],{"id":8195},"索引访问类型-tk","索引访问类型 ",[36,8198,7607],{},[15,8200,8201,8203,8204,8206],{},[36,8202,7607],{}," 取出「T 里键 K 对应的值类型」，和 ",[36,8205,1229],{}," 是天造地设的一对：",[82,8208,8210],{"className":84,"code":8209,"language":86,"meta":87,"style":87},"type UserAge = User['age']       \u002F\u002F number\ntype First = [string, number][0] \u002F\u002F string（元组按位置取）\n",[36,8211,8212,8234],{"__ignoreMap":87},[91,8213,8214,8216,8219,8221,8223,8225,8228,8231],{"class":93,"line":94},[91,8215,1213],{"class":104},[91,8217,8218],{"class":135}," UserAge",[91,8220,119],{"class":104},[91,8222,2163],{"class":135},[91,8224,8121],{"class":108},[91,8226,8227],{"class":122},"'age'",[91,8229,8230],{"class":108},"]       ",[91,8232,8233],{"class":97},"\u002F\u002F number\n",[91,8235,8236,8238,8241,8243,8245,8247,8249,8251,8254,8256,8258],{"class":93,"line":101},[91,8237,1213],{"class":104},[91,8239,8240],{"class":135}," First",[91,8242,119],{"class":104},[91,8244,479],{"class":108},[91,8246,728],{"class":115},[91,8248,416],{"class":108},[91,8250,732],{"class":115},[91,8252,8253],{"class":108},"][",[91,8255,1147],{"class":115},[91,8257,1004],{"class":108},[91,8259,8260],{"class":97},"\u002F\u002F string（元组按位置取）\n",[15,8262,8263,8264,8266,8267,8269,8270,729,8273,729,8276,8279],{},"它把「属性名」升级成「类型层面的可计算量」：配合泛型 ",[36,8265,7607],{}," 就是上面 ",[36,8268,6791],{}," 函数精确返回类型的秘密。工具类型 ",[36,8271,8272],{},"Pick",[36,8274,8275],{},"Record",[36,8277,8278],{},"ReturnType"," 全靠这套索引访问搭起来。",[28,8281,8283],{"id":8282},"映射类型入门用键的联合造新类型","映射类型入门：用键的联合造新类型",[15,8285,8286],{},"映射类型是「遍历一个键的联合，逐个生成属性」的语法，一句话就能看出威力：",[82,8288,8290],{"className":84,"code":8289,"language":86,"meta":87,"style":87},"type Mutable\u003CT> = {\n  -readonly [K in keyof T]: T[K]     \u002F\u002F 把每个属性去掉 readonly\n}\ntype Readonly2\u003CT> = {\n  readonly [K in keyof T]: T[K]      \u002F\u002F 给每个属性加 readonly\n}\n",[36,8291,8292,8309,8339,8343,8360,8390],{"__ignoreMap":87},[91,8293,8294,8296,8299,8301,8303,8305,8307],{"class":93,"line":94},[91,8295,1213],{"class":104},[91,8297,8298],{"class":135}," Mutable",[91,8300,955],{"class":108},[91,8302,5226],{"class":135},[91,8304,960],{"class":108},[91,8306,476],{"class":104},[91,8308,1723],{"class":108},[91,8310,8311,8314,8316,8318,8320,8322,8324,8326,8328,8330,8332,8334,8336],{"class":93,"line":101},[91,8312,8313],{"class":104},"  -readonly",[91,8315,479],{"class":108},[91,8317,8089],{"class":135},[91,8319,4351],{"class":104},[91,8321,5973],{"class":104},[91,8323,6045],{"class":135},[91,8325,2397],{"class":108},[91,8327,112],{"class":104},[91,8329,6045],{"class":135},[91,8331,8121],{"class":108},[91,8333,8089],{"class":135},[91,8335,2097],{"class":108},[91,8337,8338],{"class":97},"\u002F\u002F 把每个属性去掉 readonly\n",[91,8340,8341],{"class":93,"line":129},[91,8342,456],{"class":108},[91,8344,8345,8347,8350,8352,8354,8356,8358],{"class":93,"line":453},[91,8346,1213],{"class":104},[91,8348,8349],{"class":135}," Readonly2",[91,8351,955],{"class":108},[91,8353,5226],{"class":135},[91,8355,960],{"class":108},[91,8357,476],{"class":104},[91,8359,1723],{"class":108},[91,8361,8362,8365,8367,8369,8371,8373,8375,8377,8379,8381,8383,8385,8387],{"class":93,"line":459},[91,8363,8364],{"class":104},"  readonly",[91,8366,479],{"class":108},[91,8368,8089],{"class":135},[91,8370,4351],{"class":104},[91,8372,5973],{"class":104},[91,8374,6045],{"class":135},[91,8376,2397],{"class":108},[91,8378,112],{"class":104},[91,8380,6045],{"class":135},[91,8382,8121],{"class":108},[91,8384,8089],{"class":135},[91,8386,6004],{"class":108},[91,8388,8389],{"class":97},"\u002F\u002F 给每个属性加 readonly\n",[91,8391,8392],{"class":93,"line":611},[91,8393,456],{"class":108},[15,8395,8396,8399,8400,729,8402,8404,8405,729,8407,8410,8411,8414],{},[36,8397,8398],{},"[K in keyof T]"," 读作「对 T 的每个键 K」，后面是这些键在新类型里的形状。加上修饰符开关 ",[36,8401,2284],{},[36,8403,2142],{},"（还能用 ",[36,8406,447],{},[36,8408,8409],{},"-"," 增删），你能「把一个类型的所有属性整体变换一遍」。",[19,8412,8413],{},"下一章那些内置工具类型，本质全是这里这种映射类型手写的结果","——把这里看明白，第十三章就不是背 API 而是懂原理。",[10,8416,8418],{"id":8417},"十三工具类型逐个拆它们其实都是映射类型","十三、工具类型逐个拆：它们其实都是映射类型",[15,8420,8421,8422,8425],{},"TS 内置了一堆「拿来就用的类型变换」，统称工具类型。它们让日常业务不用每次手写映射。这一章逐个讲用途，并对每个",[19,8423,8424],{},"现场用映射类型手写一遍","，让你看清「它不过是前面那些零件的组装」。",[789,8427],{"height":8428,"src":8429,"title":8430},640,"\u002Ftutorials\u002Fts\u002Fdemos\u002F13-utility-types.html","示例 13：工具类型逐个拆——用映射类型手写一遍",[28,8432,8434,814,8437,814,8440],{"id":8433},"partialt-requiredt-readonlyt",[36,8435,8436],{},"Partial\u003CT>",[36,8438,8439],{},"Required\u003CT>",[36,8441,8442],{},"Readonly\u003CT>",[15,8444,8445,8446,8448,8449,8451,8452,8454],{},"给所有属性加 ",[36,8447,2142],{}," \u002F 去掉 ",[36,8450,2142],{}," \u002F 加 ",[36,8453,2284],{},"，用于「更新用的部分字段」「把可选补齐」等：",[82,8456,8458],{"className":84,"code":8457,"language":86,"meta":87,"style":87},"interface User { id: number; name: string }\ntype Upd = Partial\u003CUser>          \u002F\u002F { id?: number; name?: string }\nfunction updateUser(u: User, patch: Partial\u003CUser>): User { return { ...u, ...patch } }\n\ntype P = Partial\u003C{ a: number; b: string }>   \u002F\u002F { a?: number; b?: string }\n\n\u002F\u002F 手写一遍：\ntype MyPartial\u003CT> = { [K in keyof T]?: T[K] }          \u002F\u002F [K in keyof T]? 就是给每键加 ?\ntype MyRequired\u003CT> = { [K in keyof T]-?: T[K] }        \u002F\u002F -? 去掉可选\ntype MyReadonly\u003CT> = { readonly [K in keyof T]: T[K] } \u002F\u002F 加 readonly\n",[36,8459,8460,8484,8506,8557,8561,8595,8599,8604,8645,8687],{"__ignoreMap":87},[91,8461,8462,8464,8466,8468,8470,8472,8474,8476,8478,8480,8482],{"class":93,"line":94},[91,8463,189],{"class":104},[91,8465,2163],{"class":135},[91,8467,1085],{"class":108},[91,8469,2689],{"class":408},[91,8471,112],{"class":104},[91,8473,116],{"class":115},[91,8475,1859],{"class":108},[91,8477,2168],{"class":408},[91,8479,112],{"class":104},[91,8481,470],{"class":115},[91,8483,1124],{"class":108},[91,8485,8486,8488,8491,8493,8496,8498,8500,8503],{"class":93,"line":101},[91,8487,1213],{"class":104},[91,8489,8490],{"class":135}," Upd",[91,8492,119],{"class":104},[91,8494,8495],{"class":135}," Partial",[91,8497,955],{"class":108},[91,8499,230],{"class":135},[91,8501,8502],{"class":108},">          ",[91,8504,8505],{"class":97},"\u002F\u002F { id?: number; name?: string }\n",[91,8507,8508,8510,8513,8515,8518,8520,8522,8524,8527,8529,8531,8533,8535,8537,8539,8541,8543,8545,8547,8549,8552,8554],{"class":93,"line":129},[91,8509,399],{"class":104},[91,8511,8512],{"class":135}," updateUser",[91,8514,405],{"class":108},[91,8516,8517],{"class":408},"u",[91,8519,112],{"class":104},[91,8521,2163],{"class":135},[91,8523,416],{"class":108},[91,8525,8526],{"class":408},"patch",[91,8528,112],{"class":104},[91,8530,8495],{"class":135},[91,8532,955],{"class":108},[91,8534,230],{"class":135},[91,8536,7827],{"class":108},[91,8538,112],{"class":104},[91,8540,2163],{"class":135},[91,8542,1085],{"class":108},[91,8544,802],{"class":104},[91,8546,1085],{"class":108},[91,8548,3527],{"class":104},[91,8550,8551],{"class":108},"u, ",[91,8553,3527],{"class":104},[91,8555,8556],{"class":108},"patch } }\n",[91,8558,8559],{"class":93,"line":453},[91,8560,567],{"emptyLinePlaceholder":566},[91,8562,8563,8565,8568,8570,8572,8575,8577,8579,8581,8583,8585,8587,8589,8592],{"class":93,"line":459},[91,8564,1213],{"class":104},[91,8566,8567],{"class":135}," P",[91,8569,119],{"class":104},[91,8571,8495],{"class":135},[91,8573,8574],{"class":108},"\u003C{ ",[91,8576,409],{"class":408},[91,8578,112],{"class":104},[91,8580,116],{"class":115},[91,8582,1859],{"class":108},[91,8584,419],{"class":408},[91,8586,112],{"class":104},[91,8588,470],{"class":115},[91,8590,8591],{"class":108}," }>   ",[91,8593,8594],{"class":97},"\u002F\u002F { a?: number; b?: string }\n",[91,8596,8597],{"class":93,"line":611},[91,8598,567],{"emptyLinePlaceholder":566},[91,8600,8601],{"class":93,"line":1494},[91,8602,8603],{"class":97},"\u002F\u002F 手写一遍：\n",[91,8605,8606,8608,8611,8613,8615,8617,8619,8621,8623,8625,8627,8629,8631,8633,8635,8637,8639,8642],{"class":93,"line":2591},[91,8607,1213],{"class":104},[91,8609,8610],{"class":135}," MyPartial",[91,8612,955],{"class":108},[91,8614,5226],{"class":135},[91,8616,960],{"class":108},[91,8618,476],{"class":104},[91,8620,2387],{"class":108},[91,8622,8089],{"class":135},[91,8624,4351],{"class":104},[91,8626,5973],{"class":104},[91,8628,6045],{"class":135},[91,8630,2397],{"class":108},[91,8632,2180],{"class":104},[91,8634,6045],{"class":135},[91,8636,8121],{"class":108},[91,8638,8089],{"class":135},[91,8640,8641],{"class":108},"] }          ",[91,8643,8644],{"class":97},"\u002F\u002F [K in keyof T]? 就是给每键加 ?\n",[91,8646,8647,8649,8652,8654,8656,8658,8660,8662,8664,8666,8668,8670,8672,8675,8677,8679,8681,8684],{"class":93,"line":2602},[91,8648,1213],{"class":104},[91,8650,8651],{"class":135}," MyRequired",[91,8653,955],{"class":108},[91,8655,5226],{"class":135},[91,8657,960],{"class":108},[91,8659,476],{"class":104},[91,8661,2387],{"class":108},[91,8663,8089],{"class":135},[91,8665,4351],{"class":104},[91,8667,5973],{"class":104},[91,8669,6045],{"class":135},[91,8671,2397],{"class":108},[91,8673,8674],{"class":104},"-?:",[91,8676,6045],{"class":135},[91,8678,8121],{"class":108},[91,8680,8089],{"class":135},[91,8682,8683],{"class":108},"] }        ",[91,8685,8686],{"class":97},"\u002F\u002F -? 去掉可选\n",[91,8688,8689,8691,8694,8696,8698,8700,8702,8704,8706,8708,8710,8712,8714,8716,8718,8720,8722,8724,8726,8729],{"class":93,"line":2621},[91,8690,1213],{"class":104},[91,8692,8693],{"class":135}," MyReadonly",[91,8695,955],{"class":108},[91,8697,5226],{"class":135},[91,8699,960],{"class":108},[91,8701,476],{"class":104},[91,8703,1085],{"class":108},[91,8705,2284],{"class":104},[91,8707,479],{"class":108},[91,8709,8089],{"class":135},[91,8711,4351],{"class":104},[91,8713,5973],{"class":104},[91,8715,6045],{"class":135},[91,8717,2397],{"class":108},[91,8719,112],{"class":104},[91,8721,6045],{"class":135},[91,8723,8121],{"class":108},[91,8725,8089],{"class":135},[91,8727,8728],{"class":108},"] } ",[91,8730,8731],{"class":97},"\u002F\u002F 加 readonly\n",[15,8733,8734,186,8737,8740],{},[36,8735,8736],{},"-?",[36,8738,8739],{},"-readonly"," 里的减号是「移除该修饰符」，与不写符号（默认加）相对。",[28,8742,8744,814,8747],{"id":8743},"pickt-k-omitt-k",[36,8745,8746],{},"Pick\u003CT, K>",[36,8748,8749],{},"Omit\u003CT, K>",[15,8751,8752,8754,8755,8758],{},[36,8753,8272],{}," 从 T 里挑几个键，",[36,8756,8757],{},"Omit"," 从 T 里去掉几个键：",[82,8760,8762],{"className":84,"code":8761,"language":86,"meta":87,"style":87},"interface Full { id: number; name: string; pwd: string }\ntype Public = Pick\u003CFull, 'id' | 'name'>     \u002F\u002F { id: number; name: string }\ntype NoId = Omit\u003CFull, 'id'>                \u002F\u002F { name: string; pwd: string }\n\ntype MyPick\u003CT, K extends keyof T> = { [P in K]: T[P] }\n\u002F\u002F Omit 依赖第十四章的条件类型 Exclude，这里先给成品：\ntype MyOmit\u003CT, K extends keyof any> = Pick\u003CT, Exclude\u003Ckeyof T, K>>\n",[36,8763,8764,8798,8830,8856,8860,8907,8912],{"__ignoreMap":87},[91,8765,8766,8768,8771,8773,8775,8777,8779,8781,8783,8785,8787,8789,8792,8794,8796],{"class":93,"line":94},[91,8767,189],{"class":104},[91,8769,8770],{"class":135}," Full",[91,8772,1085],{"class":108},[91,8774,2689],{"class":408},[91,8776,112],{"class":104},[91,8778,116],{"class":115},[91,8780,1859],{"class":108},[91,8782,2168],{"class":408},[91,8784,112],{"class":104},[91,8786,470],{"class":115},[91,8788,1859],{"class":108},[91,8790,8791],{"class":408},"pwd",[91,8793,112],{"class":104},[91,8795,470],{"class":115},[91,8797,1124],{"class":108},[91,8799,8800,8802,8805,8807,8810,8812,8815,8817,8819,8821,8824,8827],{"class":93,"line":101},[91,8801,1213],{"class":104},[91,8803,8804],{"class":135}," Public",[91,8806,119],{"class":104},[91,8808,8809],{"class":135}," Pick",[91,8811,955],{"class":108},[91,8813,8814],{"class":135},"Full",[91,8816,416],{"class":108},[91,8818,1011],{"class":122},[91,8820,2072],{"class":104},[91,8822,8823],{"class":122}," 'name'",[91,8825,8826],{"class":108},">     ",[91,8828,8829],{"class":97},"\u002F\u002F { id: number; name: string }\n",[91,8831,8832,8834,8837,8839,8842,8844,8846,8848,8850,8853],{"class":93,"line":129},[91,8833,1213],{"class":104},[91,8835,8836],{"class":135}," NoId",[91,8838,119],{"class":104},[91,8840,8841],{"class":135}," Omit",[91,8843,955],{"class":108},[91,8845,8814],{"class":135},[91,8847,416],{"class":108},[91,8849,1011],{"class":122},[91,8851,8852],{"class":108},">                ",[91,8854,8855],{"class":97},"\u002F\u002F { name: string; pwd: string }\n",[91,8857,8858],{"class":93,"line":453},[91,8859,567],{"emptyLinePlaceholder":566},[91,8861,8862,8864,8867,8869,8871,8873,8875,8877,8879,8881,8883,8885,8887,8890,8892,8894,8896,8898,8900,8902,8904],{"class":93,"line":459},[91,8863,1213],{"class":104},[91,8865,8866],{"class":135}," MyPick",[91,8868,955],{"class":108},[91,8870,5226],{"class":135},[91,8872,416],{"class":108},[91,8874,8089],{"class":135},[91,8876,6500],{"class":104},[91,8878,5973],{"class":104},[91,8880,6045],{"class":135},[91,8882,960],{"class":108},[91,8884,476],{"class":104},[91,8886,2387],{"class":108},[91,8888,8889],{"class":135},"P",[91,8891,4351],{"class":104},[91,8893,8060],{"class":135},[91,8895,2397],{"class":108},[91,8897,112],{"class":104},[91,8899,6045],{"class":135},[91,8901,8121],{"class":108},[91,8903,8889],{"class":135},[91,8905,8906],{"class":108},"] }\n",[91,8908,8909],{"class":93,"line":611},[91,8910,8911],{"class":97},"\u002F\u002F Omit 依赖第十四章的条件类型 Exclude，这里先给成品：\n",[91,8913,8914,8916,8919,8921,8923,8925,8927,8929,8931,8933,8935,8937,8939,8941,8943,8945,8948,8950,8952,8954,8956,8958],{"class":93,"line":1494},[91,8915,1213],{"class":104},[91,8917,8918],{"class":135}," MyOmit",[91,8920,955],{"class":108},[91,8922,5226],{"class":135},[91,8924,416],{"class":108},[91,8926,8089],{"class":135},[91,8928,6500],{"class":104},[91,8930,5973],{"class":104},[91,8932,1279],{"class":115},[91,8934,960],{"class":108},[91,8936,476],{"class":104},[91,8938,8809],{"class":135},[91,8940,955],{"class":108},[91,8942,5226],{"class":135},[91,8944,416],{"class":108},[91,8946,8947],{"class":135},"Exclude",[91,8949,955],{"class":108},[91,8951,1229],{"class":104},[91,8953,6045],{"class":135},[91,8955,416],{"class":108},[91,8957,8089],{"class":135},[91,8959,8960],{"class":108},">>\n",[15,8962,8963,8965,8966,8968,8969,8972,8973,8975],{},[36,8964,8272],{}," 的映射源是 ",[36,8967,8089],{},"（要保留的键），每键的值取自 ",[36,8970,8971],{},"T[P]","——一句话讲完它的原理。",[36,8974,8757],{}," 是「先算出去掉 K 后剩下的键，再 Pick」。",[28,8977,8979,814,8982,814,8985],{"id":8978},"excludet-u-extractt-u-nonnullablet",[36,8980,8981],{},"Exclude\u003CT, U>",[36,8983,8984],{},"Extract\u003CT, U>",[36,8986,8987],{},"NonNullable\u003CT>",[15,8989,8990,8991,8994],{},"作用在",[19,8992,8993],{},"联合类型","上的三个筛选器：",[82,8996,8998],{"className":84,"code":8997,"language":86,"meta":87,"style":87},"type A = Exclude\u003C'a' | 'b' | 'c', 'a'>      \u002F\u002F 'b' | 'c'（从 T 里剔除 assignable to U 的）\ntype B = Extract\u003C'a' | 'b' | number, string> \u002F\u002F 'a' | 'b'（只留 assignable to string 的）\ntype C = NonNullable\u003Cstring | null | undefined> \u002F\u002F string（去掉 null\u002Fundefined）\n\ntype MyExclude\u003CT, U> = T extends U ? never : T    \u002F\u002F 条件类型：命中就丢\ntype MyExtract\u003CT, U> = T extends U ? T : never\ntype MyNonNullable\u003CT> = T extends null | undefined ? never : T\n",[36,8999,9000,9034,9066,9095,9099,9137,9171],{"__ignoreMap":87},[91,9001,9002,9004,9006,9008,9011,9013,9015,9017,9019,9021,9024,9026,9028,9031],{"class":93,"line":94},[91,9003,1213],{"class":104},[91,9005,7664],{"class":135},[91,9007,119],{"class":104},[91,9009,9010],{"class":135}," Exclude",[91,9012,955],{"class":108},[91,9014,482],{"class":122},[91,9016,2072],{"class":104},[91,9018,4573],{"class":122},[91,9020,2072],{"class":104},[91,9022,9023],{"class":122}," 'c'",[91,9025,416],{"class":108},[91,9027,482],{"class":122},[91,9029,9030],{"class":108},">      ",[91,9032,9033],{"class":97},"\u002F\u002F 'b' | 'c'（从 T 里剔除 assignable to U 的）\n",[91,9035,9036,9038,9040,9042,9045,9047,9049,9051,9053,9055,9057,9059,9061,9063],{"class":93,"line":101},[91,9037,1213],{"class":104},[91,9039,7674],{"class":135},[91,9041,119],{"class":104},[91,9043,9044],{"class":135}," Extract",[91,9046,955],{"class":108},[91,9048,482],{"class":122},[91,9050,2072],{"class":104},[91,9052,4573],{"class":122},[91,9054,2072],{"class":104},[91,9056,116],{"class":115},[91,9058,416],{"class":108},[91,9060,728],{"class":115},[91,9062,960],{"class":108},[91,9064,9065],{"class":97},"\u002F\u002F 'a' | 'b'（只留 assignable to string 的）\n",[91,9067,9068,9070,9072,9074,9077,9079,9081,9083,9086,9088,9090,9092],{"class":93,"line":129},[91,9069,1213],{"class":104},[91,9071,6944],{"class":135},[91,9073,119],{"class":104},[91,9075,9076],{"class":135}," NonNullable",[91,9078,955],{"class":108},[91,9080,728],{"class":115},[91,9082,2072],{"class":104},[91,9084,9085],{"class":115}," null",[91,9087,2072],{"class":104},[91,9089,1756],{"class":115},[91,9091,960],{"class":108},[91,9093,9094],{"class":97},"\u002F\u002F string（去掉 null\u002Fundefined）\n",[91,9096,9097],{"class":93,"line":453},[91,9098,567],{"emptyLinePlaceholder":566},[91,9100,9101,9103,9106,9108,9110,9112,9115,9117,9119,9121,9123,9126,9128,9130,9132,9134],{"class":93,"line":459},[91,9102,1213],{"class":104},[91,9104,9105],{"class":135}," MyExclude",[91,9107,955],{"class":108},[91,9109,5226],{"class":135},[91,9111,416],{"class":108},[91,9113,9114],{"class":135},"U",[91,9116,960],{"class":108},[91,9118,476],{"class":104},[91,9120,6045],{"class":135},[91,9122,6500],{"class":104},[91,9124,9125],{"class":135}," U",[91,9127,7384],{"class":104},[91,9129,1590],{"class":115},[91,9131,3442],{"class":104},[91,9133,6045],{"class":135},[91,9135,9136],{"class":97},"    \u002F\u002F 条件类型：命中就丢\n",[91,9138,9139,9141,9144,9146,9148,9150,9152,9154,9156,9158,9160,9162,9164,9166,9168],{"class":93,"line":611},[91,9140,1213],{"class":104},[91,9142,9143],{"class":135}," MyExtract",[91,9145,955],{"class":108},[91,9147,5226],{"class":135},[91,9149,416],{"class":108},[91,9151,9114],{"class":135},[91,9153,960],{"class":108},[91,9155,476],{"class":104},[91,9157,6045],{"class":135},[91,9159,6500],{"class":104},[91,9161,9125],{"class":135},[91,9163,7384],{"class":104},[91,9165,6045],{"class":135},[91,9167,3442],{"class":104},[91,9169,9170],{"class":115}," never\n",[91,9172,9173,9175,9178,9180,9182,9184,9186,9188,9190,9192,9194,9196,9198,9200,9202],{"class":93,"line":1494},[91,9174,1213],{"class":104},[91,9176,9177],{"class":135}," MyNonNullable",[91,9179,955],{"class":108},[91,9181,5226],{"class":135},[91,9183,960],{"class":108},[91,9185,476],{"class":104},[91,9187,6045],{"class":135},[91,9189,6500],{"class":104},[91,9191,9085],{"class":115},[91,9193,2072],{"class":104},[91,9195,1756],{"class":115},[91,9197,7384],{"class":104},[91,9199,1590],{"class":115},[91,9201,3442],{"class":104},[91,9203,9204],{"class":135}," T\n",[15,9206,9207,9209,9210,9213,9214,9216],{},[36,9208,1986],{}," 用在裸类型参数上会",[19,9211,9212],{},"逐个分发","到联合的每个成员（第十四章细讲分发条件类型）——所以 ",[36,9215,8947],{}," 能对联合每一支分别判断。这三个是组合其它工具类型的常用零件。",[28,9218,9220,814,9222,814,9225],{"id":9219},"returntype-parameters-constructorparameters",[36,9221,8278],{},[36,9223,9224],{},"Parameters",[36,9226,9227],{},"ConstructorParameters",[15,9229,9230],{},"从一个函数类型里「抠」出返回类型 \u002F 参数元组：",[82,9232,9234],{"className":84,"code":9233,"language":86,"meta":87,"style":87},"function make(a: number, b: string): { id: number } { return { id: a } }\ntype R = ReturnType\u003Ctypeof make>       \u002F\u002F { id: number }\ntype P = Parameters\u003Ctypeof make>       \u002F\u002F [number, string]\n\ntype MyReturnType\u003CT extends (...a: any) => any> = T extends (...a: any) => infer R ? R : never\ntype MyParameters\u003CT extends (...a: any) => any> = T extends (...a: infer P) => any ? P : never\n",[36,9235,9236,9278,9300,9320,9324,9388],{"__ignoreMap":87},[91,9237,9238,9240,9243,9245,9247,9249,9251,9253,9255,9257,9259,9261,9263,9265,9267,9269,9271,9273,9275],{"class":93,"line":94},[91,9239,399],{"class":104},[91,9241,9242],{"class":135}," make",[91,9244,405],{"class":108},[91,9246,409],{"class":408},[91,9248,112],{"class":104},[91,9250,116],{"class":115},[91,9252,416],{"class":108},[91,9254,419],{"class":408},[91,9256,112],{"class":104},[91,9258,470],{"class":115},[91,9260,426],{"class":108},[91,9262,112],{"class":104},[91,9264,1085],{"class":108},[91,9266,2689],{"class":408},[91,9268,112],{"class":104},[91,9270,116],{"class":115},[91,9272,7513],{"class":108},[91,9274,802],{"class":104},[91,9276,9277],{"class":108}," { id: a } }\n",[91,9279,9280,9282,9285,9287,9290,9292,9294,9297],{"class":93,"line":101},[91,9281,1213],{"class":104},[91,9283,9284],{"class":135}," R",[91,9286,119],{"class":104},[91,9288,9289],{"class":135}," ReturnType",[91,9291,955],{"class":108},[91,9293,1223],{"class":104},[91,9295,9296],{"class":108}," make>       ",[91,9298,9299],{"class":97},"\u002F\u002F { id: number }\n",[91,9301,9302,9304,9306,9308,9311,9313,9315,9317],{"class":93,"line":129},[91,9303,1213],{"class":104},[91,9305,8567],{"class":135},[91,9307,119],{"class":104},[91,9309,9310],{"class":135}," Parameters",[91,9312,955],{"class":108},[91,9314,1223],{"class":104},[91,9316,9296],{"class":108},[91,9318,9319],{"class":97},"\u002F\u002F [number, string]\n",[91,9321,9322],{"class":93,"line":453},[91,9323,567],{"emptyLinePlaceholder":566},[91,9325,9326,9328,9331,9333,9335,9337,9339,9341,9343,9345,9347,9349,9351,9353,9355,9357,9359,9361,9363,9365,9367,9369,9371,9373,9375,9378,9380,9382,9384,9386],{"class":93,"line":459},[91,9327,1213],{"class":104},[91,9329,9330],{"class":135}," MyReturnType",[91,9332,955],{"class":108},[91,9334,5226],{"class":135},[91,9336,6500],{"class":104},[91,9338,1220],{"class":108},[91,9340,3527],{"class":104},[91,9342,409],{"class":408},[91,9344,112],{"class":104},[91,9346,1279],{"class":115},[91,9348,2574],{"class":108},[91,9350,2577],{"class":104},[91,9352,1279],{"class":115},[91,9354,960],{"class":108},[91,9356,476],{"class":104},[91,9358,6045],{"class":135},[91,9360,6500],{"class":104},[91,9362,1220],{"class":108},[91,9364,3527],{"class":104},[91,9366,409],{"class":408},[91,9368,112],{"class":104},[91,9370,1279],{"class":115},[91,9372,2574],{"class":108},[91,9374,2577],{"class":104},[91,9376,9377],{"class":104}," infer",[91,9379,9284],{"class":135},[91,9381,7384],{"class":104},[91,9383,9284],{"class":135},[91,9385,3442],{"class":104},[91,9387,9170],{"class":115},[91,9389,9390,9392,9395,9397,9399,9401,9403,9405,9407,9409,9411,9413,9415,9417,9419,9421,9423,9425,9427,9429,9431,9433,9435,9437,9439,9441,9443,9445,9447,9449],{"class":93,"line":611},[91,9391,1213],{"class":104},[91,9393,9394],{"class":135}," MyParameters",[91,9396,955],{"class":108},[91,9398,5226],{"class":135},[91,9400,6500],{"class":104},[91,9402,1220],{"class":108},[91,9404,3527],{"class":104},[91,9406,409],{"class":408},[91,9408,112],{"class":104},[91,9410,1279],{"class":115},[91,9412,2574],{"class":108},[91,9414,2577],{"class":104},[91,9416,1279],{"class":115},[91,9418,960],{"class":108},[91,9420,476],{"class":104},[91,9422,6045],{"class":135},[91,9424,6500],{"class":104},[91,9426,1220],{"class":108},[91,9428,3527],{"class":104},[91,9430,409],{"class":408},[91,9432,112],{"class":104},[91,9434,9377],{"class":104},[91,9436,8567],{"class":135},[91,9438,2574],{"class":108},[91,9440,2577],{"class":104},[91,9442,1279],{"class":115},[91,9444,7384],{"class":104},[91,9446,8567],{"class":135},[91,9448,3442],{"class":104},[91,9450,9170],{"class":115},[15,9452,9453,9455,9456,9459],{},[36,9454,8278],{}," 的写法先用了 ",[36,9457,9458],{},"infer","（第十四章）——「在匹配函数形状时，把返回位置的那段抽出来叫 R」。这里记住结论：这几个工具是「把函数类型当数据来拆零件」。",[28,9461,9463,9466],{"id":9462},"awaitedt扒开-promise-的外壳",[36,9464,9465],{},"Awaited\u003CT>","：扒开 Promise 的外壳",[15,9468,9469,9471,9472,729,9475,9478],{},[36,9470,9465],{}," 递归地剥掉 ",[36,9473,9474],{},"Promise",[36,9476,9477],{},"thenable"," 外层，拿到最终的值类型，专门配合异步：",[82,9480,9482],{"className":84,"code":9481,"language":86,"meta":87,"style":87},"type V = Awaited\u003CPromise\u003CPromise\u003Cstring>>>   \u002F\u002F string（反复剥到底）\n\ntype MyAwaited\u003CT> =\n  T extends PromiseLike\u003Cinfer U>\n    ? T extends PromiseLike\u003CT> ? never : MyAwaited\u003CU>   \u002F\u002F 递归剥\n    : T\n",[36,9483,9484,9514,9518,9534,9553,9587],{"__ignoreMap":87},[91,9485,9486,9488,9491,9493,9496,9498,9500,9502,9504,9506,9508,9511],{"class":93,"line":94},[91,9487,1213],{"class":104},[91,9489,9490],{"class":135}," V",[91,9492,119],{"class":104},[91,9494,9495],{"class":135}," Awaited",[91,9497,955],{"class":108},[91,9499,9474],{"class":135},[91,9501,955],{"class":108},[91,9503,9474],{"class":135},[91,9505,955],{"class":108},[91,9507,728],{"class":115},[91,9509,9510],{"class":108},">>>   ",[91,9512,9513],{"class":97},"\u002F\u002F string（反复剥到底）\n",[91,9515,9516],{"class":93,"line":101},[91,9517,567],{"emptyLinePlaceholder":566},[91,9519,9520,9522,9525,9527,9529,9531],{"class":93,"line":129},[91,9521,1213],{"class":104},[91,9523,9524],{"class":135}," MyAwaited",[91,9526,955],{"class":108},[91,9528,5226],{"class":135},[91,9530,960],{"class":108},[91,9532,9533],{"class":104},"=\n",[91,9535,9536,9539,9541,9544,9546,9548,9550],{"class":93,"line":453},[91,9537,9538],{"class":135},"  T",[91,9540,6500],{"class":104},[91,9542,9543],{"class":135}," PromiseLike",[91,9545,955],{"class":108},[91,9547,9458],{"class":104},[91,9549,9125],{"class":135},[91,9551,9552],{"class":108},">\n",[91,9554,9555,9558,9560,9562,9564,9566,9568,9570,9572,9574,9576,9578,9580,9582,9584],{"class":93,"line":459},[91,9556,9557],{"class":104},"    ?",[91,9559,6045],{"class":135},[91,9561,6500],{"class":104},[91,9563,9543],{"class":135},[91,9565,955],{"class":108},[91,9567,5226],{"class":135},[91,9569,960],{"class":108},[91,9571,2142],{"class":104},[91,9573,1590],{"class":115},[91,9575,3442],{"class":104},[91,9577,9524],{"class":135},[91,9579,955],{"class":108},[91,9581,9114],{"class":135},[91,9583,5524],{"class":108},[91,9585,9586],{"class":97},"\u002F\u002F 递归剥\n",[91,9588,9589,9592],{"class":93,"line":611},[91,9590,9591],{"class":104},"    :",[91,9593,9204],{"class":135},[15,9595,9596,9597,9600,9601,9603],{},"注意它处理的是「类型」不是「值」：给一个 ",[36,9598,9599],{},"Promise\u003Cnumber>"," 的类型，得到 ",[36,9602,732],{}," 类型。取实际异步函数返回值的类型时非常顺手。",[28,9605,9607,9610],{"id":9606},"recordk-v键类型到值类型的字典",[36,9608,9609],{},"Record\u003CK, V>","：键类型到值类型的字典",[15,9612,9613,9615],{},[36,9614,9609],{}," = 「K 里每个键都映射到 V」，比手写索引签名更能表达「键是有限的几个」：",[82,9617,9619],{"className":84,"code":9618,"language":86,"meta":87,"style":87},"type Pages = Record\u003C'home' | 'about', string>   \u002F\u002F { home: string; about: string }\ntype Dict = Record\u003Cstring, number>              \u002F\u002F 等价于 { [k: string]: number }\n\ntype MyRecord\u003CK extends keyof any, V> = { [P in K]: V }\n",[36,9620,9621,9651,9676,9680],{"__ignoreMap":87},[91,9622,9623,9625,9628,9630,9632,9634,9637,9639,9642,9644,9646,9648],{"class":93,"line":94},[91,9624,1213],{"class":104},[91,9626,9627],{"class":135}," Pages",[91,9629,119],{"class":104},[91,9631,5512],{"class":135},[91,9633,955],{"class":108},[91,9635,9636],{"class":122},"'home'",[91,9638,2072],{"class":104},[91,9640,9641],{"class":122}," 'about'",[91,9643,416],{"class":108},[91,9645,728],{"class":115},[91,9647,5524],{"class":108},[91,9649,9650],{"class":97},"\u002F\u002F { home: string; about: string }\n",[91,9652,9653,9655,9658,9660,9662,9664,9666,9668,9670,9673],{"class":93,"line":101},[91,9654,1213],{"class":104},[91,9656,9657],{"class":135}," Dict",[91,9659,119],{"class":104},[91,9661,5512],{"class":135},[91,9663,955],{"class":108},[91,9665,728],{"class":115},[91,9667,416],{"class":108},[91,9669,732],{"class":115},[91,9671,9672],{"class":108},">              ",[91,9674,9675],{"class":97},"\u002F\u002F 等价于 { [k: string]: number }\n",[91,9677,9678],{"class":93,"line":129},[91,9679,567],{"emptyLinePlaceholder":566},[91,9681,9682,9684,9687,9689,9691,9693,9695,9697,9699,9702,9704,9706,9708,9710,9712,9714,9716,9718,9720],{"class":93,"line":453},[91,9683,1213],{"class":104},[91,9685,9686],{"class":135}," MyRecord",[91,9688,955],{"class":108},[91,9690,8089],{"class":135},[91,9692,6500],{"class":104},[91,9694,5973],{"class":104},[91,9696,1279],{"class":115},[91,9698,416],{"class":108},[91,9700,9701],{"class":135},"V",[91,9703,960],{"class":108},[91,9705,476],{"class":104},[91,9707,2387],{"class":108},[91,9709,8889],{"class":135},[91,9711,4351],{"class":104},[91,9713,8060],{"class":135},[91,9715,2397],{"class":108},[91,9717,112],{"class":104},[91,9719,9490],{"class":135},[91,9721,1124],{"class":108},[15,9723,9724,8965,9726,9728,9729,9731],{},[36,9725,8275],{},[36,9727,8089],{},"（键联合），每个键的值统一是 ",[36,9730,9701],{},"。它常用于给枚举\u002F联合键做完整的映射表，编译器会检查你有没有把每个键都填上。",[28,9733,9734],{"id":9734},"选用的分寸",[15,9736,9737,9738,9741,9742],{},"工具类型好用，但别堆砌。链上七八层 ",[36,9739,9740],{},"Omit\u003CPick\u003CPartial\u003C...>>>"," 会让报错信息变成天书、也让同事读不动。",[19,9743,9744],{},"先写朴素接口，需要变换时再上工具类型；能用一句话讲清它做了什么，才值得这么写。",[10,9746,9748,9749,9751],{"id":9747},"十四条件类型与-infer模板字面量类型类型体操入门","十四、条件类型与 ",[36,9750,9458],{},"、模板字面量类型：类型体操入门",[15,9753,9754,9755,9758],{},"这一章进入 TS 的「类型也能做运算」的深水区。日常业务里，你",[19,9756,9757],{},"大概率不需要写这么深","——绝大多数页面代码停在第十三章就绰绰有余。但读懂库作者写的类型、以及享受工具类型带来的推断，需要这里的知识。只讲够用的几个概念，配两三个例子，不追求把体操做到极限。",[28,9760,9762],{"id":9761},"条件类型类型层面的三元","条件类型：类型层面的三元",[15,9764,9765,9768],{},[36,9766,9767],{},"T extends U ? X : Y"," 是「如果 T 能赋给 U 就取 X 否则取 Y」，一个写在类型位置的三元表达式：",[82,9770,9772],{"className":84,"code":9771,"language":86,"meta":87,"style":87},"type IsNumber\u003CT> = T extends number ? true : false\ntype A = IsNumber\u003C42>      \u002F\u002F true\ntype B = IsNumber\u003C'x'>     \u002F\u002F false\n",[36,9773,9774,9804,9823],{"__ignoreMap":87},[91,9775,9776,9778,9781,9783,9785,9787,9789,9791,9793,9795,9797,9799,9801],{"class":93,"line":94},[91,9777,1213],{"class":104},[91,9779,9780],{"class":135}," IsNumber",[91,9782,955],{"class":108},[91,9784,5226],{"class":135},[91,9786,960],{"class":108},[91,9788,476],{"class":104},[91,9790,6045],{"class":135},[91,9792,6500],{"class":104},[91,9794,116],{"class":115},[91,9796,7384],{"class":104},[91,9798,674],{"class":115},[91,9800,3442],{"class":104},[91,9802,9803],{"class":115}," false\n",[91,9805,9806,9808,9810,9812,9814,9816,9818,9820],{"class":93,"line":101},[91,9807,1213],{"class":104},[91,9809,7664],{"class":135},[91,9811,119],{"class":104},[91,9813,9780],{"class":135},[91,9815,955],{"class":108},[91,9817,7542],{"class":115},[91,9819,9030],{"class":108},[91,9821,9822],{"class":97},"\u002F\u002F true\n",[91,9824,9825,9827,9829,9831,9833,9835,9837,9839],{"class":93,"line":129},[91,9826,1213],{"class":104},[91,9828,7674],{"class":135},[91,9830,119],{"class":104},[91,9832,9780],{"class":135},[91,9834,955],{"class":108},[91,9836,7232],{"class":122},[91,9838,8826],{"class":108},[91,9840,9841],{"class":97},"\u002F\u002F false\n",[15,9843,9844,9845,9848,9849,9851,9852,9855,9856,9858,9859,9862,9863,186,9865,186,9867,9870,9871,9873,9874,9876,9877,9880],{},"关键特性是",[19,9846,9847],{},"分发（distributive）","：当 ",[36,9850,5226],{}," 是裸类型参数且是联合，条件类型会",[19,9853,9854],{},"对联合的每个成员分别计算再并起来","。这就是 ",[36,9857,8947],{}," 能「逐个筛掉」的原理：",[36,9860,9861],{},"('a'|'b'|'c') extends 'a' ? never : T"," 会拆成对 ",[36,9864,482],{},[36,9866,487],{},[36,9868,9869],{},"'c'"," 各自判断，命中 ",[36,9872,482],{}," 变 ",[36,9875,828],{}," 被丢，剩下的并回联合。想关掉分发，把 T 包进元组 ",[36,9878,9879],{},"[T] extends [U]"," 即可。",[28,9882,9884,9886],{"id":9883},"infer在匹配中捕获一个类型",[36,9885,9458],{},"：在匹配中「捕获」一个类型",[15,9888,9889,9892,9893,186,9895,9898],{},[36,9890,9891],{},"infer R"," 写在条件类型的「被匹配的形状」里，意思是「这一段随它是什么，抽出来命名叫 R」。前面 ",[36,9894,8278],{},[36,9896,9897],{},"Awaited"," 都靠它：",[82,9900,9902],{"className":84,"code":9901,"language":86,"meta":87,"style":87},"type First\u003CT> = T extends [infer F, ...any[]] ? F : never\ntype F = First\u003C[string, number]>     \u002F\u002F string\n\ntype Flatten\u003CT> = T extends Array\u003Cinfer U> ? U : T\ntype E = Flatten\u003Cnumber[]>           \u002F\u002F number\n",[36,9903,9904,9946,9971,9975,10012],{"__ignoreMap":87},[91,9905,9906,9908,9910,9912,9914,9916,9918,9920,9922,9924,9926,9929,9931,9933,9935,9938,9940,9942,9944],{"class":93,"line":94},[91,9907,1213],{"class":104},[91,9909,8240],{"class":135},[91,9911,955],{"class":108},[91,9913,5226],{"class":135},[91,9915,960],{"class":108},[91,9917,476],{"class":104},[91,9919,6045],{"class":135},[91,9921,6500],{"class":104},[91,9923,479],{"class":108},[91,9925,9458],{"class":104},[91,9927,9928],{"class":135}," F",[91,9930,416],{"class":108},[91,9932,3527],{"class":104},[91,9934,513],{"class":115},[91,9936,9937],{"class":108},"[]] ",[91,9939,2142],{"class":104},[91,9941,9928],{"class":135},[91,9943,3442],{"class":104},[91,9945,9170],{"class":115},[91,9947,9948,9950,9952,9954,9956,9959,9961,9963,9965,9968],{"class":93,"line":101},[91,9949,1213],{"class":104},[91,9951,9928],{"class":135},[91,9953,119],{"class":104},[91,9955,8240],{"class":135},[91,9957,9958],{"class":108},"\u003C[",[91,9960,728],{"class":115},[91,9962,416],{"class":108},[91,9964,732],{"class":115},[91,9966,9967],{"class":108},"]>     ",[91,9969,9970],{"class":97},"\u002F\u002F string\n",[91,9972,9973],{"class":93,"line":129},[91,9974,567],{"emptyLinePlaceholder":566},[91,9976,9977,9979,9982,9984,9986,9988,9990,9992,9994,9996,9998,10000,10002,10004,10006,10008,10010],{"class":93,"line":453},[91,9978,1213],{"class":104},[91,9980,9981],{"class":135}," Flatten",[91,9983,955],{"class":108},[91,9985,5226],{"class":135},[91,9987,960],{"class":108},[91,9989,476],{"class":104},[91,9991,6045],{"class":135},[91,9993,6500],{"class":104},[91,9995,952],{"class":135},[91,9997,955],{"class":108},[91,9999,9458],{"class":104},[91,10001,9125],{"class":135},[91,10003,960],{"class":108},[91,10005,2142],{"class":104},[91,10007,9125],{"class":135},[91,10009,3442],{"class":104},[91,10011,9204],{"class":135},[91,10013,10014,10016,10019,10021,10023,10025,10027,10030],{"class":93,"line":459},[91,10015,1213],{"class":104},[91,10017,10018],{"class":135}," E",[91,10020,119],{"class":104},[91,10022,9981],{"class":135},[91,10024,955],{"class":108},[91,10026,732],{"class":115},[91,10028,10029],{"class":108},"[]>           ",[91,10031,8233],{"class":97},[15,10033,10034,10036],{},[36,10035,9458],{}," 让你从「一个复杂的类型」里按形状「解构」出想要的局部，是类型体操里最常用的捕获手段。",[28,10038,10040],{"id":10039},"模板字面量类型在类型层面拼字符串","模板字面量类型：在类型层面拼字符串",[15,10042,10043,10044,10047],{},"TS 4.1 起，字符串字面量类型支持模板拼接，用 ",[36,10045,10046],{},"`${A}`"," 形式：",[82,10049,10051],{"className":84,"code":10050,"language":86,"meta":87,"style":87},"type World = 'world'\ntype Greeting = `hello ${World}`     \u002F\u002F 'hello world'\n\ntype EventName\u003CT extends string> = `on${Capitalize\u003CT>}`\ntype Click = EventName\u003C'click'>      \u002F\u002F 'onClick'\n\ntype Style = `${number}px` | `${number}%`\nfunction setSize(s: Style) {}\nsetSize('10px')     \u002F\u002F ✓\nsetSize('big')      \u002F\u002F ✗ 不符合模板\n",[36,10052,10053,10065,10085,10089,10123,10144,10148,10173,10191,10205],{"__ignoreMap":87},[91,10054,10055,10057,10060,10062],{"class":93,"line":94},[91,10056,1213],{"class":104},[91,10058,10059],{"class":135}," World",[91,10061,119],{"class":104},[91,10063,10064],{"class":122}," 'world'\n",[91,10066,10067,10069,10072,10074,10077,10080,10082],{"class":93,"line":101},[91,10068,1213],{"class":104},[91,10070,10071],{"class":135}," Greeting",[91,10073,119],{"class":104},[91,10075,10076],{"class":122}," `hello ${",[91,10078,10079],{"class":135},"World",[91,10081,3439],{"class":122},[91,10083,10084],{"class":97},"     \u002F\u002F 'hello world'\n",[91,10086,10087],{"class":93,"line":129},[91,10088,567],{"emptyLinePlaceholder":566},[91,10090,10091,10093,10096,10098,10100,10102,10104,10106,10108,10111,10114,10116,10118,10121],{"class":93,"line":453},[91,10092,1213],{"class":104},[91,10094,10095],{"class":135}," EventName",[91,10097,955],{"class":108},[91,10099,5226],{"class":135},[91,10101,6500],{"class":104},[91,10103,470],{"class":115},[91,10105,960],{"class":108},[91,10107,476],{"class":104},[91,10109,10110],{"class":122}," `on${",[91,10112,10113],{"class":135},"Capitalize",[91,10115,955],{"class":122},[91,10117,5226],{"class":135},[91,10119,10120],{"class":122},">",[91,10122,3493],{"class":122},[91,10124,10125,10127,10130,10132,10134,10136,10139,10141],{"class":93,"line":459},[91,10126,1213],{"class":104},[91,10128,10129],{"class":135}," Click",[91,10131,119],{"class":104},[91,10133,10095],{"class":135},[91,10135,955],{"class":108},[91,10137,10138],{"class":122},"'click'",[91,10140,9030],{"class":108},[91,10142,10143],{"class":97},"\u002F\u002F 'onClick'\n",[91,10145,10146],{"class":93,"line":611},[91,10147,567],{"emptyLinePlaceholder":566},[91,10149,10150,10152,10155,10157,10159,10161,10164,10166,10168,10170],{"class":93,"line":1494},[91,10151,1213],{"class":104},[91,10153,10154],{"class":135}," Style",[91,10156,119],{"class":104},[91,10158,3429],{"class":122},[91,10160,732],{"class":115},[91,10162,10163],{"class":122},"}px`",[91,10165,2072],{"class":104},[91,10167,3429],{"class":122},[91,10169,732],{"class":115},[91,10171,10172],{"class":122},"}%`\n",[91,10174,10175,10177,10180,10182,10184,10186,10188],{"class":93,"line":2591},[91,10176,399],{"class":104},[91,10178,10179],{"class":135}," setSize",[91,10181,405],{"class":108},[91,10183,3006],{"class":408},[91,10185,112],{"class":104},[91,10187,10154],{"class":135},[91,10189,10190],{"class":108},") {}\n",[91,10192,10193,10196,10198,10201,10203],{"class":93,"line":2602},[91,10194,10195],{"class":135},"setSize",[91,10197,405],{"class":108},[91,10199,10200],{"class":122},"'10px'",[91,10202,3887],{"class":108},[91,10204,5745],{"class":97},[91,10206,10207,10209,10211,10214,10217],{"class":93,"line":2621},[91,10208,10195],{"class":135},[91,10210,405],{"class":108},[91,10212,10213],{"class":122},"'big'",[91,10215,10216],{"class":108},")      ",[91,10218,10219],{"class":97},"\u002F\u002F ✗ 不符合模板\n",[15,10221,10222,10223,729,10225,729,10228,10231],{},"模板字面量类型最实用的一面：给「有规律的事件名、路由、CSS 单位」这类字符串建立模式约束，让拼错在编译期就报错。用 ",[36,10224,10113],{},[36,10226,10227],{},"Lowercase",[36,10229,10230],{},"Uppercase"," 等字符串工具类型（TS 内置）还能做大小写变换。",[28,10233,10234],{"id":10234},"分寸再强调一遍",[15,10236,10237,10238,10241,10242,10244],{},"上面这些组合起来能做「类型层面的编程」，理论上能写很复杂的推断。但",[19,10239,10240],{},"在业务代码里写它们要极其克制","：库的 ",[36,10243,327],{},"、通用的类型工具值得精雕；一个具体页面的 props 用不着条件类型套娃。读懂 > 会写 > 会炫。本章示例点到为止，能认出「哦这是在用 infer 抽返回类型」就够了。",[10,10246,10248,10249,186,10252,10254],{"id":10247},"十五模块与声明文件esmimport-typedts-与给无类型库写声明","十五、模块与声明文件：ESM、",[36,10250,10251],{},"import type",[36,10253,327],{}," 与给无类型库写声明",[15,10256,10257,10258,729,10261,10264,10265,10267,10268,10270,10271,10274],{},"JS 教程第二十章讲过 ESM 的 ",[36,10259,10260],{},"import",[36,10262,10263],{},"export"," 语法。这一章讲 TS 在模块之上加的东西：",[36,10266,10251],{},"、被擦除后的类型去哪了、",[36,10269,327],{}," 声明文件是什么、怎么用 ",[36,10272,10273],{},"declare"," 给一个没有类型的 JS 库「补上」类型，以及遇到「找不到类型声明」时的三条出路。",[28,10276,10278,10279],{"id":10277},"esm-与-import-type","ESM 与 ",[36,10280,10251],{},[15,10282,10283,10284,10287,10288,10291,10292,10294],{},"TS 里导入分两种：导",[19,10285,10286],{},"值","（运行时真要有这个东西）和导",[19,10289,10290],{},"类型","（编译期用完就擦除，运行时根本不需要它存在）。把「只用来做类型」的导入写成 ",[36,10293,10251],{},"，是明确的信号也是优化：",[82,10296,10298],{"className":84,"code":10297,"language":86,"meta":87,"style":87},"import type { User } from '.\u002Ftypes'     \u002F\u002F 纯类型，擦除后这条 import 整个消失\nimport { getUser } from '.\u002Fapi'          \u002F\u002F 值，运行时真的要用\n",[36,10299,10300,10319],{"__ignoreMap":87},[91,10301,10302,10304,10307,10310,10313,10316],{"class":93,"line":94},[91,10303,10260],{"class":104},[91,10305,10306],{"class":104}," type",[91,10308,10309],{"class":108}," { User } ",[91,10311,10312],{"class":104},"from",[91,10314,10315],{"class":122}," '.\u002Ftypes'",[91,10317,10318],{"class":97},"     \u002F\u002F 纯类型，擦除后这条 import 整个消失\n",[91,10320,10321,10323,10326,10328,10331],{"class":93,"line":101},[91,10322,10260],{"class":104},[91,10324,10325],{"class":108}," { getUser } ",[91,10327,10312],{"class":104},[91,10329,10330],{"class":122}," '.\u002Fapi'",[91,10332,10333],{"class":97},"          \u002F\u002F 值，运行时真的要用\n",[15,10335,10336,10337,10339,10340,10342,10343,10346,10347,514,10349,10352,10353,10355],{},"为什么区分？因为擦除。",[36,10338,230],{}," 是 ",[36,10341,1213],{},"，运行时不存在，若写成普通 ",[36,10344,10345],{},"import { User }"," 再只在类型位置用它，打包器可能把整模块拽进产物却找不到 ",[36,10348,230],{},[36,10350,10351],{},"isolatedModules"," 下会直接报错，见第十七章）。",[36,10354,10251],{}," 让「这是类型、别当值处理」一目了然，也让单文件转译（各文件独立擦除）能安全进行。",[28,10357,10359,10361],{"id":10358},"dts-是什么",[36,10360,327],{}," 是什么",[15,10363,10364,10366,10367,10370,10371,10373,10374,514,10376,10379,10380,74],{},[36,10365,327],{}," 是",[19,10368,10369],{},"只有类型、没有实现","的声明文件。它是库对外发布的「类型说明书」：编译器读它知道 API 长什么样，运行时它一行代码都不产出。你自己的 ",[36,10372,56],{}," 编译时会附带产出 ",[36,10375,327],{},[36,10377,10378],{},"declaration: true","），第三方库的类型则来自 npm 上的包或 ",[36,10381,10382],{},"@types\u002F*",[82,10384,10386],{"className":84,"code":10385,"language":86,"meta":87,"style":87},"\u002F\u002F some-lib\u002Findex.d.ts —— 只声明，不实现\nexport interface Options { timeout?: number }\nexport function request(url: string, opts?: Options): Promise\u003CResponse>\n",[36,10387,10388,10393,10414],{"__ignoreMap":87},[91,10389,10390],{"class":93,"line":94},[91,10391,10392],{"class":97},"\u002F\u002F some-lib\u002Findex.d.ts —— 只声明，不实现\n",[91,10394,10395,10397,10400,10403,10405,10408,10410,10412],{"class":93,"line":101},[91,10396,10263],{"class":104},[91,10398,10399],{"class":104}," interface",[91,10401,10402],{"class":135}," Options",[91,10404,1085],{"class":108},[91,10406,10407],{"class":408},"timeout",[91,10409,2180],{"class":104},[91,10411,116],{"class":115},[91,10413,1124],{"class":108},[91,10415,10416,10418,10421,10424,10426,10429,10431,10433,10435,10438,10440,10442,10444,10446,10449,10451,10454],{"class":93,"line":129},[91,10417,10263],{"class":104},[91,10419,10420],{"class":104}," function",[91,10422,10423],{"class":135}," request",[91,10425,405],{"class":108},[91,10427,10428],{"class":408},"url",[91,10430,112],{"class":104},[91,10432,470],{"class":115},[91,10434,416],{"class":108},[91,10436,10437],{"class":408},"opts",[91,10439,2180],{"class":104},[91,10441,10402],{"class":135},[91,10443,426],{"class":108},[91,10445,112],{"class":104},[91,10447,10448],{"class":135}," Promise",[91,10450,955],{"class":108},[91,10452,10453],{"class":135},"Response",[91,10455,9552],{"class":108},[28,10457,10459,1685,10461],{"id":10458},"declare-与-declare-module",[36,10460,10273],{},[36,10462,10463],{},"declare module",[15,10465,10466,10468],{},[36,10467,10273],{}," 关键字用于「告诉编译器：外面有这个东西，你别生成也别检查，信我」。给一个全局脚本变量、一段内联的 JS 库打类型，就靠它：",[82,10470,10472],{"className":84,"code":10471,"language":86,"meta":87,"style":87},"\u002F\u002F 描述一个挂在 window 上的全局（例如某统计脚本）\ndeclare const _hmt: string[][]\n\n\u002F\u002F 给一个「其实有、但没类型」的模块声明形状\ndeclare module 'legacy-lib' {\n  export function doThing(x: number): void\n}\n",[36,10473,10474,10479,10495,10499,10504,10516,10540],{"__ignoreMap":87},[91,10475,10476],{"class":93,"line":94},[91,10477,10478],{"class":97},"\u002F\u002F 描述一个挂在 window 上的全局（例如某统计脚本）\n",[91,10480,10481,10483,10485,10488,10490,10492],{"class":93,"line":101},[91,10482,10273],{"class":104},[91,10484,5384],{"class":104},[91,10486,10487],{"class":115}," _hmt",[91,10489,112],{"class":104},[91,10491,470],{"class":115},[91,10493,10494],{"class":108},"[][]\n",[91,10496,10497],{"class":93,"line":129},[91,10498,567],{"emptyLinePlaceholder":566},[91,10500,10501],{"class":93,"line":453},[91,10502,10503],{"class":97},"\u002F\u002F 给一个「其实有、但没类型」的模块声明形状\n",[91,10505,10506,10508,10511,10514],{"class":93,"line":459},[91,10507,10273],{"class":104},[91,10509,10510],{"class":104}," module",[91,10512,10513],{"class":122}," 'legacy-lib'",[91,10515,1723],{"class":108},[91,10517,10518,10521,10523,10526,10528,10530,10532,10534,10536,10538],{"class":93,"line":611},[91,10519,10520],{"class":104},"  export",[91,10522,10420],{"class":104},[91,10524,10525],{"class":135}," doThing",[91,10527,405],{"class":108},[91,10529,1852],{"class":408},[91,10531,112],{"class":104},[91,10533,116],{"class":115},[91,10535,426],{"class":108},[91,10537,112],{"class":104},[91,10539,2534],{"class":115},[91,10541,10542],{"class":93,"line":1494},[91,10543,456],{"class":108},[15,10545,10546,10549,10550,10552,10553,10555,10556,10558,10559,10562],{},[36,10547,10548],{},"declare module '名字'"," 是给「JS 库但没带 ",[36,10551,327],{},"」补类型的标准姿势——你在项目里建一个 ",[36,10554,327],{},"，用 ",[36,10557,10463],{}," 声明这个包对外暴露的形状，之后 ",[36,10560,10561],{},"import ... from 'legacy-lib'"," 就有类型了。",[28,10564,10566,10569,10570,6662],{"id":10565},"types-与包里的-types-字段",[36,10567,10568],{},"@types"," 与包里的 ",[36,10571,10572],{},"types",[15,10574,10575],{},"一个包怎么被找到类型？两条路：",[204,10577,10578,10594],{},[48,10579,10580,10581,10584,10585,814,10587,10590,10591,10593],{},"包自己内置：",[36,10582,10583],{},"package.json"," 里 ",[36,10586,10572],{},[36,10588,10589],{},"typings"," 字段指向它的 ",[36,10592,327],{},"（现代 TS 库普遍这么发布）。",[48,10595,10596,10597,10600,10601,186,10604,10607,10608,9880],{},"社区补：DefinitelyTyped 维护、发布成 ",[36,10598,10599],{},"@types\u002F包名","（如 ",[36,10602,10603],{},"@types\u002Flodash",[36,10605,10606],{},"@types\u002Freact","），",[36,10609,10610],{},"npm i -D @types\u002Fxxx",[15,10612,10613,10614,10617,10618,10620,10621,74],{},"编译器解析 ",[36,10615,10616],{},"import 'lodash'"," 时，先看包自身 ",[36,10619,10572],{}," 字段，没有再找 ",[36,10622,10623],{},"node_modules\u002F@types\u002Flodash",[28,10625,10626],{"id":10626},"找不到类型声明的三种解法",[15,10628,10629,10632,10633,10636],{},[36,10630,10631],{},"import 'xxx'"," 报「找不到模块声明文件」（TS7016 之类）时，别急着 ",[36,10634,10635],{},"\u002F\u002F @ts-ignore","，三条正规出路：",[45,10638,10639,10647,10659],{},[48,10640,10641,211,10644,10646],{},[19,10642,10643],{},"装社区类型",[36,10645,10610],{},"——最常见，多数流行库都有。",[48,10648,10649,10654,10655,10658],{},[19,10650,10651,10652],{},"自己写 ",[36,10653,10463],{},"：在项目的 ",[36,10656,10657],{},"types\u002Fxxx.d.ts"," 里声明它的形状，哪怕先只写你用到的几个函数。",[48,10660,10661,211,10664,10667,10668,10670,10671,10674],{},[19,10662,10663],{},"临时兜底",[36,10665,10666],{},"declare module 'xxx'","（不写内容）把它当 ",[36,10669,513],{}," 放行，或开 ",[36,10672,10673],{},"allowJs"," 直接吃它的 JS——但这是把类型关掉，属于过渡不是终点。",[15,10676,10677],{},"顺序优先选 1，其次 2；3 只在时间紧迫时短暂使用，并留待补齐。",[10,10679,10681],{"id":10680},"十六tsconfig-全解编译选项的真实含义","十六、tsconfig 全解：编译选项的真实含义",[15,10683,10684,10687,10688,10691,10692,729,10694,729,10697,729,10700,10703,10704,10706,10707,729,10710,729,10713,729,10716,10719,10720,10722],{},[36,10685,10686],{},"tsconfig.json"," 是 TS 项目的控制台。这一章不逐条背，而是把",[19,10689,10690],{},"最常配错、影响最大","的几组讲透：",[36,10693,69],{},[36,10695,10696],{},"lib",[36,10698,10699],{},"module",[36,10701,10702],{},"moduleResolution"," 的真实差别、",[36,10705,1786],{}," 家族各自开关、",[36,10708,10709],{},"include",[36,10711,10712],{},"exclude",[36,10714,10715],{},"paths",[36,10717,10718],{},"noEmit","，以及「",[36,10721,10718],{}," + 打包器」为什么才是现代主流组合。",[789,10724],{"height":1805,"src":10725,"title":10726},"\u002Ftutorials\u002Fts\u002Fdemos\u002F16-tsconfig.html","示例 16：tsconfig 关键选项——改了会影响什么",[28,10728,10730,814,10732,10734],{"id":10729},"target-lib转译产物-vs-允许用的-api",[36,10731,69],{},[36,10733,10696],{},"：转译产物 vs 允许用的 API",[15,10736,10737],{},"这两个最容易混，分清就赢了一半：",[204,10739,10740,10754],{},[48,10741,10742,10746,10747,74,10750,10753],{},[19,10743,10744],{},[36,10745,69],{},"：决定",[19,10748,10749],{},"语法降级到哪一档 ES 版本",[36,10751,10752],{},"target: \"es2020\""," 意味着 TS 会把更新的语法（比如可选链、逻辑赋值）降级成 ES2020 能跑的 JS。它管的是「语法」。",[48,10755,10756,10746,10760,10763,10764,186,10766,186,10769,186,10772,10775,10776,10778],{},[19,10757,10758],{},[36,10759,10696],{},[19,10761,10762],{},"编译器认为环境里有哪些内置 API","。用 ",[36,10765,9474],{},[36,10767,10768],{},"Array.prototype.includes",[36,10770,10771],{},"fetch",[36,10773,10774],{},"Map"," 这些，都要 ",[36,10777,10696],{}," 里有对应声明才不报「找不到」。",[15,10780,10781,10782,5106,10784,10787,10788,10791,10792,10794,10795,10797,10798,10801,10802,10804],{},"关键陷阱：",[36,10783,69],{},[19,10785,10786],{},"不会","帮你 polyfill 任何 API。你 ",[36,10789,10790],{},"target: \"es5\""," 但代码里用了 ",[36,10793,9474],{},"，编译不报错（因为 ",[36,10796,10696],{}," 默认按 target 推），产物里也不会凭空出现 Promise——运行时 ",[36,10799,10800],{},"Promise is not defined"," 照崩。",[36,10803,10696],{}," 是「我信任运行环境有这些东西」的声明，不是「请帮我把缺的补上」。",[82,10806,10810],{"className":10807,"code":10808,"language":10809,"meta":87,"style":87},"language-jsonc shiki shiki-themes github-dark","{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",          \u002F\u002F 语法降到 ES2020\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"]  \u002F\u002F 允许用这些环境 API\n  }\n}\n","jsonc",[36,10811,10812,10817,10822,10830,10838,10842],{"__ignoreMap":87},[91,10813,10814],{"class":93,"line":94},[91,10815,10816],{},"{\n",[91,10818,10819],{"class":93,"line":101},[91,10820,10821],{},"  \"compilerOptions\": {\n",[91,10823,10824,10827],{"class":93,"line":129},[91,10825,10826],{},"    \"target\": \"ES2020\",",[91,10828,10829],{},"          \u002F\u002F 语法降到 ES2020\n",[91,10831,10832,10835],{"class":93,"line":453},[91,10833,10834],{},"    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"]",[91,10836,10837],{},"  \u002F\u002F 允许用这些环境 API\n",[91,10839,10840],{"class":93,"line":459},[91,10841,3112],{},[91,10843,10844],{"class":93,"line":611},[91,10845,456],{},[28,10847,10849,814,10851,10853],{"id":10848},"module-moduleresolution产物用什么模块格式-vs-怎么找模块",[36,10850,10699],{},[36,10852,10702],{},"：产物用什么模块格式 vs 怎么找模块",[15,10855,10856],{},"又是一对常混项：",[204,10858,10859,10888],{},[48,10860,10861,10865,10866,10869,10870,514,10873,10876,10877,10880,10881,10884,10885,10887],{},[19,10862,10863],{},[36,10864,10699],{},"：TS ",[19,10867,10868],{},"输出","的 JS 用哪种模块语法——",[36,10871,10872],{},"ESNext",[36,10874,10875],{},"import\u002Fexport"," 原样保留）、",[36,10878,10879],{},"CommonJS","（转成 ",[36,10882,10883],{},"require","）等。现代项目基本 ",[36,10886,10872],{},"，交给打包器处理。",[48,10889,10890,211,10894,10897,10898,5106,10901,10903,10904,10907,10908,729,10911,10914,10915,10918],{},[19,10891,10892],{},[36,10893,10702],{},[36,10895,10896],{},"import '.\u002Fx'"," 时 TS ",[19,10899,10900],{},"用什么规则去找",[36,10902,1852],{}," 的类型文件——",[36,10905,10906],{},"bundler","（打包器友好）、",[36,10909,10910],{},"node16",[36,10912,10913],{},"nodenext","（按 Node 的 ESM\u002FCJS 规则）、",[36,10916,10917],{},"node","（老经典 Node）。",[15,10920,10921,1685,10923,10925,10926,10928,10929,10932,10933,10935,10936,10938],{},[36,10922,10699],{},[36,10924,10702],{}," 要配套（比如 ",[36,10927,10913],{}," 解析要求 ",[36,10930,10931],{},"module: nodenext","），否则会出现「运行时能 import、类型却找不到」的错位。纯打包器项目用 ",[36,10934,10906],{},"，写 Node 服务端用 ",[36,10937,10913],{},"，是当前两条主流路线。",[28,10940,10942,10944],{"id":10941},"strict-家族一次打开一串安全闸",[36,10943,1786],{}," 家族：一次打开一串安全闸",[15,10946,10947,10950,10951,10954],{},[36,10948,10949],{},"\"strict\": true"," 不是一个开关，而是",[19,10952,10953],{},"一揽子","——它同时打开：",[254,10956,10957,10967],{},[257,10958,10959],{},[260,10960,10961,10964],{},[263,10962,10963],{},"开关",[263,10965,10966],{},"管什么",[270,10968,10969,10982,10994,11004,11017,11027,11037,11052],{},[260,10970,10971,10975],{},[275,10972,10973],{},[36,10974,1772],{},[275,10976,10977,729,10979,10981],{},[36,10978,295],{},[36,10980,1684],{}," 从各类型独立出来（第三章）",[260,10983,10984,10988],{},[275,10985,10986],{},[36,10987,517],{},[275,10989,10990,10991,10993],{},"推断不出来的地方不许悄悄变 ",[36,10992,513],{},"，要你显式写",[260,10995,10996,11001],{},[275,10997,10998],{},[36,10999,11000],{},"strictFunctionTypes",[275,11002,11003],{},"函数参数按逆变检查，函数类型更严格",[260,11005,11006,11011],{},[275,11007,11008],{},[36,11009,11010],{},"strictBindCallApply",[275,11012,11013,11016],{},[36,11014,11015],{},"bind\u002Fcall\u002Fapply"," 的参数类型要正确",[260,11018,11019,11024],{},[275,11020,11021],{},[36,11022,11023],{},"strictPropertyInitialization",[275,11025,11026],{},"类的非可选属性必须在构造函数里初始化",[260,11028,11029,11034],{},[275,11030,11031],{},[36,11032,11033],{},"noImplicitThis",[275,11035,11036],{},"不许出现「this 是 any」的函数",[260,11038,11039,11044],{},[275,11040,11041],{},[36,11042,11043],{},"useUnknownInCatchVariables",[275,11045,11046,11049,11050],{},[36,11047,11048],{},"catch"," 里的错误默认是 ",[36,11051,825],{},[260,11053,11054,11059],{},[275,11055,11056],{},[36,11057,11058],{},"alwaysStrict",[275,11060,11061,11062],{},"每份产物顶部加 ",[36,11063,11064],{},"\"use strict\"",[15,11066,11067,11068,11073,11074,11077,11078,11080,11081,186,11083,11085,11086,74],{},"新项目",[19,11069,11070,11071],{},"必开 ",[36,11072,1786],{},"。老项目从 ",[36,11075,11076],{},"false"," 往 ",[36,11079,1638],{}," 迁会一夜冒出成百上千条错，常见过渡法是先把 ",[36,11082,517],{},[36,11084,1772],{}," 逐个单独打开、逐步修，最后收敛到 ",[36,11087,11088],{},"strict: true",[28,11090,11092,814,11094,814,11096],{"id":11091},"include-exclude-paths",[36,11093,10709],{},[36,11095,10712],{},[36,11097,10715],{},[204,11099,11100,11121],{},[48,11101,11102,11108,11109,5807,11111,11113,11114,186,11117,11120],{},[19,11103,11104,729,11106],{},[36,11105,10709],{},[36,11107,10712],{},"：告诉编译器「哪些文件参与类型检查」。默认包含项目下所有 ",[36,11110,56],{},[36,11112,10712],{}," 里默认已有 ",[36,11115,11116],{},"node_modules",[36,11118,11119],{},"outDir","。范围外的文件即使被 import 也照样解析，但不会被当成「入口」单独检查。",[48,11122,11123,11127,11128,11131,11132,74,11135,11141,11142,11145,11146,11149],{},[19,11124,11125],{},[36,11126,10715],{},"：模块路径别名，把 ",[36,11129,11130],{},"@\u002Futils\u002Ffoo"," 映射到 ",[36,11133,11134],{},"src\u002Futils\u002Ffoo",[19,11136,11137,11138,11140],{},"注意 ",[36,11139,10715],{}," 只影响编译器的类型解析，不影响运行时","——运行时还得靠打包器（Vite 的 ",[36,11143,11144],{},"resolve.alias","）或 Node 的 ",[36,11147,11148],{},"exports"," 真正解析。这是「TS 里能用别名、上线就找不到模块」的经典原因：两边都要配。",[28,11151,11153,11155],{"id":11152},"noemit-与增量构建项目引用",[36,11154,10718],{}," 与增量构建、项目引用",[204,11157,11158,11170,11181],{},[48,11159,11160,11165,11166,11169],{},[19,11161,11162],{},[36,11163,11164],{},"noEmit: true","：只做类型检查、",[19,11167,11168],{},"不产出 JS","。现代项目里这几乎是标配——见下一节。",[48,11171,11172,11180],{},[19,11173,11174,814,11177],{},[36,11175,11176],{},"incremental",[36,11178,11179],{},"tsBuildInfoFile","：把上次检查的缓存写盘，第二次只重查变动文件，加速。",[48,11182,11183,11186,11187,11190],{},[19,11184,11185],{},"项目引用（composite + references）","：把大型仓库拆成多个 ",[36,11188,11189],{},"tsconfig","，各自独立、按依赖增量重建，monorepo 常用。",[28,11192,11194,11195,11197],{"id":11193},"为什么-noemit-打包器才是常见组合","为什么 ",[36,11196,10718],{}," + 打包器才是常见组合",[15,11199,11200,11201,11204],{},"核心答案回到第一章：",[19,11202,11203],{},"类型检查和转译是两件事","。现代工具链把这两件事分工给不同工具：",[204,11206,11207,11216],{},[48,11208,11209,11212,11213,74],{},[19,11210,11211],{},"打包器（Vite\u002Fesbuild\u002FSWC\u002Fwebpack）负责转译","：快，专门把 TS 擦成 JS、把模块打包，但它",[19,11214,11215],{},"不做完整的跨文件类型检查",[48,11217,11218,11224],{},[19,11219,11220,11223],{},[36,11221,11222],{},"tsc --noEmit"," 或 IDE \u002F ESLint 负责类型检查","：慢但准确，专挑类型错误，不负责产生产物。",[15,11226,11227,11228,11230,11231,11233,11234,11237,11238,11240],{},"所以典型配置是 ",[36,11229,11164],{}," 让 ",[36,11232,38],{}," 只当「类型守门员」，转译交给打包器。这也解释了为什么「类型明明报错、",[36,11235,11236],{},"vite dev"," 还照常跑」——因为跑起来的是打包器那份不检查类型的转译。真正把关靠 CI 里那条 ",[36,11239,11222],{},"。第十七章会把这个分工讲得更完整。",[10,11242,11244],{"id":11243},"十七工程实践与常见坑谁转译谁检查以及那份坑清单","十七、工程实践与常见坑：谁转译谁检查，以及那份坑清单",[15,11246,11247,11248,186,11251,74],{},"这一章把前面所有原理收到「工程现场」。先讲清楚 TS 在真实项目里和各种工具怎么分工配合，再列出那些「知道就躲得过、不知道就踩个够」的坑。它们大多源自第一章的两个事实——",[19,11249,11250],{},"类型会被擦除",[19,11252,11253],{},"类型检查与转译互不干涉",[789,11255],{"height":8428,"src":11256,"title":11257},"\u002Ftutorials\u002Fts\u002Fdemos\u002F17-pitfalls.html","示例 17：常见坑清单——每一条都是真实报错样本",[28,11259,11261],{"id":11260},"谁做转译谁做检查vite-vue-react-node-的分工","谁做转译谁做检查：Vite \u002F Vue \u002F React \u002F Node 的分工",[15,11263,11264],{},"不同栈里，「把 TS 变成能跑的 JS」和「检查 TS 类型对不对」这两件事分别由谁干：",[254,11266,11267,11279],{},[257,11268,11269],{},[260,11270,11271,11274,11277],{},[263,11272,11273],{},"场景",[263,11275,11276],{},"转译（擦类型、产出）",[263,11278,52],{},[270,11280,11281,11299,11313,11325,11336,11347],{},[260,11282,11283,11286,11293],{},[275,11284,11285],{},"Vite（纯 TS）",[275,11287,11288,11289,11292],{},"esbuild（极快，",[19,11290,11291],{},"不检查类型","）",[275,11294,11295,11296,11298],{},"需另跑 ",[36,11297,11222],{},"，或靠 IDE",[260,11300,11301,11304,11307],{},[275,11302,11303],{},"Vue + vue-tsc",[275,11305,11306],{},"Vite\u002Fesbuild",[275,11308,11309,11312],{},[36,11310,11311],{},"vue-tsc","（模板里的类型也要查）",[260,11314,11315,11318,11321],{},[275,11316,11317],{},"React + Vite",[275,11319,11320],{},"esbuild \u002F SWC",[275,11322,11323],{},[36,11324,11222],{},[260,11326,11327,11330,11333],{},[275,11328,11329],{},"Node（ts-node \u002F tsx）",[275,11331,11332],{},"ts-node\u002Ftsx 即时擦除",[275,11334,11335],{},"看配置，默认可能不查",[260,11337,11338,11341,11344],{},[275,11339,11340],{},"Node 原生 type stripping",[275,11342,11343],{},"V8 直接抹类型跑",[275,11345,11346],{},"不做检查，纯转译",[260,11348,11349,11354,11359],{},[275,11350,11351,11353],{},[36,11352,38],{}," 直接构建",[275,11355,11356,11358],{},[36,11357,38],{}," 转译 + 检查一起做",[275,11360,11361],{},"就是它",[15,11363,11364,11365,11368,11369,11371,11372,11374,11375,11377],{},"结论只有一句：",[19,11366,11367],{},"跑得快的工具（esbuild\u002FSWC\u002FV8）通常只转译不检查","，所以「本地能跑」不等于「类型没问题」——完整检查必须由 ",[36,11370,11222],{},"（CI 里）或 IDE 提供。这也是 Vue 项目要专门用 ",[36,11373,11311],{}," 的原因：模板里的表达式普通 ",[36,11376,38],{}," 看不到。",[28,11379,11381,11383],{"id":11380},"isolatedmodules-为什么需要",[36,11382,10351],{}," 为什么需要",[15,11385,11386,11387,11390,11391,11230,11394,11396],{},"打包器是",[19,11388,11389],{},"一个文件一个文件独立转译","的（它拿不到全局类型图），因此某些「需要跨文件才知道」的写法它处理不了。",[36,11392,11393],{},"isolatedModules: true",[36,11395,38],{}," 提前拒掉这些写法，保证你的代码「每个文件都能被单独安全转译」。典型要求：",[204,11398,11399,11407,11419],{},[48,11400,11401,11402,729,11404,11406],{},"每个文件都得是真模块（有 ",[36,11403,10260],{},[36,11405,10263],{},"），别写会被当成全局脚本的文件。",[48,11408,11409,11410,11412,11413,3127,11416,74],{},"重导出",[19,11411,10290],{},"要用 ",[36,11414,11415],{},"export type { T }",[36,11417,11418],{},"export { T }",[48,11420,11421,11422,11424,11425,11428],{},"别用 ",[36,11423,1166],{}," 跨文件、别用只有 ",[36,11426,11427],{},"namespace"," 里的运行时值等。",[15,11430,11431,11432,11434],{},"现代框架脚手架几乎都默认开它——它不是让你更严，而是让「",[36,11433,38],{}," 检查通过」和「打包器转译行为」对齐，避免出现「tsc 说没问题、esbuild 转出来不对」的裂缝。",[28,11436,11438],{"id":11437},"eslint-与类型规则","ESLint 与类型规则",[15,11440,11441,11442,11445,11446,11449,11450,11453,11454,11456,11457,11459],{},"ESLint 负责 lint（代码风格、可疑写法、一致性），",[36,11443,11444],{},"@typescript-eslint"," 让它认识 TS 语法。其中一部分规则",[19,11447,11448],{},"需要类型信息","（如「禁止在非 thenable 上 await」「no-misused-promises」），要在 ESLint 配置里提供 ",[36,11451,11452],{},"parserOptions.project"," 指向 ",[36,11455,11189],{},"。分工：",[36,11458,11222],{}," 管「类型对不对」，ESLint 管「写法好不好」，两者互补，不重复。常见踩坑是类型感知规则让 lint 变慢——把重规则集中到 CI，本地跑轻量集是通行做法。",[28,11461,11463,11464,11466],{"id":11462},"坑一any-顺着返回值泄漏","坑一：",[36,11465,513],{}," 顺着返回值泄漏",[15,11468,11469,11471,11472,11475,11476,11478,11479,74,11481,11483,11484,11487,11488,11491,11492,11494,11495,11497,11498,11500],{},[36,11470,513],{}," 不只影响你标它的那一处，它会",[19,11473,11474],{},"沿着数据流传染","：一个返回 ",[36,11477,513],{}," 的调用，你把结果再传给别的运算，那条链上的类型全塌成 ",[36,11480,513],{},[36,11482,618],{},"、部分老库、",[36,11485,11486],{},"(e: any)"," 事件都是源头。对策：在函数边界加返回注解（",[36,11489,11490],{},"const u: User = JSON.parse(s) as User"," 至少标明你要什么），开 ",[36,11493,517],{}," 拦住隐式漏点，遇到 ",[36,11496,513],{}," 尽量转成 ",[36,11499,825],{}," 再收窄（第三章）。",[28,11502,11504],{"id":11503},"坑二结构化类型导致的假兼容","坑二：结构化类型导致的「假兼容」",[15,11506,11507,11508,11511],{},"第四章讲过 TS 看形状不看名字，副作用就是两个",[19,11509,11510],{},"语义完全不同、形状恰好一致","的类型可以互相赋值，编译器一声不吭：",[82,11513,11515],{"className":84,"code":11514,"language":86,"meta":87,"style":87},"type UserID = string\ntype OrderID = string\nfunction get(u: UserID) {}\nget(orderId)   \u002F\u002F ✓ 编译通过！但语义上是把订单号当用户号传了\n",[36,11516,11517,11528,11539,11555],{"__ignoreMap":87},[91,11518,11519,11521,11524,11526],{"class":93,"line":94},[91,11520,1213],{"class":104},[91,11522,11523],{"class":135}," UserID",[91,11525,119],{"class":104},[91,11527,3798],{"class":115},[91,11529,11530,11532,11535,11537],{"class":93,"line":101},[91,11531,1213],{"class":104},[91,11533,11534],{"class":135}," OrderID",[91,11536,119],{"class":104},[91,11538,3798],{"class":115},[91,11540,11541,11543,11545,11547,11549,11551,11553],{"class":93,"line":129},[91,11542,399],{"class":104},[91,11544,8080],{"class":135},[91,11546,405],{"class":108},[91,11548,8517],{"class":408},[91,11550,112],{"class":104},[91,11552,11523],{"class":135},[91,11554,10190],{"class":108},[91,11556,11557,11559,11562],{"class":93,"line":453},[91,11558,6791],{"class":135},[91,11560,11561],{"class":108},"(orderId)   ",[91,11563,11564],{"class":97},"\u002F\u002F ✓ 编译通过！但语义上是把订单号当用户号传了\n",[15,11566,11567,11568,11570,11571,11574],{},"两者都是 ",[36,11569,728],{},"，TS 不拦。想制造「名义类型」的区分感，给类型加一个独有的品牌字段（branded type）：",[36,11572,11573],{},"type UserID = string & { readonly __brand: 'User' }","。否则别指望 TS 帮你区分「同为 string 的不同含义」。",[28,11576,11578,11579],{"id":11577},"坑三枚举的运行时成本与-const-enum","坑三：枚举的运行时成本与 ",[36,11580,1166],{},[15,11582,11583,11584,11586,11587,11589,11590,11592,11593,11595,11596,11599,11600,11602,11603,11609,11610,11612,11613,11615,11616,11618],{},"第三章说过 ",[36,11585,1059],{}," 会真实生成对象。普通 ",[36,11588,1059],{}," 的问题是有运行时体积、跨文件在 ",[36,11591,10351],{}," 下受限；",[36,11594,1166],{}," 想省体积（编译期直接把 ",[36,11597,11598],{},"Direction.Up"," 内联成 ",[36,11601,1147],{},"，不生成对象），但",[19,11604,11605,11606,11608],{},"在打包\u002F分包场景 ",[36,11607,1166],{}," 坑更多","：内联要求编译器看到定义，esbuild\u002Fswc 各自独立转译时可能把 ",[36,11611,1166],{}," 当普通 enum 处理，行为不一致。这也是大量团队弃用 ",[36,11614,1059],{},"、改用「",[36,11617,1173],{}," 对象 + 联合字面量」的原因——零运行时、无内联歧义、还能窄化（第九章）。",[28,11620,11622,11623,11625],{"id":11621},"坑四import-type-与循环引用","坑四：",[36,11624,10251],{}," 与循环引用",[15,11627,11628,11629,11631,11632,11635,11636,11638],{},"类型之间会互相引用形成环（A 的类型用到 B，B 又用到 A）。纯类型循环对 ",[36,11630,38],{}," 无害（编译期解析、运行时无物），但一旦有人用值形式 ",[36,11633,11634],{},"import { SomeType }"," 引入了运行时 import，就可能造出真实的模块循环、触发初始化顺序 bug。用 ",[36,11637,10251],{},"（第十五章）把「只是类型」的依赖和「真要在运行时取值」的依赖分开，循环就只存在于编译期、不会带进运行时。",[28,11640,11642],{"id":11641},"坑五过度断言","坑五：过度断言",[15,11644,11645,11646,186,11648,186,11650,11653,11654,11671],{},"第八章那句「断言是写给编译器的情书」在此回收。满屏 ",[36,11647,195],{},[36,11649,198],{},[36,11651,11652],{},"as unknown as","，等于亲手把类型系统关掉却以为还开着——编译器不再报错，但运行时报错照旧。",[19,11655,11656,11657,11659,11660,11662,11663,11665,11666,5807,11668,11670],{},"能用收窄（",[36,11658,4816],{}," 谓词）就别用 ",[36,11661,195],{},"；能用 ",[36,11664,4996],{}," 就别用 ",[36,11667,195],{},[36,11669,198],{}," 只在确实无法表达的非空场景用","。断言是最后手段不是默认工具。",[28,11673,11675,11676,11678,11679,11681],{"id":11674},"坑六把-unknown-用-as-直接吃掉","坑六：把 ",[36,11677,825],{}," 用 ",[36,11680,195],{}," 直接吃掉",[15,11683,11684,11685,11687,11688,11690,11691,729,11693,11695,11696,11699,11700,11702,11703,10339,11705,11707,11708,74],{},"面对 ",[36,11686,825],{},"（外部数据、",[36,11689,11048],{}," 错误），正确姿势是收窄验证（",[36,11692,1223],{},[36,11694,4260],{},"\u002F校验库）。最省事也最危险的做法是 ",[36,11697,11698],{},"const u = err as MyType"," 一步到「我以为的类型」——这等于绕过了 ",[36,11701,825],{}," 特意设置的那道「先验再用」的闸，把不安全又塞回了系统。",[36,11704,825],{},[36,11706,513],{}," 的安全版（第三章），",[19,11709,11421,11710,11712,11713,11715],{},[36,11711,195],{}," 把它退化回 ",[36,11714,513],{}," 的效果",[10,11717,11719],{"id":11718},"十八typescript-与-javascript-的区别","十八、TypeScript 与 JavaScript 的区别",[15,11721,11722,11723,11726],{},"全课收口，回答三个终极问题：",[19,11724,11725],{},"「TS 是 JS 的超集」对不对、TS 会不会让程序变快、哪些 JS 代码原样搬进 TS 会报错。"," 先给一张维度对照大表，再逐条展开。",[789,11728],{"height":11729,"src":11730,"title":11731},680,"\u002Ftutorials\u002Fts\u002Fdemos\u002F18-ts-vs-js.html","示例 18：TS 与 JS 的区别——同一份逻辑，两个世界",[28,11733,11735],{"id":11734},"一张总表维度-javascript-typescript-一句话备注","一张总表：维度 \u002F JavaScript \u002F TypeScript \u002F 一句话备注",[254,11737,11738,11754],{},[257,11739,11740],{},[260,11741,11742,11745,11748,11751],{},[263,11743,11744],{},"维度",[263,11746,11747],{},"JavaScript",[263,11749,11750],{},"TypeScript",[263,11752,11753],{},"一句话备注",[270,11755,11756,11770,11789,11803,11820,11834,11848,11872,11894,11910,11924,11943],{},[260,11757,11758,11761,11764,11767],{},[275,11759,11760],{},"类型系统",[275,11762,11763],{},"动态弱类型，运行时才定",[275,11765,11766],{},"静态强类型，编译期标注+推断",[275,11768,11769],{},"TS 的类型只在编译期存在",[260,11771,11772,11775,11780,11786],{},[275,11773,11774],{},"变量声明",[275,11776,11777],{},[36,11778,11779],{},"let\u002Fconst\u002Fvar",[275,11781,11782,11783,11785],{},"同左，可加 ",[36,11784,345],{}," 注解",[275,11787,11788],{},"注解是「可加」不是「必加」",[260,11790,11791,11794,11797,11800],{},[275,11792,11793],{},"编译与运行",[275,11795,11796],{},"引擎直接执行源码",[275,11798,11799],{},"需转译成 JS 再执行",[275,11801,11802],{},"运行时永远跑的是 JS",[260,11804,11805,11808,11811,11817],{},[275,11806,11807],{},"工具链",[275,11809,11810],{},"引擎\u002F浏览器即可",[275,11812,11813,11814,11816],{},"多一层 ",[36,11815,38],{},"\u002F打包器\u002F类型检查",[275,11818,11819],{},"见第十六、十七章分工",[260,11821,11822,11825,11828,11831],{},[275,11823,11824],{},"错误暴露时机",[275,11826,11827],{},"多在运行时",[275,11829,11830],{},"大量前移到编辑\u002F构建期",[275,11832,11833],{},"TS 的核心价值所在",[260,11835,11836,11839,11842,11845],{},[275,11837,11838],{},"重构安全性",[275,11840,11841],{},"靠人肉+全局搜",[275,11843,11844],{},"类型图兜底，改一处红一片",[275,11846,11847],{},"TS 最被低估的收益",[260,11849,11850,11853,11860,11869],{},[275,11851,11852],{},"文件后缀与模块",[275,11854,11855,729,11857],{},[36,11856,73],{},[36,11858,11859],{},".mjs",[275,11861,11862,729,11864,729,11867],{},[36,11863,56],{},[36,11865,11866],{},".tsx",[36,11868,327],{},[275,11870,11871],{},"ESM 规则一致，多了类型文件",[260,11873,11874,11877,11883,11886],{},[275,11875,11876],{},"类与字段",[275,11878,11879,11880,11882],{},"标准 class、",[36,11881,6331],{}," 私有",[275,11884,11885],{},"加修饰符\u002F抽象\u002F参数属性",[275,11887,11888,11890,11891,11893],{},[36,11889,6119],{}," 是编译期，",[36,11892,6331],{}," 是运行期",[260,11895,11896,11899,11902,11907],{},[275,11897,11898],{},"运行时能力",[275,11900,11901],{},"完整 JS 引擎能力",[275,11903,11904],{},[19,11905,11906],{},"与 JS 完全相同，无增减",[275,11908,11909],{},"类型不带来任何新运行时能力",[260,11911,11912,11915,11918,11921],{},[275,11913,11914],{},"生态与学习成本",[275,11916,11917],{},"上手快、心智负担低",[275,11919,11920],{},"有学习曲线、类型体操的成本",[275,11922,11923],{},"换确定性要付出复杂度",[260,11925,11926,11929,11932,11935],{},[275,11927,11928],{},"可混合性",[275,11930,11931],{},"独立存在",[275,11933,11934],{},"可与 JS 共存、逐步迁移",[275,11936,11937,11939,11940,11942],{},[36,11938,56],{}," 能 import ",[36,11941,73],{},"（配 allowJs）",[260,11944,11945,11948,11951,11954],{},[275,11946,11947],{},"渐进采纳",[275,11949,11950],{},"无此概念",[275,11952,11953],{},"可从「几乎不写类型」起步",[275,11955,11956,11959],{},[36,11957,11958],{},"checkJs"," 关闭时 TS 项目也能纯放 JS",[15,11961,11962],{},"下面逐条把关键的几行讲透。",[28,11964,11966],{"id":11965},"ts-是不是-js-的超集对但只对了一半","TS 是不是 JS 的超集——对，但只对了一半",[15,11968,11969,11970,11973,11974,11976,11977,11980,11981,11984],{},"严格地说，",[19,11971,11972],{},"语法上 TS ⊇ JS","：任何合法 JS 文件改名 ",[36,11975,56],{}," 都能被 TS 解析（JS 是 TS 的子集）。但「超集」这个词让人以为「TS = JS 的全部行为 + 额外的东西」，这有误导性：TS ",[19,11978,11979],{},"不新增任何运行时行为","，它新增的是",[19,11982,11983],{},"编译期的一层检查","。所以更准确的表述是：TS 是「JS + 一套会被擦除的静态类型语法 + 一个会在运行前挑毛病的检查器」。",[15,11986,11987,11988,11991,11992,11994,11995,11997,11998],{},"还有一个反直觉的事实（也是第三个问题的答案）：",[19,11989,11990],{},"并非所有 JS 都能原样塞进 TS 且不报错","。因为 TS 默认开 ",[36,11993,1786],{},"、开 ",[36,11996,517],{},"，纯 JS 里那些「隐式 any、可能为 null 就直接用、参数没传全」的写法，在 TS 里会变成编译错误。也就是说：",[19,11999,12000],{},"JS 是 TS 语法上的子集，但一段典型的裸 JS 放进严格配置的 TS 里常常通不过类型检查。",[28,12002,12004],{"id":12003},"ts-会不会让程序跑得更快不会","TS 会不会让程序跑得更快——不会",[15,12006,12007,12008,12011,12012,12014,12015,12018,12019,12022],{},"程序实际执行的是擦除类型后的 JS，跟直接写 JS 的最终产物基本等价，",[19,12009,12010],{},"运行时性能没有提升","。恰恰相反，多出来的 ",[36,12013,38],{},"\u002F类型检查是",[19,12016,12017],{},"构建期","的开销，让「改代码到能跑」这一环变慢（这也是把转译交给 esbuild、检查交给 tsc 的分工动机）。TS 换来的是「",[19,12020,12021],{},"人","的效率」——少 debug、少回归、重构敢下手——而不是「机器执行」的效率。谁指望加类型让接口变快，那是要失望的。",[28,12024,12026],{"id":12025},"哪些-js-代码原样放进-ts-会报错","哪些 JS 代码原样放进 TS 会报错",[15,12028,12029,12030,12032],{},"把这三类裸 JS 丢进 ",[36,12031,1786],{}," 的 TS，立刻红给你看，最能说明「类型检查改的是编译期，不是运行期」：",[82,12034,12036],{"className":84,"code":12035,"language":86,"meta":87,"style":87},"\u002F\u002F ① 隐式 any：参数没类型，noImplicitAny 直接拦\nfunction add(a, b) { return a + b }        \u002F\u002F ✗ a、b 隐式 any\n\n\u002F\u002F ② 可能为 null 却不判断：strictNullChecks 拦\nfunction greet(u) { return u.name.toUpperCase() }  \u002F\u002F 若 u: { name?: string } 则 name 可能 undefined\n\n\u002F\u002F ③ 给 const 重新赋值 \u002F 用了未声明的变量——这些 JS 运行时也炸，但 TS 提前在编辑期就标红\nconst x = 1; x = 2                          \u002F\u002F ✗ 对 const 赋值\ny = 10                                       \u002F\u002F ✗ 找不到 y（除非声明或挂全局）\n",[36,12037,12038,12043,12071,12075,12080,12105,12109,12114,12134],{"__ignoreMap":87},[91,12039,12040],{"class":93,"line":94},[91,12041,12042],{"class":97},"\u002F\u002F ① 隐式 any：参数没类型，noImplicitAny 直接拦\n",[91,12044,12045,12047,12049,12051,12053,12055,12057,12059,12061,12063,12065,12068],{"class":93,"line":101},[91,12046,399],{"class":104},[91,12048,402],{"class":135},[91,12050,405],{"class":108},[91,12052,409],{"class":408},[91,12054,416],{"class":108},[91,12056,419],{"class":408},[91,12058,3697],{"class":108},[91,12060,802],{"class":104},[91,12062,444],{"class":108},[91,12064,447],{"class":104},[91,12066,12067],{"class":108}," b }        ",[91,12069,12070],{"class":97},"\u002F\u002F ✗ a、b 隐式 any\n",[91,12072,12073],{"class":93,"line":129},[91,12074,567],{"emptyLinePlaceholder":566},[91,12076,12077],{"class":93,"line":453},[91,12078,12079],{"class":97},"\u002F\u002F ② 可能为 null 却不判断：strictNullChecks 拦\n",[91,12081,12082,12084,12086,12088,12090,12092,12094,12097,12099,12102],{"class":93,"line":459},[91,12083,399],{"class":104},[91,12085,3393],{"class":135},[91,12087,405],{"class":108},[91,12089,8517],{"class":408},[91,12091,3697],{"class":108},[91,12093,802],{"class":104},[91,12095,12096],{"class":108}," u.name.",[91,12098,142],{"class":135},[91,12100,12101],{"class":108},"() }  ",[91,12103,12104],{"class":97},"\u002F\u002F 若 u: { name?: string } 则 name 可能 undefined\n",[91,12106,12107],{"class":93,"line":611},[91,12108,567],{"emptyLinePlaceholder":566},[91,12110,12111],{"class":93,"line":1494},[91,12112,12113],{"class":97},"\u002F\u002F ③ 给 const 重新赋值 \u002F 用了未声明的变量——这些 JS 运行时也炸，但 TS 提前在编辑期就标红\n",[91,12115,12116,12118,12120,12122,12124,12127,12129,12131],{"class":93,"line":2591},[91,12117,462],{"class":104},[91,12119,1274],{"class":115},[91,12121,119],{"class":104},[91,12123,659],{"class":115},[91,12125,12126],{"class":108},"; x ",[91,12128,476],{"class":104},[91,12130,1111],{"class":115},[91,12132,12133],{"class":97},"                          \u002F\u002F ✗ 对 const 赋值\n",[91,12135,12136,12139,12141,12144],{"class":93,"line":2602},[91,12137,12138],{"class":108},"y ",[91,12140,476],{"class":104},[91,12142,12143],{"class":115}," 10",[91,12145,12146],{"class":97},"                                       \u002F\u002F ✗ 找不到 y（除非声明或挂全局）\n",[15,12148,12149,12150,211,12153,186,12156,186,12158,186,12160,12162,12163,186,12165,729,12167,729,12169,12171,12172,12174],{},"反过来，",[19,12151,12152],{},"这些是「TS 有、JS 完全没有」的纯类型语法",[36,12154,12155],{},": 类型",[36,12157,189],{},[36,12159,1213],{},[36,12161,192],{}," 泛型、",[36,12164,1059],{},[36,12166,195],{},[36,12168,4996],{},[36,12170,198],{}," 断言、",[36,12173,10273],{},"。它们写出来 JS 根本不认，也正是转译时被擦掉的部分。",[28,12176,12177],{"id":12177},"一句话收尾",[15,12179,12180,12181,12184,12185,12188,12189,729,12191,12193],{},"TS 的全部故事，第一章就讲完了：",[19,12182,12183],{},"它把「运行时的不确定」变成「编辑时的红波浪线」","，代价是多一套工具链和一层要理解的类型规则。学 TS 不是学一门新语言，而是学",[19,12186,12187],{},"如何把你的意图，用类型这套语言讲给编译器听","——讲对了，它在运行前就替你把关；讲歪了（滥用 ",[36,12190,513],{},[36,12192,195],{},"），它就把闸放开、假装一切正常，把问题原样还给运行时。理解「类型擦除」和「检查与转译两件事」，你就拿到了后面所有细节的解释力。",[12195,12196,12197],"style",{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":87,"searchDepth":101,"depth":101,"links":12199},[12200,12205,12215,12230,12242,12251,12262,12277,12290,12301,12315,12324,12334,12349,12357,12369,12383,12399],{"id":12,"depth":101,"text":13,"children":12201},[12202,12203,12204],{"id":30,"depth":129,"text":31},{"id":167,"depth":129,"text":168},{"id":244,"depth":129,"text":245},{"id":341,"depth":101,"text":12206,"children":12207},"二、第一个类型错误：注解语法 : T 与类型推断",[12208,12209,12210,12212,12214],{"id":367,"depth":129,"text":367},{"id":525,"depth":129,"text":525},{"id":629,"depth":129,"text":12211},"let 推断成联合类型",{"id":713,"depth":129,"text":12213},"const 的字面量窄化",{"id":796,"depth":129,"text":796},{"id":810,"depth":101,"text":12216,"children":12217},"三、基础类型：string \u002F number \u002F boolean、数组与元组、枚举，以及 any \u002F unknown \u002F never",[12218,12219,12220,12222,12224,12226,12228],{"id":845,"depth":129,"text":845},{"id":888,"depth":129,"text":888},{"id":1055,"depth":129,"text":12221},"枚举：enum 与它的运行时代价",{"id":1241,"depth":129,"text":12223},"any：逃生舱，不是「未知类型」",{"id":1332,"depth":129,"text":12225},"unknown：安全的任意值",{"id":1550,"depth":129,"text":12227},"never：不可能到达",{"id":1679,"depth":129,"text":12229},"null \u002F undefined 与 void",{"id":1810,"depth":101,"text":12231,"children":12232},"四、对象类型与接口：type 与 interface 的差别",[12233,12234,12236,12238,12240,12241],{"id":1827,"depth":129,"text":1828},{"id":1949,"depth":129,"text":12235},"type 与 interface：该怎么选",{"id":2138,"depth":129,"text":12237},"可选属性 ?",{"id":2280,"depth":129,"text":12239},"只读 readonly 与 as const",{"id":2368,"depth":129,"text":2369},{"id":2472,"depth":129,"text":2472},{"id":2641,"depth":101,"text":2642,"children":12243},[12244,12246,12248,12249],{"id":2652,"depth":129,"text":12245},"联合类型 A | B：值要么是 A 要么是 B",{"id":2762,"depth":129,"text":12247},"交叉类型 A & B：同时满足两边",{"id":2871,"depth":129,"text":2872},{"id":3163,"depth":129,"text":12250},"穷尽检查：用 never 兜底",{"id":3357,"depth":101,"text":12252,"children":12253},"六、函数类型：参数、可选、剩余、重载与 this",[12254,12255,12256,12257,12258,12260],{"id":3372,"depth":129,"text":3372},{"id":3507,"depth":129,"text":3507},{"id":3626,"depth":129,"text":3626},{"id":3764,"depth":129,"text":3765},{"id":3900,"depth":129,"text":12259},"箭头函数与 this",{"id":4052,"depth":129,"text":12261},"void 与 undefined 作返回类型的差别",{"id":4092,"depth":101,"text":4093,"children":12263},[12264,12265,12267,12269,12271,12272,12274,12276],{"id":4106,"depth":129,"text":4107},{"id":4215,"depth":129,"text":12266},"typeof 收窄",{"id":4257,"depth":129,"text":12268},"in 收窄",{"id":4386,"depth":129,"text":12270},"instanceof 收窄",{"id":4478,"depth":129,"text":4478},{"id":4648,"depth":129,"text":12273},"类型谓词 x is T：把你的判断经验交给编译器",{"id":4829,"depth":129,"text":12275},"asserts x is T：断言函数",{"id":4967,"depth":129,"text":4967},{"id":4989,"depth":101,"text":12278,"children":12279},"八、断言：as、双重断言、非空断言，以及 satisfies 与 as const",[12280,12282,12284,12286,12288],{"id":5008,"depth":129,"text":12281},"as T：普通断言",{"id":5129,"depth":129,"text":12283},"双重断言 as unknown as T",{"id":5206,"depth":129,"text":12285},"非空断言 !",{"id":5323,"depth":129,"text":12287},"as const：把字面量彻底钉死",{"id":5427,"depth":129,"text":12289},"satisfies：校验形状但不丢窄化",{"id":5587,"depth":101,"text":12291,"children":12292},"九、字面量类型与 as const：把「值」当「类型」用",[12293,12294,12295,12297,12299],{"id":5606,"depth":129,"text":5606},{"id":5775,"depth":129,"text":5775},{"id":5819,"depth":129,"text":12296},"as const 的三种落点",{"id":5918,"depth":129,"text":12298},"把值当类型用：typeof 类型查询",{"id":6013,"depth":129,"text":12300},"const 类型参数（简提）",{"id":6089,"depth":101,"text":6090,"children":12302},[12303,12305,12306,12308,12310,12312,12314],{"id":6113,"depth":129,"text":12304},"public \u002F private \u002F protected",{"id":6334,"depth":129,"text":6335},{"id":6411,"depth":129,"text":12307},"static 与抽象类",{"id":6567,"depth":129,"text":12309},"implements：声明「我满足某个契约」",{"id":6658,"depth":129,"text":12311},"真正的私有：# 字段",{"id":6787,"depth":129,"text":12313},"访问器 get \u002F set",{"id":6880,"depth":129,"text":6881},{"id":6996,"depth":101,"text":12316,"children":12317},"十一、泛型入门：为什么需要泛型而不是 any",[12318,12320,12322,12323],{"id":7012,"depth":129,"text":12319},"同一个例子：先写成 any，再写成泛型",{"id":7303,"depth":129,"text":12321},"泛型约束 extends：给类型变量划范围",{"id":7463,"depth":129,"text":7463},{"id":7570,"depth":129,"text":7571},{"id":7594,"depth":101,"text":12325,"children":12326},"十二、泛型进阶：接口、类、keyof、索引访问与映射类型入门",[12327,12328,12329,12331,12333],{"id":7611,"depth":129,"text":7611},{"id":7877,"depth":129,"text":7877},{"id":8015,"depth":129,"text":12330},"keyof：取一个类型所有键的联合",{"id":8195,"depth":129,"text":12332},"索引访问类型 T[K]",{"id":8282,"depth":129,"text":8283},{"id":8417,"depth":101,"text":8418,"children":12335},[12336,12338,12340,12342,12344,12346,12348],{"id":8433,"depth":129,"text":12337},"Partial\u003CT> \u002F Required\u003CT> \u002F Readonly\u003CT>",{"id":8743,"depth":129,"text":12339},"Pick\u003CT, K> \u002F Omit\u003CT, K>",{"id":8978,"depth":129,"text":12341},"Exclude\u003CT, U> \u002F Extract\u003CT, U> \u002F NonNullable\u003CT>",{"id":9219,"depth":129,"text":12343},"ReturnType \u002F Parameters \u002F ConstructorParameters",{"id":9462,"depth":129,"text":12345},"Awaited\u003CT>：扒开 Promise 的外壳",{"id":9606,"depth":129,"text":12347},"Record\u003CK, V>：键类型到值类型的字典",{"id":9734,"depth":129,"text":9734},{"id":9747,"depth":101,"text":12350,"children":12351},"十四、条件类型与 infer、模板字面量类型：类型体操入门",[12352,12353,12355,12356],{"id":9761,"depth":129,"text":9762},{"id":9883,"depth":129,"text":12354},"infer：在匹配中「捕获」一个类型",{"id":10039,"depth":129,"text":10040},{"id":10234,"depth":129,"text":10234},{"id":10247,"depth":101,"text":12358,"children":12359},"十五、模块与声明文件：ESM、import type、.d.ts 与给无类型库写声明",[12360,12362,12364,12366,12368],{"id":10277,"depth":129,"text":12361},"ESM 与 import type",{"id":10358,"depth":129,"text":12363},".d.ts 是什么",{"id":10458,"depth":129,"text":12365},"declare 与 declare module",{"id":10565,"depth":129,"text":12367},"@types 与包里的 types 字段",{"id":10626,"depth":129,"text":10626},{"id":10680,"depth":101,"text":10681,"children":12370},[12371,12373,12375,12377,12379,12381],{"id":10729,"depth":129,"text":12372},"target \u002F lib：转译产物 vs 允许用的 API",{"id":10848,"depth":129,"text":12374},"module \u002F moduleResolution：产物用什么模块格式 vs 怎么找模块",{"id":10941,"depth":129,"text":12376},"strict 家族：一次打开一串安全闸",{"id":11091,"depth":129,"text":12378},"include \u002F exclude \u002F paths",{"id":11152,"depth":129,"text":12380},"noEmit 与增量构建、项目引用",{"id":11193,"depth":129,"text":12382},"为什么 noEmit + 打包器才是常见组合",{"id":11243,"depth":101,"text":11244,"children":12384},[12385,12386,12388,12389,12391,12392,12394,12396,12397],{"id":11260,"depth":129,"text":11261},{"id":11380,"depth":129,"text":12387},"isolatedModules 为什么需要",{"id":11437,"depth":129,"text":11438},{"id":11462,"depth":129,"text":12390},"坑一：any 顺着返回值泄漏",{"id":11503,"depth":129,"text":11504},{"id":11577,"depth":129,"text":12393},"坑三：枚举的运行时成本与 const enum",{"id":11621,"depth":129,"text":12395},"坑四：import type 与循环引用",{"id":11641,"depth":129,"text":11642},{"id":11674,"depth":129,"text":12398},"坑六：把 unknown 用 as 直接吃掉",{"id":11718,"depth":101,"text":11719,"children":12400},[12401,12402,12403,12404,12405],{"id":11734,"depth":129,"text":11735},{"id":11965,"depth":129,"text":11966},{"id":12003,"depth":129,"text":12004},{"id":12025,"depth":129,"text":12026},{"id":12177,"depth":129,"text":12177},"\u002Fimg\u002Fts-cover.svg","2026-10-04","零基础到精通 TypeScript：基础类型、接口、泛型、工具类型、声明文件与 tsconfig 工程化，18 章循序渐进，末章逐条对比 TS 与 JS 的区别。","md",{},"\u002Ftutorials\u002Fts",{"title":5,"description":12408},{"loc":12411},"tutorials\u002Fts","WOAPYVcLinHKb3sSlr6-5iIQ6PhpS9Kv6DaMKOIjKtQ",[12417,12419,12423,12427,12431,12435,12439,12443,12447,12451,12455,12459,12463,12467,12471,12475,12479,12483,12487,12491,12495,12499,12503,12507,12511,12515,12519,12523,12527,12531,12535,12539,12543,12547,12551,12555,12559,12563,12567,12571,12575,12579,12583,12587,12591,12595,12599,12603,12607,12611,12615,12619,12623,12627,12631,12635,12639,12643,12647,12651,12655,12659,12663,12667,12671,12675,12679,12683,12687,12691,12695,12699,12703,12707,12711,12715,12719,12723,12727,12731,12735,12739,12743,12747,12751,12755,12759,12763,12767,12771,12775,12779,12783,12787,12791,12795,12799,12803,12807,12811,12815,12819,12823,12827,12831,12835,12839,12843,12847,12851,12855,12859,12863,12867,12871,12875,12879,12883,12887,12891],{"id":12411,"title":5,"titles":12418,"content":12408,"level":94},[],{"id":12420,"title":13,"titles":12421,"content":12422,"level":101},"\u002Ftutorials\u002Fts#一typescript-是什么类型检查与转译是两件事",[5],"先纠正一句最流行的口号。人们说「TypeScript 是 JavaScript 的超集」，这话对了一半：语法上它确实是超集——任何合法的 JS 都是合法的 TS，你只是在 JS 之上加了一层可以写的类型标注。但 TS 不是「多了一套新语法能做的事」，运行时它不提供任何 JS 没有的能力。它做的事情只有一件额外的：在你运行代码之前，多问一句「这个类型对得上吗」。",{"id":12424,"title":31,"titles":12425,"content":12426,"level":129},"\u002Ftutorials\u002Fts#tsc-到底干了两件事",[5,13],"命令行里敲 tsc，它其实同时在做两件互相独立的事： 类型检查：读你的 .ts，对照类型规则挑毛病，有问题就打印错误。 转译（emit）：把 .ts 里的类型全部抹掉，再按 target 把新语法降级，输出成一份 .js。 关键认知是：这两件事的结论彼此不影响。 \u002F\u002F 这份代码有类型错误，但它照样会被转译出 .js\nlet n: number = 'hello'   \u002F\u002F tsc 报错：Type 'string' is not assignable to type 'number'\nconsole.log(n.toUpperCase())  \u002F\u002F 运行时 n 其实是字符串，这行反而能跑 tsc 会一边打印那条错误、一边把 .js 生成出来（除非你开了 noEmitOnError）。也就是说类型报错拦不住转译——这跟编译型语言「语法不过就产不出二进制」的直觉完全不同。理解了这点，后面很多「为什么有红波浪线还能跑起来」的困惑就都通了。",{"id":12428,"title":168,"titles":12429,"content":12430,"level":129},"\u002Ftutorials\u002Fts#为什么运行时不需要-typescript",[5,13],"浏览器和 Node 从来不认识 TypeScript。你发出去的、被执行的，永远是类型被删光之后的 JS。类型只活在你写代码和 tsc\u002F构建工具检查的那几分钟里，从不出现在最终的产物中。这条机制有个正式名字：类型擦除（type erasure）——转译时所有 : number、interface、\u003CT>、as、! 都会被原样撕掉，留下的就是等价 JS。 由此推出三条实践后果，值得现在就记住： 类型错了不等于程序出错：tsc 报红，程序可能照跑；tsc 全绿，程序也可能崩——因为类型只保证「标注自洽」，不保证「运行时真实」。 泛型、接口、类型别名在运行时不存在：你不能 if (x is User) 里靠 User 这个 type 判断，type 是编译期的，instanceof 只认 class 这种运行时的东西（第七章展开）。 别指望类型能在运行时保护你：外部数据（接口返回、用户输入）永远是「未经检验」的，类型标注只是你对编译器许的愿，编译器不会替你去核实网络回来的 JSON（第十五、十七章会反复强调）。",{"id":12432,"title":245,"titles":12433,"content":12434,"level":129},"\u002Ftutorials\u002Fts#那-ts-到底图什么",[5,13],"既然运行时无非就是 JS，为什么要多这一层？答案全在写代码的那段时间里： 你得到的 从哪来 自动补全 编译器知道 user. 后面能接什么 提前暴露错误 拼错属性名、少传参数、null 直接 .map，写的时候就红 安全重构 改一个字段名，所有用到它的地方全变红，一个不漏 类型即文档 函数签名就是最准的说明书，不会像注释那样过期 接住第三方库 有 .d.ts 声明的库，你在编辑器里就能看见它的全部 API 一句话：TS 把「运行时才炸的 bug」往前挪到「编辑时就红」。它买的是确定性，不是性能，也不是新能力。 本章没有示例可点，因为这一章讲的是「转译」这个概念本身，而转译发生在构建阶段。从第二章起，凡是「点得动、看得出区别」的主题都会配可运行示例。",{"id":12436,"title":12206,"titles":12437,"content":12438,"level":101},"\u002Ftutorials\u002Fts#二第一个类型错误注解语法-t-与类型推断",[5],"TS 最核心的动作就一个：给一个值「说出它的类型」。写 : T 叫类型注解（annotation），但你会发现——大多数时候你根本不写它，因为 TS 会自己猜，这个猜叫类型推断（inference）。这门语言的体验好不好，八成取决于你会不会跟推断打交道。",{"id":12440,"title":367,"titles":12441,"content":12442,"level":129},"\u002Ftutorials\u002Fts#注解语法长什么样",[5,12206],"类型跟在冒号后面，出现在四个高频位置： let count: number = 0            \u002F\u002F 变量：值前面\nfunction add(a: number, b: number): number {   \u002F\u002F 参数：名字后面；返回值：括号后面\n  return a + b\n}\nconst users: string[] = ['a', 'b']  \u002F\u002F 数组：元素类型后面加 [] 参数必须逐个标注，function add(a, b: number) 里的 a 不会自动继承 b 的类型，它会退回隐式 any（noImplicitAny 开着的严格模式下这里就会报错）。返回值可以不写让编译器推断，但导出的公共函数最好显式写——这样改内部实现时调用方不受影响，接口是一份契约。",{"id":12444,"title":525,"titles":12445,"content":12446,"level":129},"\u002Ftutorials\u002Fts#为什么有推断你还写注解",[5,12206],"因为推断是有方向、有极限的，注解是你对编译器的一次「纠偏」： let x = 'hello'      \u002F\u002F 推断成 string（注意：不是字面量类型，见第五章）\nconst y = 'hello'    \u002F\u002F 推断成 'hello'（字面量窄化，const 更精确）\n\nfunction parse(input: string) {   \u002F\u002F 参数不标就报错，所以必须写\n  return JSON.parse(input)        \u002F\u002F 返回值被推断成 any（JSON.parse 的返回是 any）\n} JSON.parse 返回 any——这就是「any 顺着返回值泄漏」的源头之一（第十七章专门讲）。你在这里给它补一个返回注解 : User，就把不确定性挡在了函数边界内。",{"id":12448,"title":12211,"titles":12449,"content":12450,"level":129},"\u002Ftutorials\u002Fts#let-推断成联合类型",[5,12206],"TS 对 let 有个聪明的默认：如果声明时给了初值、之后又可能改成别的类型，它会推断成一个能容纳所有写入值的联合类型，而不是死盯初值： let id = 1          \u002F\u002F 推断 number；随后 id = 'a' 会报错\nlet flag = true     \u002F\u002F 推断 boolean\n\n\u002F\u002F 但若你压根没给初值，它就退化成 any 或需要注解\nlet name            \u002F\u002F 无初值 + 严格模式：隐式 any，报错\nlet name2: string   \u002F\u002F 补注解即可",{"id":12452,"title":12213,"titles":12453,"content":12454,"level":129},"\u002Ftutorials\u002Fts#const-的字面量窄化",[5,12206],"const 因为不能再改，编译器就敢把它的类型收成那个具体的字面量，而不是宽泛的 string\u002Fnumber： const dir = 'up'      \u002F\u002F 类型是 'up'（字面量类型），不是 string\nlet dir2 = 'up'       \u002F\u002F 类型是 string\nconst num = 42        \u002F\u002F 类型是 42 这条差异在第五章「可辨识联合」和第九章「字面量类型」里会起到关键作用，先埋个印象：const 帮你把类型收得更窄，窄到具体值。",{"id":12456,"title":796,"titles":12457,"content":12458,"level":129},"\u002Ftutorials\u002Fts#函数返回值的推断",[5,12206],"不写返回类型时，TS 看 return 后面到底吐什么来反推。推断通常很准，但有个坑：递归函数不写返回类型可能推错（编译器还没算完函数体就遇到自己调用自己，会要求你显式标注）。公共 API 上老老实实写返回类型，既是自我约束，也是给下游的一份稳定契约。",{"id":12460,"title":12216,"titles":12461,"content":12462,"level":101},"\u002Ftutorials\u002Fts#三基础类型string-number-boolean数组与元组枚举以及-any-unknown-never",[5],"这一章把 TS 里最常被写错、也最常被误解的几种类型过一遍。JS 教程第三章已经讲清楚了 JS 世界里的八种值，所以这里只讲类型系统那一层：TS 怎么描述它们，以及三个特殊类型 any\u002Funknown\u002Fnever 到底各管什么。",{"id":12464,"title":845,"titles":12465,"content":12466,"level":129},"\u002Ftutorials\u002Fts#三种原始类型没有惊喜",[5,12216],"string、number、boolean 就是 JS 的字符串、数字、布尔——注意 TS 的 number 和 JS 一样不区分整数与浮点，没有 int\u002Ffloat\u002Fdouble 这些。别下意识写 let age: int，那是 Java\u002FC# 的手感，TS 里只有 number。bigint 和 symbol 也各有对应类型，用得少。",{"id":12468,"title":888,"titles":12469,"content":12470,"level":129},"\u002Ftutorials\u002Fts#数组与元组",[5,12216],"数组用 T[] 或 Array\u003CT> 写，两种等价，Array\u003CT> 在类型复杂时更好读： let list: number[] = [1, 2, 3]\nlet list2: Array\u003Cnumber> = [1, 2, 3]   \u002F\u002F 完全等价\n\nlet pair: [string, number] = ['id', 1]  \u002F\u002F 元组：位置和类型都固定\npair = [1, 'id']                        \u002F\u002F ✗ 顺序错了也报错 元组（tuple）是「长度和各位置类型都写死」的数组，常用于函数一次返回多个值、或表示定长的键值对。长度可变时还有可选元组元素 [string, number?] 和展开元组 [...T[], string]，用得不多，知道有即可。",{"id":12472,"title":12221,"titles":12473,"content":12474,"level":129},"\u002Ftutorials\u002Fts#枚举enum-与它的运行时代价",[5,12216],"枚举是 TS 里少见的真的会往 JS 里生成代码的特性（第七章会看到别的类型都被擦除，enum 恰恰不会）。它给一组常量起名字： enum Direction { Up = 0, Down = 1, Left = 2, Right = 3 }\nconst d = Direction.Up     \u002F\u002F 0\nDirection[0]               \u002F\u002F 'Up'——反向映射：数字也能查回名字 正因为 enum 会编译成一个真实的对象，它有运行时成本：体积、反查、还有 const enum 那种「编译期直接内联成数字、不留对象」的特例（第十七章会讲 const enum 在打包工具下的一系列坑）。实践中很多团队宁可用「联合字面量 + as const 对象」替代 enum，因为后者零运行时开销、还能享受字面量窄化： const Direction = { Up: 0, Down: 1 } as const\ntype Direction = (typeof Direction)[keyof typeof Direction]   \u002F\u002F 0 | 1",{"id":12476,"title":12223,"titles":12477,"content":12478,"level":129},"\u002Ftutorials\u002Fts#any逃生舱不是未知类型",[5,12216],"any 的意思是「关掉这个值上的所有类型检查」。你想让编译器闭嘴时，写 any 就行——但代价是：从这一刻起，这个值身上发生的一切错误，编译器都不再提醒你。用了 any，你实际上是把这块代码降级回了纯 JS。 let x: any = 'text'\nx = 42\nx.toExponential(2)   \u002F\u002F 合法！any 上什么成员都允许\nx.somethingNotThere  \u002F\u002F 也不报错——这正是危险之处 any 只该用在「真的还不知道、且急着跑起来」的临时场合，以及迁移老 JS 代码的过渡期。它是逃生舱，不该是默认选择。",{"id":12480,"title":12225,"titles":12481,"content":12482,"level":129},"\u002Ftutorials\u002Fts#unknown安全的任意值",[5,12216],"unknown 才是「我不知道这是什么类型」的正确表达。它和 any 的区别，是这一章的核心： any unknown 能赋给别的类型吗 能（any 赋值到哪都不报错，还会传染） 不能（必须先收窄） 能直接调用\u002F取属性吗 能（不检查） 不能（报错，逼你判断） 定位 逃生舱：关掉检查 类型安全的 any let u: unknown = 'hello'\nu.toUpperCase()          \u002F\u002F ✗ 报错：unknown 上什么都不能直接做\nconst s: string = u      \u002F\u002F ✗ 报错：unknown 不能赋给 string\nif (typeof u === 'string') {\n  u.toUpperCase()        \u002F\u002F ✓ 收窄后才能用\n}\nconst n: number = u as number   \u002F\u002F 也能用断言强行说它是 number（自担风险） 任何值都能赋给 unknown，但 unknown 想再往下走得先「验明正身」。TS 4.4 起，catch (err) 里的 err 默认就是 unknown 而不是 any，逼你去判断捕获到的到底是什么——这是把不确定性挡在使用点之前的设计哲学。记住口诀：any 是让编译器别管，unknown 是让编译器管但先别信。",{"id":12484,"title":12227,"titles":12485,"content":12486,"level":129},"\u002Ftutorials\u002Fts#never不可能到达",[5,12216],"never 是「没有任何值」的类型，出现在两种地方： function fail(msg: string): never {    \u002F\u002F 永不正常返回：要么抛错要么死循环\n  throw new Error(msg)\n}\nfunction loop(): never { while (true) {} }\n\ntype Empty = number & string   \u002F\u002F 交集为空 → never never 最重要的用法在第五章——穷尽检查：当联合类型的所有分支都被处理完，剩下的分支应当是 never，一旦哪天加了新分支没处理，编译器就会在这里报错。它是 TS 里少数「用来验证你写全了」的机制。",{"id":12488,"title":12229,"titles":12489,"content":12490,"level":129},"\u002Ftutorials\u002Fts#null-undefined-与-void",[5,12216],"void 表示「没有值」，主要用作函数返回类型，告诉编译器「别指望这里返回东西」： function log(msg: string): void {\n  console.log(msg)     \u002F\u002F 没有 return\n}\nlet v: void = undefined   \u002F\u002F void 类型的变量只能装 undefined（严格模式下） null 和 undefined 在关掉 strictNullChecks 时可以被赋给任何类型，这是老代码里无数 Cannot read property of undefined 的根源。打开 strictNullChecks（属于 strict 家族，第六章、十六章详谈）后，null\u002Fundefined 从各类型里独立出来：string 不再包含 null，你要表达「可能是空」必须显式写 string | null。这是 TS 最有价值的一条开关——它把「这里会不会是 null」从运行时惊喜变成了编译期契约。",{"id":12492,"title":12231,"titles":12493,"content":12494,"level":101},"\u002Ftutorials\u002Fts#四对象类型与接口type-与-interface-的差别",[5],"JS 教程第十章讲了对象在运行时怎么写；这一章讲 TS 怎么给对象「画形状」。核心是两种描述结构的方式 type 和 interface，以及围绕对象的四种修饰：可选、只读、索引签名、函数与对象混合。",{"id":12496,"title":1828,"titles":12497,"content":12498,"level":129},"\u002Ftutorials\u002Fts#先看清结构相同即类型相同",[5,12231],"TS 是结构化类型（structural typing）——名字不重要，形状重要。两个接口哪怕毫不相干、没继承关系，只要字段对得上，一个就能当另一个用： interface Point { x: number; y: number }\nconst p: Point = { x: 1, y: 2, z: 3 }   \u002F\u002F ✗ 报错（多余属性检查，见下）\nfunction use(pt: { x: number }) { \u002F* 只用 x *\u002F }\nuse(p)   \u002F\u002F ✓ 只要 p 至少有 x 就行，不在乎它还有 y 这点和第 17 章「假兼容」的坑直接相关：结构化类型很灵活，但会让你以为「这两个类型不同所以不通用」，实际上编译器只看形状。",{"id":12500,"title":12235,"titles":12501,"content":12502,"level":129},"\u002Ftutorials\u002Fts#type-与-interface该怎么选",[5,12231],"两者大部分场景能互换，差别集中在几条： interface type（类型别名） 扩展方式 extends，可以继承多个 交叉 &，也能拼 同名声明 会自动合并（声明合并） 同名再定义会报错 能表达的范畴 只能描述对象\u002F函数\u002F构造这类「形状」 几乎什么都能：联合、元组、映射类型、条件类型 报错信息 通常更简洁 复杂组合时更易读（保留了名字） 惯例 描述对象契约、公共 API 需要联合\u002F工具\u002F高级类型时用 一句话经验：描述一个对象的形状优先 interface；一旦需要联合、映射、条件这类「类型的运算」，只能用 type。团队里两者都允许即可，不必教条。 type ID = string | number        \u002F\u002F 联合——interface 做不到\ntype Pair = [number, number]     \u002F\u002F 元组——type 更顺手\ntype Flags = { a: boolean } & { b: boolean }   \u002F\u002F 交叉，等价于 {a:boolean;b:boolean}",{"id":12504,"title":12237,"titles":12505,"content":12506,"level":129},"\u002Ftutorials\u002Fts#可选属性",[5,12231],"问号表示「这个键可以不存在」，一旦标了 ?，它的类型就自动变成 T | undefined： interface User { name: string; age?: number }\nconst u: User = { name: '阿蓝' }    \u002F\u002F 合法，age 省略\nu.age                               \u002F\u002F number | undefined\nu.age.toFixed(2)                    \u002F\u002F ✗ 报错：可能是 undefined\nu.age?.toFixed(2)                   \u002F\u002F ✓ 可选链（JS 教程第四章的语法，类型上同样有效）\nif (u.age !== undefined) u.age.toFixed(2)   \u002F\u002F ✓ 收窄后能用",{"id":12508,"title":12239,"titles":12509,"content":12510,"level":129},"\u002Ftutorials\u002Fts#只读-readonly-与-as-const",[5,12231],"readonly 让属性只能初始化时赋值一次，之后再改就报错： interface Config { readonly api: string }\nconst c: Config = { api: '\u002Fv1' }\nc.api = '\u002Fv2'     \u002F\u002F ✗ 只读属性不能改 readonly 是编译期约束，运行时值仍可改（除非 Object.freeze）。第九章会看到 as const 把整个对象所有属性一次性都标成 readonly 且窄化成字面量。",{"id":12512,"title":2369,"titles":12513,"content":12514,"level":129},"\u002Ftutorials\u002Fts#索引签名描述键不固定的对象",[5,12231],"当对象的键你说不全（比如一个「字符串映射到数字」的字典），用索引签名： interface StringNum { [key: string]: number }\nconst map: StringNum = { a: 1, b: 2 }\nmap.c = 3        \u002F\u002F 合法：任意字符串键 → number\nmap.d = 'x'      \u002F\u002F ✗ 值必须是 number\n\n\u002F\u002F 数字索引：数组本质上就是 { [index: number]: T } 的特例 数字键的索引签名比字符串键更严格：[key: number]: T 只约束数字下标访问。",{"id":12516,"title":2472,"titles":12517,"content":12518,"level":129},"\u002Ftutorials\u002Fts#函数类型与可调用对象混合",[5,12231],"一个东西既能被调用、又带属性，用「调用签名 + 属性」混合写法： interface Counter {\n  (start: number): string     \u002F\u002F 调用签名：它是个函数\n  interval: number            \u002F\u002F 同时又带属性\n  reset(): void\n}\nfunction getCounter(): Counter {\n  const c = ((start: number) => String(start)) as Counter\n  c.interval = 10\n  c.reset = () => {}\n  return c\n} 这类写法在描述「函数上挂了些元信息」的库 API 时很常见。函数本身的类型还可以直接写成 type Fn = (a: number) => number，第六章细讲函数类型。",{"id":12520,"title":2642,"titles":12521,"content":12522,"level":101},"\u002Ftutorials\u002Fts#五联合类型交叉类型与可辨识联合",[5],"联合和交叉是 TS 类型系统里最像「代数运算」的两个东西：联合是「或」，交叉是「且」。掌握它们，尤其是联合的收窄玩法，是从「会用 TS」到「用得好 TS」的分水岭。",{"id":12524,"title":12245,"titles":12525,"content":12526,"level":129},"\u002Ftutorials\u002Fts#联合类型-a-b值要么是-a-要么是-b",[5,2642],"type ID = string | number\nfunction render(id: ID) {\n  id.toString()        \u002F\u002F ✓ string 和 number 都有 toString，取交集成员\n  id.length            \u002F\u002F ✗ number 上没有 length，联合里不能直接用某一分支独有的成员\n  id.toUpperCase()     \u002F\u002F ✗ 同理，未收窄前只能用「所有分支都保证」的成员\n} 联合类型在没收窄之前，只允许访问所有分支共有的成员（是各分支成员的交集）。要访问某一分支独有的，得先把它收窄成那个分支——第七章会系统讲收窄，这里先看联合的基本脾气。 给联合赋值时反过来：任何一支的值都能赋进去，id = 'a'、id = 1 都合法；但从联合取用时受限。这就是它的核心张力。",{"id":12528,"title":12247,"titles":12529,"content":12530,"level":129},"\u002Ftutorials\u002Fts#交叉类型-a-b同时满足两边",[5,2642],"type HasName = { name: string }\ntype HasAge = { age: number }\ntype Person = HasName & HasAge     \u002F\u002F { name: string; age: number }\nconst p: Person = { name: '阿蓝', age: 18 } 交叉是把多个形状「叠加」成一个更具体的形状，字段全都要满足。接口之间用 extends、type 之间用 &，效果对对象而言基本一致。当两支有同名不同类型字段时会得到 never，这也是「类型层面的空集」的一个来源。",{"id":12532,"title":2872,"titles":12533,"content":12534,"level":129},"\u002Ftutorials\u002Fts#可辨识联合ts-里最好用的模式",[5,2642],"如果联合的每一支都带一个字面量类型的「标签」字段，TS 就能凭这个标签把分支自动区分开——这叫 discriminated union，是整个 TS 里最被推崇的建模手法： type Shape =\n  | { kind: 'circle'; r: number }\n  | { kind: 'rect'; w: number; h: number }\n  | { kind: 'triangle'; base: number; height: number }\n\nfunction area(s: Shape): number {\n  switch (s.kind) {\n    case 'circle':   return Math.PI * s.r ** 2          \u002F\u002F 这支里 s 收窄成 circle\n    case 'rect':     return s.w * s.h                   \u002F\u002F 只能访问 w\u002Fh\n    case 'triangle': return (s.base * s.height) \u002F 2     \u002F\u002F 只能访问 base\u002Fheight\n  }\n} 关键在于 kind 是字面量类型（'circle' 而非 'string'）。在 switch (s.kind) 的每个 case 里，s 会自动收窄到对应那一支，于是 s.r、s.w 这些专属字段都变得类型安全，不需要任何断言。没有标签字段（kind: string）这一整套就全塌了——这就是第九章「把值当类型用」和第二章 const 窄化真正兑现价值的地方。",{"id":12536,"title":12250,"titles":12537,"content":12538,"level":129},"\u002Ftutorials\u002Fts#穷尽检查用-never-兜底",[5,2642],"上面的 area 还差一步：万一以后给 Shape 加了 case 'polygon' 却忘了在 switch 里处理怎么办？用 never 兜底： function area(s: Shape): number {\n  switch (s.kind) {\n    case 'circle': return Math.PI * s.r ** 2\n    case 'rect': return s.w * s.h\n    case 'triangle': return (s.base * s.height) \u002F 2\n    default:\n      const _exhaustive: never = s   \u002F\u002F 若上面没覆盖全，s 还有剩余类型，赋给 never 就报错\n      return _exhaustive\n  }\n} 处理完全部分支后，default 里的 s 类型应当是 never（所有可能都被 case 吃掉了），赋给一个 never 变量正好成立。一旦你漏了某一支，s 就不是 never 而是那个没处理的具体分支类型，赋值报错——你等于给编译器留了个哨兵，专门盯着「未来加了新分支但你忘了改这里」。业务里凡是 switch 一个可辨识联合，都建议加上这条 never 兜底。",{"id":12540,"title":12252,"titles":12541,"content":12542,"level":101},"\u002Ftutorials\u002Fts#六函数类型参数可选剩余重载与-this",[5],"函数是 TS 里类型信息最密集的地方，也是注解写得最多的一处。JS 教程第六章讲过 JS 函数的行为（默认值、剩余参数、闭包捕获），这里不重复那些运行时语义，只讲类型怎么描述它们，以及几个 TS 特有的点：重载签名、void 与 undefined 作为返回类型的差别。",{"id":12544,"title":3372,"titles":12545,"content":12546,"level":129},"\u002Ftutorials\u002Fts#可选参数与默认值",[5,12252],"参数上加 ? 表示可省略，它的类型随即变成 T | undefined；给了默认值的参数自动是可选的，而且类型从默认值推断，通常不用再标： function greet(name: string, title?: string) {\n  \u002F\u002F title: string | undefined\n  return title ? `${title} ${name}` : name\n}\nfunction greet2(name: string, title = '先生') {   \u002F\u002F title 推断为 string，且可选\n  return `${title} ${name}`\n} 注意一个顺序规则：可选参数和带默认值的参数要排在必选参数后面，否则调用时没法靠位置区分。",{"id":12548,"title":3507,"titles":12549,"content":12550,"level":129},"\u002Ftutorials\u002Fts#剩余参数",[5,12252],"剩余参数在类型上写成一个数组类型，泛型写法更常见： function sum(...nums: number[]): number {\n  return nums.reduce((a, b) => a + b, 0)\n}\nfunction join(sep: string, ...parts: string[]) { \u002F* parts: string[] *\u002F } 元组也能描述「固定前缀 + 可变后缀」的签名：(...args: [string, ...number[]]) => void。",{"id":12552,"title":3626,"titles":12553,"content":12554,"level":129},"\u002Ftutorials\u002Fts#函数类型表达式",[5,12252],"函数本身可以当成一个类型来命名和传递，用 (参数: 类型) => 返回： type Comparator = (a: string, b: string) => number\nfunction sortBy(arr: string[], cmp: Comparator) { \u002F* ... *\u002F }\n\nconst byLen: Comparator = (a, b) => a.length - b.length   \u002F\u002F cmp 的类型已知，a\u002Fb 自动推断 把函数签名抽成 type 别名，是回调型 API（Array.prototype.map 的 callback、事件处理器）反复用到的写法，也让「传进去的函数长什么样」一目了然。",{"id":12556,"title":3765,"titles":12557,"content":12558,"level":129},"\u002Ftutorials\u002Fts#重载多个签名-一个实现",[5,12252],"当一个函数「传不同类型进去、返回不同类型出来」，用重载。语法是先列多个对外签名，再写一个对内的实现签名： function pick(x: string): string\nfunction pick(x: number): number\nfunction pick(x: string | number): string | number {   \u002F\u002F 实现签名不对外暴露\n  return x\n}\npick('a')   \u002F\u002F 返回 string\npick(1)     \u002F\u002F 返回 number 要点：实现签名必须能兼容所有对外签名，且调用方只能看见对外的那几个签名，看不见实现签名。外部调用如果落不进任何对外签名，就报错。重载写起来啰嗦，现代 TS 里很多场景可以用「泛型 + 条件返回类型」（第十一、十四章）替代，更简洁也更不容易出错。",{"id":12560,"title":12259,"titles":12561,"content":12562,"level":129},"\u002Ftutorials\u002Fts#箭头函数与-this",[5,12252],"箭头函数没有自己的 this，它捕获定义时外层的 this。这在类型上表现为：普通函数的 this 默认是 any（严格模式下需要 ThisType 或显式 this: T 参数），而箭头函数的 this 从上下文来，无需注解： const timer = {\n  seconds: 0,\n  start() {\n    setInterval(function () {\n      this.seconds++     \u002F\u002F ✗ 这里 this 是全局\u002Fwindow，不是 timer——经典坑\n    }, 1000)\n    setInterval(() => {\n      this.seconds++     \u002F\u002F ✓ 箭头函数捕获 timer 的 this，正确\n    }, 1000)\n  }\n} 普通函数若要显式约束 this，在参数表最前面加一个 this: T（它不是真参数，调用时不占位）。这是 TS 提供的一个「只给 this 打类型」的特殊语法。",{"id":12564,"title":12261,"titles":12565,"content":12566,"level":129},"\u002Ftutorials\u002Fts#void-与-undefined-作返回类型的差别",[5,12252],"两者细微不同，但会咬人：void 表示「调用方不关心返回值」，TS 对返回 void 的函数很宽容——它可以返回任何东西，反正调用方承诺不读（比如 arr.forEach(() => x())，回调返回什么都行）。而 undefined 作返回类型则严格要求真的只返回 undefined。所以当你想表达「随便返回啥但我不看」时，用 void；想表达「它必须什么都不返回」时，才用 undefined。",{"id":12568,"title":4093,"titles":12569,"content":12570,"level":101},"\u002Ftutorials\u002Fts#七类型收窄编译器怎么一步步相信你的判断",[5],"如果说这门课只能精读一章，就是这一章。收窄（narrowing）是 TS 类型系统的发动机：类型不会自己变精确，是你在代码里做的判断，让编译器在某个分支内把一个宽类型「收窄」成具体的窄类型。第五章的可辨识联合、第三章的 unknown 使用点，全都靠收窄落地。",{"id":12572,"title":4107,"titles":12573,"content":12574,"level":129},"\u002Ftutorials\u002Fts#什么是收窄一个直观例子",[5,4093],"function len(x: string | number) {\n  \u002F\u002F 这里 x 是 string | number，length\u002FtoFixed 都不能直接用\n  if (typeof x === 'string') {\n    return x.length       \u002F\u002F ✓ 这个分支里 x 被收窄成 string\n  }\n  return x.toFixed(2)     \u002F\u002F ✓ 走到 else，x 收窄成 number\n} typeof 这个判断本身在 JS 里就有，TS 只是读懂了它的含义：在 === 'string' 为真的分支里，把 x 的类型从联合里剩下的分支收窄。收窄不改变运行时的值，只改变编译器在这一段里对它的看法。",{"id":12576,"title":12266,"titles":12577,"content":12578,"level":129},"\u002Ftutorials\u002Fts#typeof-收窄",[5,4093],"支持的字面量：\"string\"、\"number\"、\"boolean\"、\"function\"、\"object\"、\"symbol\"、\"undefined\"，以及 \"bigint\"。注意 typeof null === 'object' 这条 JS 老 bug 也影响收窄——判对象为真还要再 x !== null。",{"id":12580,"title":12268,"titles":12581,"content":12582,"level":129},"\u002Ftutorials\u002Fts#in-收窄",[5,4093],"用一个「只有某支才有」的属性名来区分，靠 in： type Fish = { swim: () => void }\ntype Bird = { fly: () => void }\nfunction move(a: Fish | Bird) {\n  if ('fly' in a) a.fly()    \u002F\u002F ✓ 'fly' in a 为真时 a 收窄成 Bird\n  else a.swim()\n} in 收窄的正是那种「没设计标签字段、但各支属性不同」的场合——不过更好的做法通常是回去把它改成可辨识联合。",{"id":12584,"title":12270,"titles":12585,"content":12586,"level":129},"\u002Ftutorials\u002Fts#instanceof-收窄",[5,4093],"对 class 实例，用 instanceof 判断，收窄到对应类。因为 class 是运行时真实存在的，所以这条能用；而纯 interface\u002Ftype 运行时不存在，instanceof 帮不上忙（第七章的这条限制，正是第一章「类型擦除」的直接后果）。 function describe(e: Error | Date) {\n  if (e instanceof Error) return e.message   \u002F\u002F 收窄成 Error\n  return e.getTime()                          \u002F\u002F 收窄成 Date\n}",{"id":12588,"title":4478,"titles":12589,"content":12590,"level":129},"\u002Ftutorials\u002Fts#真值收窄与相等收窄",[5,4093],"if (x)、x && ...、x === value、x !== value 这些都能收窄。真值收窄会把 0、''、null、undefined、NaN 这些假值从类型里剔除；相等收窄则配合字面量类型精确命中： function f(s?: string) {\n  if (s) {\n    s.toUpperCase()   \u002F\u002F ✓ 排除了 undefined 和空串（string 仍在）\n  }\n}\ntype Status = 'a' | 'b' | 'c'\nfunction g(x: Status) {\n  if (x === 'a') { \u002F* x: 'a' *\u002F }\n  else { \u002F* x: 'b' | 'c' *\u002F }   \u002F\u002F 排除 'a' 后剩两支\n} 真值收窄有个陷阱：0 和 '' 也会被一起剔掉，如果你本意只是「判空」，用 x !== undefined 更精确，别误用 if (x)。",{"id":12592,"title":12273,"titles":12593,"content":12594,"level":129},"\u002Ftutorials\u002Fts#类型谓词-x-is-t把你的判断经验交给编译器",[5,4093],"标准判断不够用时，你写一个返回 boolean 的函数，但用 x is T 告诉编译器「返回真即说明 x 是 T」——这就是类型谓词（type predicate），用户自定义收窄的核心工具： function isString(x: unknown): x is string {\n  return typeof x === 'string'\n}\nfunction use(v: unknown) {\n  if (isString(v)) {\n    v.toUpperCase()   \u002F\u002F ✓ 因为 isString 声明了 x is string\n  }\n}\n\n\u002F\u002F 配合 filter 极其好用：过滤后的数组类型会自动窄化\nconst nums = ['a', 1, 'b', 2].filter(isString)   \u002F\u002F nums: string[] is 前面必须是函数参数本身（x is T），TS 只信任「对这个变量做的判断」。这个写法把「我确信它是这个类型」的经验，安全地注入编译器，而不必用 as 硬断言。",{"id":12596,"title":12275,"titles":12597,"content":12598,"level":129},"\u002Ftutorials\u002Fts#asserts-x-is-t断言函数",[5,4093],"类型谓词的「加强版」——它不返回布尔，而是不满足就抛错，从而在调用点之后把变量收窄。适合做参数校验： function assertIsNumber(v: unknown): asserts v is number {\n  if (typeof v !== 'number') throw new Error('not a number')\n}\nfunction compute(v: unknown) {\n  assertIsNumber(v)\n  v.toFixed(2)   \u002F\u002F ✓ 调用之后，编译器认定 v 是 number\n} 断言函数对调用方是「隐形收窄」：它没有返回值可用，副作用是改变了后续代码里变量的类型。校验外部输入时特别顺手。",{"id":12600,"title":4967,"titles":12601,"content":12602,"level":129},"\u002Ftutorials\u002Fts#收窄不是运行时保护",[5,4093],"最后再敲一次黑板：收窄只影响编译期看法。typeof x === 'string' 这个分支运行时确实会执行对应 JS，但那是 JS 的判断在起作用，不是类型系统。外部数据你没法靠类型「声明」它是 string——必须靠 typeof\u002Fin\u002F校验库在运行时真的验一遍（第十五、十七章）。收窄是「你在代码里做出判断，编译器据此更新它的看法」这个协作过程，两半缺一不可。",{"id":12604,"title":12278,"titles":12605,"content":12606,"level":101},"\u002Ftutorials\u002Fts#八断言as双重断言非空断言以及-satisfies-与-as-const",[5],"断言是 TS 里最强大也最容易被滥用的一组工具。它的本质一句话：你对编译器说「相信我，这个值其实是某个类型」，编译器照办，且不再检查——出了错你自己担。本章把四种断言工具各自的分工讲清楚。",{"id":12608,"title":12281,"titles":12609,"content":12610,"level":129},"\u002Ftutorials\u002Fts#as-t普通断言",[5,12278],"const el = document.getElementById('app') as HTMLElement\nconst input = el.querySelector('#name') as HTMLInputElement\nconst data = response.body as User as 用来在「编译器知道的类型」和「你知道的实际类型」之间搭桥，常见于 DOM 查询（querySelector 默认返回宽泛的 Element）、以及类型断言从 unknown 收窄。但 as 不做运行时转换，x as number 不会把字符串转成数字，它只是改编译器看法。 这里引用本课程反复强调的一句话，正好给断言下个定义：断言是你写给编译器的情书——你告诉它该怎么想，它全盘接受，不做任何核实。 情书写得越满，运行时被打脸越疼。所以能用收窄（is 谓词）解决的地方，就别用 as。",{"id":12612,"title":12283,"titles":12613,"content":12614,"level":129},"\u002Ftutorials\u002Fts#双重断言-as-unknown-as-t",[5,12278],"直接从 A 断言到八竿子打不着的 B 会被拒（「两种类型重叠不足」），此时先绕到 unknown 再断言就通得了： const el = document as unknown as HTMLElement   \u002F\u002F 直断报错，借 unknown 中转\nconst v = someValue as unknown as TargetType 双重断言是「我最清楚，编译器你别拦」的终极表达，几乎放弃了所有安全检查。它合理的时候很少——比如把 DOM 元素硬当某个自定义类型、老库类型写错时——但每次用都该停下来想想：是不是该用校验代替。",{"id":12616,"title":12285,"titles":12617,"content":12618,"level":129},"\u002Ftutorials\u002Fts#非空断言",[5,12278],"后缀 ! 告诉编译器「这个值这里一定不是 null\u002Fundefined」，把 T | undefined 收成 T： function f(s?: string) {\n  const len = s!.length    \u002F\u002F 你保证 s 一定有值；真给了 undefined 就运行时崩\n}\nconst el = document.getElementById('root')!    \u002F\u002F 断言这个 id 一定存在 ! 是「我检查过了，编译器别念叨」的速记。风险显而易见：如果你没真检查过，s!.length 就是 Cannot read properties of undefined。它是 strictNullChecks 下的止痛药，不该变成主食——能用可选链 ?.、能用收窄分支解决的，都别用 !。",{"id":12620,"title":12287,"titles":12621,"content":12622,"level":129},"\u002Ftutorials\u002Fts#as-const把字面量彻底钉死",[5,12278],"as const 不是断言类型，而是断言「别再宽化了」：加在字面量后面，所有属性变 readonly，所有值收成最具体的字面量类型： const a = { dir: 'up' }         \u002F\u002F dir: string\nconst b = { dir: 'up' } as const \u002F\u002F dir: 'up'（且整个对象只读）\n\nconst dirs = ['up', 'down'] as const   \u002F\u002F readonly ['up', 'down']，可当联合 'up'|'down' 用 第九章会专门讲它解决「字面量宽化」的价值，这里先记住：as const 帮你把「值」冻成「类型」。",{"id":12624,"title":12289,"titles":12625,"content":12626,"level":129},"\u002Ftutorials\u002Fts#satisfies校验形状但不丢窄化",[5,12278],"TS 4.9 新增的 satisfies 解决一个 as 解决不好的两难。你想让一个值符合某类型、又不想因此丢掉它精确的字面量推断： type Route = string | (() => string)\nconst routes = {\n  home: '\u002F',\n  about: () => '\u002Fabout',\n} satisfies Record\u003Cstring, Route>   \u002F\u002F 校验：值必须是 Route\n\nroutes.home     \u002F\u002F 类型是 '\u002F'（字面量保住），而不是被 route 的注解宽化\nroutes.about()  \u002F\u002F 能调用，且知道存在 对比 const routes: Record\u003Cstring, Route> = {...}——那样 routes.home 会变成宽泛的 string | (() => string)，丢掉「它其实就是 '\u002F'」的信息。satisfies 的分工就是：做「类型检查」但保留「更精确的推断结果」，还能在使用点发现多余\u002F缺失属性。三者记住一句话——as 改编译器看法、as const 钉死字面量、satisfies 校验又不丢精度。",{"id":12628,"title":12291,"titles":12629,"content":12630,"level":101},"\u002Ftutorials\u002Fts#九字面量类型与-as-const把值当类型用",[5],"这一章解释一件很多人后知后觉的事：在 TS 里，一个具体的值本身就是一种类型。'up' 不只是字符串的一个实例，它还是一个「只可能是 up 这个字符串」的类型。理解这点，第五、七章的字面量窄化、switch 穷尽才真正说得通。",{"id":12632,"title":5606,"titles":12633,"content":12634,"level":129},"\u002Ftutorials\u002Fts#字面量类型家族",[5,12291],"字符串、数字、布尔、null、undefined 都能各自成为「只包含这一个值」的字面量类型： let a: 'hello' = 'hello'     \u002F\u002F 只能是 hello\nlet n: 42 = 42               \u002F\u002F 只能是 42\nlet t: true = true           \u002F\u002F 只能是 true（注意不是 boolean） 它们的价值全在联合里：把一堆允许值并成一个联合类型，等于给参数列了白名单： type Dir = 'up' | 'down' | 'left' | 'right'\nfunction move(d: Dir) { \u002F* 只认这四个 *\u002F }\nmove('up')     \u002F\u002F ✓\nmove('diagonal')   \u002F\u002F ✗ 不在白名单里，直接报错 这比用 string 宽泛接参、运行时再 if 判断安全太多——白名单错误在写的时候就暴露了。",{"id":12636,"title":5775,"titles":12637,"content":12638,"level":129},"\u002Ftutorials\u002Fts#字面量宽化问题",[5,12291],"TS 默认会把字面量「往宽里推」：let x = 'up' 里的 x 被推断成 string 而不是 'up'，因为 let 意味着以后可能改成别的字符串。这个「从具体值放宽到所属基本类型」就叫宽化（widening）。多数时候它合理，但当你想要那个具体类型时就成了障碍。 三条把类型「钉在字面量」的手段：显式注解 const d: Dir = 'up'；const 声明（第二章讲过的窄化）；as const。",{"id":12640,"title":12296,"titles":12641,"content":12642,"level":129},"\u002Ftutorials\u002Fts#as-const-的三种落点",[5,12291],"const s = 'hi' as const          \u002F\u002F 'hi'\nconst arr = [1, 2, 3] as const   \u002F\u002F readonly [1, 2, 3]（元组 + 只读）\nconst obj = { x: 1, y: 'a' } as const  \u002F\u002F { readonly x: 1; readonly y: 'a' } 对象用了 as const，所有属性都变 readonly 且值收成字面量；数组变成只读元组。它常和 satisfies、可辨识联合、枚举替代方案搭配——第五章那个「联合字面量替代 enum」的写法就靠 as const。",{"id":12644,"title":12298,"titles":12645,"content":12646,"level":129},"\u002Ftutorials\u002Fts#把值当类型用typeof-类型查询",[5,12291],"typeof x 出现在类型位置时，是「取 x 的类型」，配合索引访问能从一组常量里提炼出联合类型： const CONFIG = { api: '\u002Fv1', retry: 3 } as const\ntype ConfigKey = keyof typeof CONFIG          \u002F\u002F 'api' | 'retry'\ntype RetryVal = (typeof CONFIG)['retry']      \u002F\u002F 3 这是「值」和「类型」两个世界之间的一扇后门：常量对象一处维护，它的键、它的值类型都能反查出来，不用手写两遍。",{"id":12648,"title":12300,"titles":12649,"content":12650,"level":129},"\u002Ftutorials\u002Fts#const-类型参数简提",[5,12291],"TS 5.0 引入 \u003Cconst T>，让泛型函数在接收实参时自动对它做 as const 式窄化，免去调用方手打： function keep\u003Cconst T>(x: T): T { return x }\nkeep({ a: 1 })   \u002F\u002F 推断为 { readonly a: 1 }，而不是 { a: number } 日常用得不多，知道它存在、能理解库作者为什么有时用它即可，不深入。",{"id":12652,"title":6090,"titles":12653,"content":12654,"level":101},"\u002Ftutorials\u002Fts#十类修饰符参数属性抽象与真正的私有",[5],"JS 教程第十一章讲过 class 的运行时行为（extends、super、方法、静态成员）。这一章只看 TS 在类上加了哪些类型层面的东西：可见性修饰符、参数属性、implements、抽象类，以及「类型的类」和「实例的类」这一组容易混的概念。",{"id":12656,"title":12304,"titles":12657,"content":12658,"level":129},"\u002Ftutorials\u002Fts#public-private-protected",[5,6090],"TS 给成员加了三种可见性，只在编译期生效——擦除后 JS 里根本没有私有： class Account {\n  public owner: string      \u002F\u002F public 可省略，默认就是\n  private balance: number   \u002F\u002F 只能在类内部访问\n  protected level: number   \u002F\u002F 类内部 + 子类可访问\n\n  constructor(owner: string) {\n    this.owner = owner\n    this.balance = 0\n    this.level = 1\n  }\n  deposit(n: number) { this.balance += n }\n  getBalance() { return this.balance }\n}\nnew Account('a').balance   \u002F\u002F ✗ 报错：private 不能外部访问 要认清：private 挡的是编译器，不是铁了心的运行时攻击——account['balance'] 用字符串索引能绕开（TS 层面也会红，但值确实读得到）。它是「契约」不是「保险箱」。真要运行时私有见下面的 #。",{"id":12660,"title":6335,"titles":12661,"content":12662,"level":129},"\u002Ftutorials\u002Fts#参数属性构造函数的样板消除",[5,6090],"在 constructor 参数前面直接加修饰符和 readonly，TS 会自动帮你声明字段并赋值： class Point {\n  constructor(\n    public readonly x: number,\n    public readonly y: number\n  ) {}          \u002F\u002F 不用手写 this.x = x 了\n} 这一条能把「声明字段 + 构造函数赋值」的重复代码砍掉大半，日常写数据类时高频使用。注意它是 TS 独有语法，擦除后仍会还原成 this.x = x 的普通赋值。",{"id":12664,"title":12307,"titles":12665,"content":12666,"level":129},"\u002Ftutorials\u002Fts#static-与抽象类",[5,6090],"static 是 JS 就有的（挂在类本身而非实例），TS 只是给静态成员也加上类型注解。abstract 是纯 TS 的：抽象类不能直接 new，它的抽象方法没有实现、必须由子类补全，是「强制子类实现某些方法」的手段： abstract class Shape {\n  abstract area(): number         \u002F\u002F 只声明不实现\n  describe() { return `area=${this.area()}` }   \u002F\u002F 可以有普通实现\n}\nclass Square extends Shape {\n  constructor(private side: number) { super() }\n  area() { return this.side ** 2 }   \u002F\u002F 必须实现，否则报错\n}\nnew Shape()   \u002F\u002F ✗ 抽象类不能实例化",{"id":12668,"title":12309,"titles":12669,"content":12670,"level":129},"\u002Ftutorials\u002Fts#implements声明我满足某个契约",[5,6090],"implements 让类显式承诺它符合某个接口。它只检查「类有没有接口的成员」，不改变类型本身： interface Drivable { steer(angle: number): void }\nclass Car implements Drivable {\n  steer(angle: number) { \u002F* ... *\u002F }   \u002F\u002F 漏了 steer 就编译报错\n} 一个常见误解：implements 不是运行时校验，也不像 Java 那样参与多态判定——它只是编译期的一张检查清单。忘了实现某方法时报错，仅此而已。",{"id":12672,"title":12311,"titles":12673,"content":12674,"level":129},"\u002Ftutorials\u002Fts#真正的私有-字段",[5,6090],"ES 标准的私有字段 #name 才是运行时真正私有的，外部任何方式都摸不到： class Vault {\n  #secret = 'abc'              \u002F\u002F 运行时私有，编译后是真私有\n  private hint = 'x'           \u002F\u002F 仅编译期私有\n  reveal() { return this.#secret }\n}\nnew Vault().#secret            \u002F\u002F ✗ 语法直接报错\n(new Vault() as any).secret    \u002F\u002F 也拿不到，运行时确实没有这个公开属性 # 与 private 别混用概念：private 是 TS 的类型层约束（会被擦除），# 是 JS 的运行层约束（会保留）。老打包目标不支持 # 时 TS 会把它降级成 WeakMap。",{"id":12676,"title":12313,"titles":12677,"content":12678,"level":129},"\u002Ftutorials\u002Fts#访问器-get-set",[5,6090],"访问器和 JS 一样，TS 只是给读写分别加类型： class Temp {\n  #c = 0\n  get celsius() { return this.#c }\n  set celsius(v: number) { this.#c = v }\n} 只写 get 不写 set 会读得到、写不进（严格模式下赋值报错），等价于一个「计算得出的只读属性」。",{"id":12680,"title":6881,"titles":12681,"content":12682,"level":129},"\u002Ftutorials\u002Fts#类的类型-vs-实例的类型",[5,6090],"一个 class 名字在 TS 里同时是两个类型：作为「实例的形状」和作为「构造器本身」。区分它们是理解 new 和泛型约束的关键： class User { name = '' }\nlet u: User = new User()          \u002F\u002F User = 实例类型\nlet C: typeof User = User         \u002F\u002F typeof User = 构造器类型（能 new 的那个）\nlet C2: new () => User = User     \u002F\u002F 也可以直接写构造签名 User 指实例的形状，typeof User 指「这个类本身」。写「接收一个类、内部 new 它」的泛型函数时（第十一章），这个区别会立刻用上。",{"id":12684,"title":12316,"titles":12685,"content":12686,"level":101},"\u002Ftutorials\u002Fts#十一泛型入门为什么需要泛型而不是-any",[5],"泛型是 TS 的灵魂。这一章先建立直觉：泛型是「类型的参数」——函数接收值作为参数，泛型函数额外接收「类型」作为参数，让返回类型能跟着输入类型的形状变化。先看不用泛型、用 any 的失败，再看泛型怎么救场。",{"id":12688,"title":12319,"titles":12689,"content":12690,"level":129},"\u002Ftutorials\u002Fts#同一个例子先写成-any再写成泛型",[5,12316],"需求：写一个函数，返回数组的第一个元素。用 any： function firstAny(arr: any[]): any {\n  return arr[0]\n}\nconst x = firstAny([1, 2, 3])   \u002F\u002F x: any —— 丢了元素类型！\nx.toFixed(2)                     \u002F\u002F 不报错，但可能运行时崩\nfirstAny(['a']).toUpperCase()    \u002F\u002F 也不能安全地知道是 string any 版本致命缺陷：进去的时候你知道是 number[]，出来的时候啥都不知道了——信息在 any 处漏光了。现在用泛型： function first\u003CT>(arr: T[]): T {\n  return arr[0]\n}\nconst a = first([1, 2, 3])   \u002F\u002F T 被推断成 number，a: number\nconst b = first(['x', 'y'])  \u002F\u002F T 推断成 string，b: string\na.toFixed(2)                  \u002F\u002F ✓ 知道是 number\nb.toUpperCase()               \u002F\u002F ✓ 知道是 string \u003CT> 是一个「类型变量」：调用时 TS 看实参 number[]，把 T 填成 number，于是返回值也就是 number。类型进了函数又活着出来了——这就是泛型相对 any 的全部价值：既复用代码，又不牺牲信息。",{"id":12692,"title":12321,"titles":12693,"content":12694,"level":129},"\u002Ftutorials\u002Fts#泛型约束-extends给类型变量划范围",[5,12316],"\u003CT> 天真的地方是「T 上什么成员都不能访问」（因为编译器不知道 T 长啥样）。想访问某成员，就得用 extends 约束它至少具备什么： function longest\u003CT extends { length: number }>(a: T, b: T): T {\n  return a.length >= b.length ? a : b    \u002F\u002F ✓ 约束保证 a\u002Fb 一定有 length\n}\nlongest('abc', 'a')        \u002F\u002F string 有 length，通过\nlongest([1, 2], [])        \u002F\u002F 数组也有 length，通过\nlongest(1, 2)              \u002F\u002F ✗ number 没有 length，被约束挡住 T extends U 读作「T 至少是 U 的形状」。约束既让函数内部能安全访问成员，又在调用点挡住了不合规格的实参——这是泛型「有边界」的两面。",{"id":12696,"title":7463,"titles":12697,"content":12698,"level":129},"\u002Ftutorials\u002Fts#默认类型参数",[5,12316],"\u003CT = 某类型> 给类型变量一个默认，调用时不显式传就用默认： function box\u003CT = string>(v: T): { value: T } { return { value: v } }\nbox('a')       \u002F\u002F T = string\nbox(42)        \u002F\u002F T = number（实参优先于默认）\nbox\u003Cstring>(42)   \u002F\u002F 显式指定 T = string，此时 42 反而报错 默认类型参数常用于「给个合理缺省、但允许覆盖」的通用容器和工具函数。",{"id":12700,"title":7571,"titles":12701,"content":12702,"level":129},"\u002Ftutorials\u002Fts#什么时候用泛型什么时候别",[5,12316],"经验法则：当「输入类型」和「输出类型」之间存在形状依赖时，用泛型。像 first、map、identity、Record\u003CK,V> 这种「你给啥类型、我按同样形状返回」的，泛型最合适。如果函数的返回类型和入参类型毫无关系，或者你只想要「多种固定情况之一」，用联合类型（第五章）而不是泛型——别为了看起来高级而把简单联合写成复杂泛型，可读性会付代价。",{"id":12704,"title":12325,"titles":12705,"content":12706,"level":101},"\u002Ftutorials\u002Fts#十二泛型进阶接口类keyof索引访问与映射类型入门",[5],"泛型不只活在函数上。这一章把泛型铺到接口、类型别名、类上，并介绍三件「类型的运算」工具：keyof、索引访问 T[K]、以及映射类型的雏形——它们是下一章工具类型的地基。",{"id":12708,"title":7611,"titles":12709,"content":12710,"level":129},"\u002Ftutorials\u002Fts#泛型接口与泛型类型别名",[5,12325],"把类型变量放进接口\u002F别名的尖括号，就得到「可参数化的形状」： interface Box\u003CT> { value: T }\ninterface Pair\u003CA, B> { first: A; second: B }\n\nlet b: Box\u003Cnumber> = { value: 1 }\nlet p: Pair\u003Cstring, boolean> = { first: 'a', second: true }\n\ntype Result\u003CT> = { ok: true; data: T } | { ok: false; error: string }\nfunction unwrap\u003CT>(r: Result\u003CT>): T | undefined {\n  return r.ok ? r.data : undefined\n} 泛型接口是 API 建模的主力：Result\u003CT>、Promise\u003CT>、Array\u003CT>、React 的 Props\u003CT> 全是它。",{"id":12712,"title":7877,"titles":12713,"content":12714,"level":129},"\u002Ftutorials\u002Fts#泛型类",[5,12325],"类同样能带类型参数，实例化时确定： class Stack\u003CT> {\n  private items: T[] = []\n  push(v: T) { this.items.push(v) }\n  pop(): T | undefined { return this.items.pop() }\n}\nconst s = new Stack\u003Cnumber>()   \u002F\u002F 只能 push number 上一节 typeof User 那对概念在这里闭环：写一个「接收某个类的实例并返回同类」的泛型方法时，靠 new () => T 这样的构造签名约束，TS 才知道你能 new 它、且 new 出来的就是 T。",{"id":12716,"title":12330,"titles":12717,"content":12718,"level":129},"\u002Ftutorials\u002Fts#keyof取一个类型所有键的联合",[5,12325],"keyof T 返回 T 全部属性名组成的字符串\u002F数字联合： interface User { name: string; age: number }\ntype K = keyof User        \u002F\u002F 'name' | 'age'\n\nfunction get\u003CT, K extends keyof T>(obj: T, key: K): T[K] {\n  return obj[key]\n}\nget({ name: 'a', age: 1 }, 'name')   \u002F\u002F ✓ 返回类型是 string\nget({ name: 'a' }, 'nope')           \u002F\u002F ✗ 'nope' 不在 keyof 里 keyof + extends keyof T 约束，让「按名字安全取属性」成为可能——key 只能是真实存在的键名，返回类型还能精确到那个键的值类型。",{"id":12720,"title":12332,"titles":12721,"content":12722,"level":129},"\u002Ftutorials\u002Fts#索引访问类型-tk",[5,12325],"T[K] 取出「T 里键 K 对应的值类型」，和 keyof 是天造地设的一对： type UserAge = User['age']       \u002F\u002F number\ntype First = [string, number][0] \u002F\u002F string（元组按位置取） 它把「属性名」升级成「类型层面的可计算量」：配合泛型 T[K] 就是上面 get 函数精确返回类型的秘密。工具类型 Pick\u002FRecord\u002FReturnType 全靠这套索引访问搭起来。",{"id":12724,"title":8283,"titles":12725,"content":12726,"level":129},"\u002Ftutorials\u002Fts#映射类型入门用键的联合造新类型",[5,12325],"映射类型是「遍历一个键的联合，逐个生成属性」的语法，一句话就能看出威力： type Mutable\u003CT> = {\n  -readonly [K in keyof T]: T[K]     \u002F\u002F 把每个属性去掉 readonly\n}\ntype Readonly2\u003CT> = {\n  readonly [K in keyof T]: T[K]      \u002F\u002F 给每个属性加 readonly\n} [K in keyof T] 读作「对 T 的每个键 K」，后面是这些键在新类型里的形状。加上修饰符开关 readonly\u002F?（还能用 +\u002F- 增删），你能「把一个类型的所有属性整体变换一遍」。下一章那些内置工具类型，本质全是这里这种映射类型手写的结果——把这里看明白，第十三章就不是背 API 而是懂原理。",{"id":12728,"title":8418,"titles":12729,"content":12730,"level":101},"\u002Ftutorials\u002Fts#十三工具类型逐个拆它们其实都是映射类型",[5],"TS 内置了一堆「拿来就用的类型变换」，统称工具类型。它们让日常业务不用每次手写映射。这一章逐个讲用途，并对每个现场用映射类型手写一遍，让你看清「它不过是前面那些零件的组装」。",{"id":12732,"title":12337,"titles":12733,"content":12734,"level":129},"\u002Ftutorials\u002Fts#partialt-requiredt-readonlyt",[5,8418],"给所有属性加 ? \u002F 去掉 ? \u002F 加 readonly，用于「更新用的部分字段」「把可选补齐」等： interface User { id: number; name: string }\ntype Upd = Partial\u003CUser>          \u002F\u002F { id?: number; name?: string }\nfunction updateUser(u: User, patch: Partial\u003CUser>): User { return { ...u, ...patch } }\n\ntype P = Partial\u003C{ a: number; b: string }>   \u002F\u002F { a?: number; b?: string }\n\n\u002F\u002F 手写一遍：\ntype MyPartial\u003CT> = { [K in keyof T]?: T[K] }          \u002F\u002F [K in keyof T]? 就是给每键加 ?\ntype MyRequired\u003CT> = { [K in keyof T]-?: T[K] }        \u002F\u002F -? 去掉可选\ntype MyReadonly\u003CT> = { readonly [K in keyof T]: T[K] } \u002F\u002F 加 readonly -?、-readonly 里的减号是「移除该修饰符」，与不写符号（默认加）相对。",{"id":12736,"title":12339,"titles":12737,"content":12738,"level":129},"\u002Ftutorials\u002Fts#pickt-k-omitt-k",[5,8418],"Pick 从 T 里挑几个键，Omit 从 T 里去掉几个键： interface Full { id: number; name: string; pwd: string }\ntype Public = Pick\u003CFull, 'id' | 'name'>     \u002F\u002F { id: number; name: string }\ntype NoId = Omit\u003CFull, 'id'>                \u002F\u002F { name: string; pwd: string }\n\ntype MyPick\u003CT, K extends keyof T> = { [P in K]: T[P] }\n\u002F\u002F Omit 依赖第十四章的条件类型 Exclude，这里先给成品：\ntype MyOmit\u003CT, K extends keyof any> = Pick\u003CT, Exclude\u003Ckeyof T, K>> Pick 的映射源是 K（要保留的键），每键的值取自 T[P]——一句话讲完它的原理。Omit 是「先算出去掉 K 后剩下的键，再 Pick」。",{"id":12740,"title":12341,"titles":12741,"content":12742,"level":129},"\u002Ftutorials\u002Fts#excludet-u-extractt-u-nonnullablet",[5,8418],"作用在联合类型上的三个筛选器： type A = Exclude\u003C'a' | 'b' | 'c', 'a'>      \u002F\u002F 'b' | 'c'（从 T 里剔除 assignable to U 的）\ntype B = Extract\u003C'a' | 'b' | number, string> \u002F\u002F 'a' | 'b'（只留 assignable to string 的）\ntype C = NonNullable\u003Cstring | null | undefined> \u002F\u002F string（去掉 null\u002Fundefined）\n\ntype MyExclude\u003CT, U> = T extends U ? never : T    \u002F\u002F 条件类型：命中就丢\ntype MyExtract\u003CT, U> = T extends U ? T : never\ntype MyNonNullable\u003CT> = T extends null | undefined ? never : T extends 用在裸类型参数上会逐个分发到联合的每个成员（第十四章细讲分发条件类型）——所以 Exclude 能对联合每一支分别判断。这三个是组合其它工具类型的常用零件。",{"id":12744,"title":12343,"titles":12745,"content":12746,"level":129},"\u002Ftutorials\u002Fts#returntype-parameters-constructorparameters",[5,8418],"从一个函数类型里「抠」出返回类型 \u002F 参数元组： function make(a: number, b: string): { id: number } { return { id: a } }\ntype R = ReturnType\u003Ctypeof make>       \u002F\u002F { id: number }\ntype P = Parameters\u003Ctypeof make>       \u002F\u002F [number, string]\n\ntype MyReturnType\u003CT extends (...a: any) => any> = T extends (...a: any) => infer R ? R : never\ntype MyParameters\u003CT extends (...a: any) => any> = T extends (...a: infer P) => any ? P : never ReturnType 的写法先用了 infer（第十四章）——「在匹配函数形状时，把返回位置的那段抽出来叫 R」。这里记住结论：这几个工具是「把函数类型当数据来拆零件」。",{"id":12748,"title":12345,"titles":12749,"content":12750,"level":129},"\u002Ftutorials\u002Fts#awaitedt扒开-promise-的外壳",[5,8418],"Awaited\u003CT> 递归地剥掉 Promise\u002Fthenable 外层，拿到最终的值类型，专门配合异步： type V = Awaited\u003CPromise\u003CPromise\u003Cstring>>>   \u002F\u002F string（反复剥到底）\n\ntype MyAwaited\u003CT> =\n  T extends PromiseLike\u003Cinfer U>\n    ? T extends PromiseLike\u003CT> ? never : MyAwaited\u003CU>   \u002F\u002F 递归剥\n    : T 注意它处理的是「类型」不是「值」：给一个 Promise\u003Cnumber> 的类型，得到 number 类型。取实际异步函数返回值的类型时非常顺手。",{"id":12752,"title":12347,"titles":12753,"content":12754,"level":129},"\u002Ftutorials\u002Fts#recordk-v键类型到值类型的字典",[5,8418],"Record\u003CK, V> = 「K 里每个键都映射到 V」，比手写索引签名更能表达「键是有限的几个」： type Pages = Record\u003C'home' | 'about', string>   \u002F\u002F { home: string; about: string }\ntype Dict = Record\u003Cstring, number>              \u002F\u002F 等价于 { [k: string]: number }\n\ntype MyRecord\u003CK extends keyof any, V> = { [P in K]: V } Record 的映射源是 K（键联合），每个键的值统一是 V。它常用于给枚举\u002F联合键做完整的映射表，编译器会检查你有没有把每个键都填上。",{"id":12756,"title":9734,"titles":12757,"content":12758,"level":129},"\u002Ftutorials\u002Fts#选用的分寸",[5,8418],"工具类型好用，但别堆砌。链上七八层 Omit\u003CPick\u003CPartial\u003C...>>> 会让报错信息变成天书、也让同事读不动。先写朴素接口，需要变换时再上工具类型；能用一句话讲清它做了什么，才值得这么写。",{"id":12760,"title":12350,"titles":12761,"content":12762,"level":101},"\u002Ftutorials\u002Fts#十四条件类型与-infer模板字面量类型类型体操入门",[5],"这一章进入 TS 的「类型也能做运算」的深水区。日常业务里，你大概率不需要写这么深——绝大多数页面代码停在第十三章就绰绰有余。但读懂库作者写的类型、以及享受工具类型带来的推断，需要这里的知识。只讲够用的几个概念，配两三个例子，不追求把体操做到极限。",{"id":12764,"title":9762,"titles":12765,"content":12766,"level":129},"\u002Ftutorials\u002Fts#条件类型类型层面的三元",[5,12350],"T extends U ? X : Y 是「如果 T 能赋给 U 就取 X 否则取 Y」，一个写在类型位置的三元表达式： type IsNumber\u003CT> = T extends number ? true : false\ntype A = IsNumber\u003C42>      \u002F\u002F true\ntype B = IsNumber\u003C'x'>     \u002F\u002F false 关键特性是分发（distributive）：当 T 是裸类型参数且是联合，条件类型会对联合的每个成员分别计算再并起来。这就是 Exclude 能「逐个筛掉」的原理：('a'|'b'|'c') extends 'a' ? never : T 会拆成对 'a'、'b'、'c' 各自判断，命中 'a' 变 never 被丢，剩下的并回联合。想关掉分发，把 T 包进元组 [T] extends [U] 即可。",{"id":12768,"title":12354,"titles":12769,"content":12770,"level":129},"\u002Ftutorials\u002Fts#infer在匹配中捕获一个类型",[5,12350],"infer R 写在条件类型的「被匹配的形状」里，意思是「这一段随它是什么，抽出来命名叫 R」。前面 ReturnType、Awaited 都靠它： type First\u003CT> = T extends [infer F, ...any[]] ? F : never\ntype F = First\u003C[string, number]>     \u002F\u002F string\n\ntype Flatten\u003CT> = T extends Array\u003Cinfer U> ? U : T\ntype E = Flatten\u003Cnumber[]>           \u002F\u002F number infer 让你从「一个复杂的类型」里按形状「解构」出想要的局部，是类型体操里最常用的捕获手段。",{"id":12772,"title":10040,"titles":12773,"content":12774,"level":129},"\u002Ftutorials\u002Fts#模板字面量类型在类型层面拼字符串",[5,12350],"TS 4.1 起，字符串字面量类型支持模板拼接，用 `${A}` 形式： type World = 'world'\ntype Greeting = `hello ${World}`     \u002F\u002F 'hello world'\n\ntype EventName\u003CT extends string> = `on${Capitalize\u003CT>}`\ntype Click = EventName\u003C'click'>      \u002F\u002F 'onClick'\n\ntype Style = `${number}px` | `${number}%`\nfunction setSize(s: Style) {}\nsetSize('10px')     \u002F\u002F ✓\nsetSize('big')      \u002F\u002F ✗ 不符合模板 模板字面量类型最实用的一面：给「有规律的事件名、路由、CSS 单位」这类字符串建立模式约束，让拼错在编译期就报错。用 Capitalize\u002FLowercase\u002FUppercase 等字符串工具类型（TS 内置）还能做大小写变换。",{"id":12776,"title":10234,"titles":12777,"content":12778,"level":129},"\u002Ftutorials\u002Fts#分寸再强调一遍",[5,12350],"上面这些组合起来能做「类型层面的编程」，理论上能写很复杂的推断。但在业务代码里写它们要极其克制：库的 .d.ts、通用的类型工具值得精雕；一个具体页面的 props 用不着条件类型套娃。读懂 > 会写 > 会炫。本章示例点到为止，能认出「哦这是在用 infer 抽返回类型」就够了。",{"id":12780,"title":12358,"titles":12781,"content":12782,"level":101},"\u002Ftutorials\u002Fts#十五模块与声明文件esmimport-typedts-与给无类型库写声明",[5],"JS 教程第二十章讲过 ESM 的 import\u002Fexport 语法。这一章讲 TS 在模块之上加的东西：import type、被擦除后的类型去哪了、.d.ts 声明文件是什么、怎么用 declare 给一个没有类型的 JS 库「补上」类型，以及遇到「找不到类型声明」时的三条出路。",{"id":12784,"title":12361,"titles":12785,"content":12786,"level":129},"\u002Ftutorials\u002Fts#esm-与-import-type",[5,12358],"TS 里导入分两种：导值（运行时真要有这个东西）和导类型（编译期用完就擦除，运行时根本不需要它存在）。把「只用来做类型」的导入写成 import type，是明确的信号也是优化： import type { User } from '.\u002Ftypes'     \u002F\u002F 纯类型，擦除后这条 import 整个消失\nimport { getUser } from '.\u002Fapi'          \u002F\u002F 值，运行时真的要用 为什么区分？因为擦除。User 是 type，运行时不存在，若写成普通 import { User } 再只在类型位置用它，打包器可能把整模块拽进产物却找不到 User（isolatedModules 下会直接报错，见第十七章）。import type 让「这是类型、别当值处理」一目了然，也让单文件转译（各文件独立擦除）能安全进行。",{"id":12788,"title":12363,"titles":12789,"content":12790,"level":129},"\u002Ftutorials\u002Fts#dts-是什么",[5,12358],".d.ts 是只有类型、没有实现的声明文件。它是库对外发布的「类型说明书」：编译器读它知道 API 长什么样，运行时它一行代码都不产出。你自己的 .ts 编译时会附带产出 .d.ts（declaration: true），第三方库的类型则来自 npm 上的包或 @types\u002F*。 \u002F\u002F some-lib\u002Findex.d.ts —— 只声明，不实现\nexport interface Options { timeout?: number }\nexport function request(url: string, opts?: Options): Promise\u003CResponse>",{"id":12792,"title":12365,"titles":12793,"content":12794,"level":129},"\u002Ftutorials\u002Fts#declare-与-declare-module",[5,12358],"declare 关键字用于「告诉编译器：外面有这个东西，你别生成也别检查，信我」。给一个全局脚本变量、一段内联的 JS 库打类型，就靠它： \u002F\u002F 描述一个挂在 window 上的全局（例如某统计脚本）\ndeclare const _hmt: string[][]\n\n\u002F\u002F 给一个「其实有、但没类型」的模块声明形状\ndeclare module 'legacy-lib' {\n  export function doThing(x: number): void\n} declare module '名字' 是给「JS 库但没带 .d.ts」补类型的标准姿势——你在项目里建一个 .d.ts，用 declare module 声明这个包对外暴露的形状，之后 import ... from 'legacy-lib' 就有类型了。",{"id":12796,"title":12367,"titles":12797,"content":12798,"level":129},"\u002Ftutorials\u002Fts#types-与包里的-types-字段",[5,12358],"一个包怎么被找到类型？两条路： 包自己内置：package.json 里 types \u002F typings 字段指向它的 .d.ts（现代 TS 库普遍这么发布）。 社区补：DefinitelyTyped 维护、发布成 @types\u002F包名（如 @types\u002Flodash、@types\u002Freact），npm i -D @types\u002Fxxx 即可。 编译器解析 import 'lodash' 时，先看包自身 types 字段，没有再找 node_modules\u002F@types\u002Flodash。",{"id":12800,"title":10626,"titles":12801,"content":12802,"level":129},"\u002Ftutorials\u002Fts#找不到类型声明的三种解法",[5,12358],"import 'xxx' 报「找不到模块声明文件」（TS7016 之类）时，别急着 \u002F\u002F @ts-ignore，三条正规出路： 装社区类型：npm i -D @types\u002Fxxx——最常见，多数流行库都有。 自己写 declare module：在项目的 types\u002Fxxx.d.ts 里声明它的形状，哪怕先只写你用到的几个函数。 临时兜底：declare module 'xxx'（不写内容）把它当 any 放行，或开 allowJs 直接吃它的 JS——但这是把类型关掉，属于过渡不是终点。 顺序优先选 1，其次 2；3 只在时间紧迫时短暂使用，并留待补齐。",{"id":12804,"title":10681,"titles":12805,"content":12806,"level":101},"\u002Ftutorials\u002Fts#十六tsconfig-全解编译选项的真实含义",[5],"tsconfig.json 是 TS 项目的控制台。这一章不逐条背，而是把最常配错、影响最大的几组讲透：target\u002Flib\u002Fmodule\u002FmoduleResolution 的真实差别、strict 家族各自开关、include\u002Fexclude\u002Fpaths\u002FnoEmit，以及「noEmit + 打包器」为什么才是现代主流组合。",{"id":12808,"title":12372,"titles":12809,"content":12810,"level":129},"\u002Ftutorials\u002Fts#target-lib转译产物-vs-允许用的-api",[5,10681],"这两个最容易混，分清就赢了一半： target：决定语法降级到哪一档 ES 版本。target: \"es2020\" 意味着 TS 会把更新的语法（比如可选链、逻辑赋值）降级成 ES2020 能跑的 JS。它管的是「语法」。 lib：决定编译器认为环境里有哪些内置 API。用 Promise、Array.prototype.includes、fetch、Map 这些，都要 lib 里有对应声明才不报「找不到」。 关键陷阱：target 不会帮你 polyfill 任何 API。你 target: \"es5\" 但代码里用了 Promise，编译不报错（因为 lib 默认按 target 推），产物里也不会凭空出现 Promise——运行时 Promise is not defined 照崩。lib 是「我信任运行环境有这些东西」的声明，不是「请帮我把缺的补上」。 {\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",          \u002F\u002F 语法降到 ES2020\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"]  \u002F\u002F 允许用这些环境 API\n  }\n}",{"id":12812,"title":12374,"titles":12813,"content":12814,"level":129},"\u002Ftutorials\u002Fts#module-moduleresolution产物用什么模块格式-vs-怎么找模块",[5,10681],"又是一对常混项： module：TS 输出的 JS 用哪种模块语法——ESNext（import\u002Fexport 原样保留）、CommonJS（转成 require）等。现代项目基本 ESNext，交给打包器处理。 moduleResolution：import '.\u002Fx' 时 TS 用什么规则去找 x 的类型文件——bundler（打包器友好）、node16\u002Fnodenext（按 Node 的 ESM\u002FCJS 规则）、node（老经典 Node）。 module 与 moduleResolution 要配套（比如 nodenext 解析要求 module: nodenext），否则会出现「运行时能 import、类型却找不到」的错位。纯打包器项目用 bundler，写 Node 服务端用 nodenext，是当前两条主流路线。",{"id":12816,"title":12376,"titles":12817,"content":12818,"level":129},"\u002Ftutorials\u002Fts#strict-家族一次打开一串安全闸",[5,10681],"\"strict\": true 不是一个开关，而是一揽子——它同时打开： 开关 管什么 strictNullChecks null\u002Fundefined 从各类型独立出来（第三章） noImplicitAny 推断不出来的地方不许悄悄变 any，要你显式写 strictFunctionTypes 函数参数按逆变检查，函数类型更严格 strictBindCallApply bind\u002Fcall\u002Fapply 的参数类型要正确 strictPropertyInitialization 类的非可选属性必须在构造函数里初始化 noImplicitThis 不许出现「this 是 any」的函数 useUnknownInCatchVariables catch 里的错误默认是 unknown alwaysStrict 每份产物顶部加 \"use strict\" 新项目必开 strict。老项目从 false 往 true 迁会一夜冒出成百上千条错，常见过渡法是先把 noImplicitAny、strictNullChecks 逐个单独打开、逐步修，最后收敛到 strict: true。",{"id":12820,"title":12378,"titles":12821,"content":12822,"level":129},"\u002Ftutorials\u002Fts#include-exclude-paths",[5,10681],"include\u002Fexclude：告诉编译器「哪些文件参与类型检查」。默认包含项目下所有 .ts；exclude 里默认已有 node_modules、outDir。范围外的文件即使被 import 也照样解析，但不会被当成「入口」单独检查。 paths：模块路径别名，把 @\u002Futils\u002Ffoo 映射到 src\u002Futils\u002Ffoo。注意 paths 只影响编译器的类型解析，不影响运行时——运行时还得靠打包器（Vite 的 resolve.alias）或 Node 的 exports 真正解析。这是「TS 里能用别名、上线就找不到模块」的经典原因：两边都要配。",{"id":12824,"title":12380,"titles":12825,"content":12826,"level":129},"\u002Ftutorials\u002Fts#noemit-与增量构建项目引用",[5,10681],"noEmit: true：只做类型检查、不产出 JS。现代项目里这几乎是标配——见下一节。 incremental \u002F tsBuildInfoFile：把上次检查的缓存写盘，第二次只重查变动文件，加速。 项目引用（composite + references）：把大型仓库拆成多个 tsconfig，各自独立、按依赖增量重建，monorepo 常用。",{"id":12828,"title":12382,"titles":12829,"content":12830,"level":129},"\u002Ftutorials\u002Fts#为什么-noemit-打包器才是常见组合",[5,10681],"核心答案回到第一章：类型检查和转译是两件事。现代工具链把这两件事分工给不同工具： 打包器（Vite\u002Fesbuild\u002FSWC\u002Fwebpack）负责转译：快，专门把 TS 擦成 JS、把模块打包，但它不做完整的跨文件类型检查。 tsc --noEmit 或 IDE \u002F ESLint 负责类型检查：慢但准确，专挑类型错误，不负责产生产物。 所以典型配置是 noEmit: true 让 tsc 只当「类型守门员」，转译交给打包器。这也解释了为什么「类型明明报错、vite dev 还照常跑」——因为跑起来的是打包器那份不检查类型的转译。真正把关靠 CI 里那条 tsc --noEmit。第十七章会把这个分工讲得更完整。",{"id":12832,"title":11244,"titles":12833,"content":12834,"level":101},"\u002Ftutorials\u002Fts#十七工程实践与常见坑谁转译谁检查以及那份坑清单",[5],"这一章把前面所有原理收到「工程现场」。先讲清楚 TS 在真实项目里和各种工具怎么分工配合，再列出那些「知道就躲得过、不知道就踩个够」的坑。它们大多源自第一章的两个事实——类型会被擦除、类型检查与转译互不干涉。",{"id":12836,"title":11261,"titles":12837,"content":12838,"level":129},"\u002Ftutorials\u002Fts#谁做转译谁做检查vite-vue-react-node-的分工",[5,11244],"不同栈里，「把 TS 变成能跑的 JS」和「检查 TS 类型对不对」这两件事分别由谁干： 场景 转译（擦类型、产出） 类型检查 Vite（纯 TS） esbuild（极快，不检查类型） 需另跑 tsc --noEmit，或靠 IDE Vue + vue-tsc Vite\u002Fesbuild vue-tsc（模板里的类型也要查） React + Vite esbuild \u002F SWC tsc --noEmit Node（ts-node \u002F tsx） ts-node\u002Ftsx 即时擦除 看配置，默认可能不查 Node 原生 type stripping V8 直接抹类型跑 不做检查，纯转译 tsc 直接构建 tsc 转译 + 检查一起做 就是它 结论只有一句：跑得快的工具（esbuild\u002FSWC\u002FV8）通常只转译不检查，所以「本地能跑」不等于「类型没问题」——完整检查必须由 tsc --noEmit（CI 里）或 IDE 提供。这也是 Vue 项目要专门用 vue-tsc 的原因：模板里的表达式普通 tsc 看不到。",{"id":12840,"title":12387,"titles":12841,"content":12842,"level":129},"\u002Ftutorials\u002Fts#isolatedmodules-为什么需要",[5,11244],"打包器是一个文件一个文件独立转译的（它拿不到全局类型图），因此某些「需要跨文件才知道」的写法它处理不了。isolatedModules: true 让 tsc 提前拒掉这些写法，保证你的代码「每个文件都能被单独安全转译」。典型要求： 每个文件都得是真模块（有 import\u002Fexport），别写会被当成全局脚本的文件。 重导出类型要用 export type { T } 而非 export { T }。 别用 const enum 跨文件、别用只有 namespace 里的运行时值等。 现代框架脚手架几乎都默认开它——它不是让你更严，而是让「tsc 检查通过」和「打包器转译行为」对齐，避免出现「tsc 说没问题、esbuild 转出来不对」的裂缝。",{"id":12844,"title":11438,"titles":12845,"content":12846,"level":129},"\u002Ftutorials\u002Fts#eslint-与类型规则",[5,11244],"ESLint 负责 lint（代码风格、可疑写法、一致性），@typescript-eslint 让它认识 TS 语法。其中一部分规则需要类型信息（如「禁止在非 thenable 上 await」「no-misused-promises」），要在 ESLint 配置里提供 parserOptions.project 指向 tsconfig。分工：tsc --noEmit 管「类型对不对」，ESLint 管「写法好不好」，两者互补，不重复。常见踩坑是类型感知规则让 lint 变慢——把重规则集中到 CI，本地跑轻量集是通行做法。",{"id":12848,"title":12390,"titles":12849,"content":12850,"level":129},"\u002Ftutorials\u002Fts#坑一any-顺着返回值泄漏",[5,11244],"any 不只影响你标它的那一处，它会沿着数据流传染：一个返回 any 的调用，你把结果再传给别的运算，那条链上的类型全塌成 any。JSON.parse、部分老库、(e: any) 事件都是源头。对策：在函数边界加返回注解（const u: User = JSON.parse(s) as User 至少标明你要什么），开 noImplicitAny 拦住隐式漏点，遇到 any 尽量转成 unknown 再收窄（第三章）。",{"id":12852,"title":11504,"titles":12853,"content":12854,"level":129},"\u002Ftutorials\u002Fts#坑二结构化类型导致的假兼容",[5,11244],"第四章讲过 TS 看形状不看名字，副作用就是两个语义完全不同、形状恰好一致的类型可以互相赋值，编译器一声不吭： type UserID = string\ntype OrderID = string\nfunction get(u: UserID) {}\nget(orderId)   \u002F\u002F ✓ 编译通过！但语义上是把订单号当用户号传了 两者都是 string，TS 不拦。想制造「名义类型」的区分感，给类型加一个独有的品牌字段（branded type）：type UserID = string & { readonly __brand: 'User' }。否则别指望 TS 帮你区分「同为 string 的不同含义」。",{"id":12856,"title":12393,"titles":12857,"content":12858,"level":129},"\u002Ftutorials\u002Fts#坑三枚举的运行时成本与-const-enum",[5,11244],"第三章说过 enum 会真实生成对象。普通 enum 的问题是有运行时体积、跨文件在 isolatedModules 下受限；const enum 想省体积（编译期直接把 Direction.Up 内联成 0，不生成对象），但在打包\u002F分包场景 const enum 坑更多：内联要求编译器看到定义，esbuild\u002Fswc 各自独立转译时可能把 const enum 当普通 enum 处理，行为不一致。这也是大量团队弃用 enum、改用「as const 对象 + 联合字面量」的原因——零运行时、无内联歧义、还能窄化（第九章）。",{"id":12860,"title":12395,"titles":12861,"content":12862,"level":129},"\u002Ftutorials\u002Fts#坑四import-type-与循环引用",[5,11244],"类型之间会互相引用形成环（A 的类型用到 B，B 又用到 A）。纯类型循环对 tsc 无害（编译期解析、运行时无物），但一旦有人用值形式 import { SomeType } 引入了运行时 import，就可能造出真实的模块循环、触发初始化顺序 bug。用 import type（第十五章）把「只是类型」的依赖和「真要在运行时取值」的依赖分开，循环就只存在于编译期、不会带进运行时。",{"id":12864,"title":11642,"titles":12865,"content":12866,"level":129},"\u002Ftutorials\u002Fts#坑五过度断言",[5,11244],"第八章那句「断言是写给编译器的情书」在此回收。满屏 as、!、as unknown as，等于亲手把类型系统关掉却以为还开着——编译器不再报错，但运行时报错照旧。能用收窄（is 谓词）就别用 as；能用 satisfies 就别用 as；! 只在确实无法表达的非空场景用。断言是最后手段不是默认工具。",{"id":12868,"title":12398,"titles":12869,"content":12870,"level":129},"\u002Ftutorials\u002Fts#坑六把-unknown-用-as-直接吃掉",[5,11244],"面对 unknown（外部数据、catch 错误），正确姿势是收窄验证（typeof\u002Fin\u002F校验库）。最省事也最危险的做法是 const u = err as MyType 一步到「我以为的类型」——这等于绕过了 unknown 特意设置的那道「先验再用」的闸，把不安全又塞回了系统。unknown 是 any 的安全版（第三章），别用 as 把它退化回 any 的效果。",{"id":12872,"title":11719,"titles":12873,"content":12874,"level":101},"\u002Ftutorials\u002Fts#十八typescript-与-javascript-的区别",[5],"全课收口，回答三个终极问题：「TS 是 JS 的超集」对不对、TS 会不会让程序变快、哪些 JS 代码原样搬进 TS 会报错。 先给一张维度对照大表，再逐条展开。",{"id":12876,"title":11735,"titles":12877,"content":12878,"level":129},"\u002Ftutorials\u002Fts#一张总表维度-javascript-typescript-一句话备注",[5,11719],"维度 JavaScript TypeScript 一句话备注 类型系统 动态弱类型，运行时才定 静态强类型，编译期标注+推断 TS 的类型只在编译期存在 变量声明 let\u002Fconst\u002Fvar 同左，可加 : T 注解 注解是「可加」不是「必加」 编译与运行 引擎直接执行源码 需转译成 JS 再执行 运行时永远跑的是 JS 工具链 引擎\u002F浏览器即可 多一层 tsc\u002F打包器\u002F类型检查 见第十六、十七章分工 错误暴露时机 多在运行时 大量前移到编辑\u002F构建期 TS 的核心价值所在 重构安全性 靠人肉+全局搜 类型图兜底，改一处红一片 TS 最被低估的收益 文件后缀与模块 .js\u002F.mjs .ts\u002F.tsx\u002F.d.ts ESM 规则一致，多了类型文件 类与字段 标准 class、# 私有 加修饰符\u002F抽象\u002F参数属性 private 是编译期，# 是运行期 运行时能力 完整 JS 引擎能力 与 JS 完全相同，无增减 类型不带来任何新运行时能力 生态与学习成本 上手快、心智负担低 有学习曲线、类型体操的成本 换确定性要付出复杂度 可混合性 独立存在 可与 JS 共存、逐步迁移 .ts 能 import .js（配 allowJs） 渐进采纳 无此概念 可从「几乎不写类型」起步 checkJs 关闭时 TS 项目也能纯放 JS 下面逐条把关键的几行讲透。",{"id":12880,"title":11966,"titles":12881,"content":12882,"level":129},"\u002Ftutorials\u002Fts#ts-是不是-js-的超集对但只对了一半",[5,11719],"严格地说，语法上 TS ⊇ JS：任何合法 JS 文件改名 .ts 都能被 TS 解析（JS 是 TS 的子集）。但「超集」这个词让人以为「TS = JS 的全部行为 + 额外的东西」，这有误导性：TS 不新增任何运行时行为，它新增的是编译期的一层检查。所以更准确的表述是：TS 是「JS + 一套会被擦除的静态类型语法 + 一个会在运行前挑毛病的检查器」。 还有一个反直觉的事实（也是第三个问题的答案）：并非所有 JS 都能原样塞进 TS 且不报错。因为 TS 默认开 strict、开 noImplicitAny，纯 JS 里那些「隐式 any、可能为 null 就直接用、参数没传全」的写法，在 TS 里会变成编译错误。也就是说：JS 是 TS 语法上的子集，但一段典型的裸 JS 放进严格配置的 TS 里常常通不过类型检查。",{"id":12884,"title":12004,"titles":12885,"content":12886,"level":129},"\u002Ftutorials\u002Fts#ts-会不会让程序跑得更快不会",[5,11719],"程序实际执行的是擦除类型后的 JS，跟直接写 JS 的最终产物基本等价，运行时性能没有提升。恰恰相反，多出来的 tsc\u002F类型检查是构建期的开销，让「改代码到能跑」这一环变慢（这也是把转译交给 esbuild、检查交给 tsc 的分工动机）。TS 换来的是「人的效率」——少 debug、少回归、重构敢下手——而不是「机器执行」的效率。谁指望加类型让接口变快，那是要失望的。",{"id":12888,"title":12026,"titles":12889,"content":12890,"level":129},"\u002Ftutorials\u002Fts#哪些-js-代码原样放进-ts-会报错",[5,11719],"把这三类裸 JS 丢进 strict 的 TS，立刻红给你看，最能说明「类型检查改的是编译期，不是运行期」： \u002F\u002F ① 隐式 any：参数没类型，noImplicitAny 直接拦\nfunction add(a, b) { return a + b }        \u002F\u002F ✗ a、b 隐式 any\n\n\u002F\u002F ② 可能为 null 却不判断：strictNullChecks 拦\nfunction greet(u) { return u.name.toUpperCase() }  \u002F\u002F 若 u: { name?: string } 则 name 可能 undefined\n\n\u002F\u002F ③ 给 const 重新赋值 \u002F 用了未声明的变量——这些 JS 运行时也炸，但 TS 提前在编辑期就标红\nconst x = 1; x = 2                          \u002F\u002F ✗ 对 const 赋值\ny = 10                                       \u002F\u002F ✗ 找不到 y（除非声明或挂全局） 反过来，这些是「TS 有、JS 完全没有」的纯类型语法：: 类型、interface、type、\u003CT> 泛型、enum、as\u002Fsatisfies\u002F! 断言、declare。它们写出来 JS 根本不认，也正是转译时被擦掉的部分。",{"id":12892,"title":12177,"titles":12893,"content":12894,"level":129},"\u002Ftutorials\u002Fts#一句话收尾",[5,11719],"TS 的全部故事，第一章就讲完了：它把「运行时的不确定」变成「编辑时的红波浪线」，代价是多一套工具链和一层要理解的类型规则。学 TS 不是学一门新语言，而是学如何把你的意图，用类型这套语言讲给编译器听——讲对了，它在运行前就替你把关；讲歪了（滥用 any\u002Fas），它就把闸放开、假装一切正常，把问题原样还给运行时。理解「类型擦除」和「检查与转译两件事」，你就拿到了后面所有细节的解释力。 html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",1793263562518]