[{"data":1,"prerenderedAt":726},["ShallowReactive",2],{"post-\u002Fposts\u002Ftypescript-satisfies-in-practice":3,"surround-\u002Fposts\u002Ftypescript-satisfies-in-practice":717},{"id":4,"title":5,"body":6,"cover":705,"date":706,"description":707,"draft":708,"extension":709,"meta":710,"minutes":84,"navigation":87,"path":711,"seo":712,"stem":713,"tags":714,"updated":705,"__hash__":716},"posts\u002Fposts\u002Ftypescript-satisfies-in-practice.md","satisfies、as const 与类型收窄的日常",{"type":7,"value":8,"toc":699},"minimark",[9,17,22,25,266,271,400,404,413,497,500,504,510,676,683,686,695],[10,11,12,16],"p",{},[13,14,15],"code",{},"satisfies"," 出现之前，我们在「类型标注」和「类型推断」之间只能二选一。标注了就丢失精度，不标注就没有校验。它的价值在于两个都要。",[18,19,21],"h2",{"id":20},"场景一配置对象","场景一：配置对象",[10,23,24],{},"先看没有它的时候会发生什么：",[26,27,32],"pre",{"className":28,"code":29,"language":30,"meta":31,"style":31},"language-ts shiki shiki-themes tokyo-night github-light","type Route = { path: string, prerender?: boolean }\n\n\u002F\u002F 标注：校验通过，但 routes.home.path 的类型退化成 string\nconst routes: Record\u003Cstring, Route> = {\n  home: { path: '\u002F', prerender: true },\n  posts: { path: '\u002Fposts' },\n}\n\n\u002F\u002F 推断：保住了字面量类型，但拼错字段没人管\nconst routes2 = {\n  home: { path: '\u002F', prerennder: true }, \u002F\u002F 拼错了，无人报错\n}\n","ts","",[13,33,34,82,89,96,130,172,197,203,208,214,226,261],{"__ignoreMap":31},[35,36,39,43,47,51,55,59,62,66,70,73,76,79],"span",{"class":37,"line":38},"line",1,[35,40,42],{"class":41},"sBxw3","type",[35,44,46],{"class":45},"sirr2"," Route",[35,48,50],{"class":49},"sNP9S"," =",[35,52,54],{"class":53},"sjXpA"," {",[35,56,58],{"class":57},"surVy"," path",[35,60,61],{"class":49},":",[35,63,65],{"class":64},"sZlli"," string",[35,67,69],{"class":68},"szu1u",",",[35,71,72],{"class":57}," prerender",[35,74,75],{"class":49},"?:",[35,77,78],{"class":64}," boolean",[35,80,81],{"class":53}," }\n",[35,83,85],{"class":37,"line":84},2,[35,86,88],{"emptyLinePlaceholder":87},true,"\n",[35,90,92],{"class":37,"line":91},3,[35,93,95],{"class":94},"sSWvG","\u002F\u002F 标注：校验通过，但 routes.home.path 的类型退化成 string\n",[35,97,99,103,107,109,112,115,118,120,122,125,127],{"class":37,"line":98},4,[35,100,102],{"class":101},"sG9mu","const",[35,104,106],{"class":105},"sV0ZF"," routes",[35,108,61],{"class":49},[35,110,111],{"class":45}," Record",[35,113,114],{"class":68},"\u003C",[35,116,117],{"class":64},"string",[35,119,69],{"class":68},[35,121,46],{"class":45},[35,123,124],{"class":68},">",[35,126,50],{"class":49},[35,128,129],{"class":53}," {\n",[35,131,133,137,139,141,143,145,149,153,156,158,160,162,166,169],{"class":37,"line":132},5,[35,134,136],{"class":135},"s9SBS","  home",[35,138,61],{"class":68},[35,140,54],{"class":53},[35,142,58],{"class":135},[35,144,61],{"class":68},[35,146,148],{"class":147},"sbW-k"," '",[35,150,152],{"class":151},"snP_1","\u002F",[35,154,155],{"class":147},"'",[35,157,69],{"class":68},[35,159,72],{"class":135},[35,161,61],{"class":68},[35,163,165],{"class":164},"sjYRd"," true",[35,167,168],{"class":53}," }",[35,170,171],{"class":68},",\n",[35,173,175,178,180,182,184,186,188,191,193,195],{"class":37,"line":174},6,[35,176,177],{"class":135},"  posts",[35,179,61],{"class":68},[35,181,54],{"class":53},[35,183,58],{"class":135},[35,185,61],{"class":68},[35,187,148],{"class":147},[35,189,190],{"class":151},"\u002Fposts",[35,192,155],{"class":147},[35,194,168],{"class":53},[35,196,171],{"class":68},[35,198,200],{"class":37,"line":199},7,[35,201,202],{"class":53},"}\n",[35,204,206],{"class":37,"line":205},8,[35,207,88],{"emptyLinePlaceholder":87},[35,209,211],{"class":37,"line":210},9,[35,212,213],{"class":94},"\u002F\u002F 推断：保住了字面量类型，但拼错字段没人管\n",[35,215,217,219,222,224],{"class":37,"line":216},10,[35,218,102],{"class":101},[35,220,221],{"class":105}," routes2",[35,223,50],{"class":49},[35,225,129],{"class":53},[35,227,229,231,233,235,237,239,241,243,245,247,250,252,254,256,258],{"class":37,"line":228},11,[35,230,136],{"class":135},[35,232,61],{"class":68},[35,234,54],{"class":53},[35,236,58],{"class":135},[35,238,61],{"class":68},[35,240,148],{"class":147},[35,242,152],{"class":151},[35,244,155],{"class":147},[35,246,69],{"class":68},[35,248,249],{"class":135}," prerennder",[35,251,61],{"class":68},[35,253,165],{"class":164},[35,255,168],{"class":53},[35,257,69],{"class":68},[35,259,260],{"class":94}," \u002F\u002F 拼错了，无人报错\n",[35,262,264],{"class":37,"line":263},12,[35,265,202],{"class":53},[10,267,268,270],{},[13,269,15],{}," 同时拿到两边的好处：",[26,272,274],{"className":28,"code":273,"language":30,"meta":31,"style":31},"const routes = {\n  home: { path: '\u002F', prerender: true },\n  posts: { path: '\u002Fposts' },\n} satisfies Record\u003Cstring, Route>\n\nroutes.home.path \u002F\u002F 类型是 '\u002F'，不是 string\nroutes.home.prerennder \u002F\u002F 编译期报错\n",[13,275,276,286,316,338,359,363,384],{"__ignoreMap":31},[35,277,278,280,282,284],{"class":37,"line":38},[35,279,102],{"class":101},[35,281,106],{"class":105},[35,283,50],{"class":49},[35,285,129],{"class":53},[35,287,288,290,292,294,296,298,300,302,304,306,308,310,312,314],{"class":37,"line":84},[35,289,136],{"class":135},[35,291,61],{"class":68},[35,293,54],{"class":53},[35,295,58],{"class":135},[35,297,61],{"class":68},[35,299,148],{"class":147},[35,301,152],{"class":151},[35,303,155],{"class":147},[35,305,69],{"class":68},[35,307,72],{"class":135},[35,309,61],{"class":68},[35,311,165],{"class":164},[35,313,168],{"class":53},[35,315,171],{"class":68},[35,317,318,320,322,324,326,328,330,332,334,336],{"class":37,"line":91},[35,319,177],{"class":135},[35,321,61],{"class":68},[35,323,54],{"class":53},[35,325,58],{"class":135},[35,327,61],{"class":68},[35,329,148],{"class":147},[35,331,190],{"class":151},[35,333,155],{"class":147},[35,335,168],{"class":53},[35,337,171],{"class":68},[35,339,340,343,346,348,350,352,354,356],{"class":37,"line":98},[35,341,342],{"class":53},"}",[35,344,345],{"class":41}," satisfies",[35,347,111],{"class":45},[35,349,114],{"class":68},[35,351,117],{"class":64},[35,353,69],{"class":68},[35,355,46],{"class":45},[35,357,358],{"class":68},">\n",[35,360,361],{"class":37,"line":132},[35,362,88],{"emptyLinePlaceholder":87},[35,364,365,369,372,375,377,381],{"class":37,"line":174},[35,366,368],{"class":367},"s4do5","routes",[35,370,371],{"class":68},".",[35,373,374],{"class":367},"home",[35,376,371],{"class":68},[35,378,380],{"class":379},"sVZrm","path",[35,382,383],{"class":94}," \u002F\u002F 类型是 '\u002F'，不是 string\n",[35,385,386,388,390,392,394,397],{"class":37,"line":199},[35,387,368],{"class":367},[35,389,371],{"class":68},[35,391,374],{"class":367},[35,393,371],{"class":68},[35,395,396],{"class":379},"prerennder",[35,398,399],{"class":94}," \u002F\u002F 编译期报错\n",[18,401,403],{"id":402},"场景二和-as-const-叠加","场景二：和 as const 叠加",[10,405,406,409,410,412],{},[13,407,408],{},"as const"," 负责把值冻结成最窄的字面量类型，",[13,411,15],{}," 负责校验形状，两者叠加是常见组合：",[26,414,416],{"className":28,"code":415,"language":30,"meta":31,"style":31},"const themes = ['dark', 'light'] as const satisfies readonly string[]\n\ntype Theme = (typeof themes)[number] \u002F\u002F 'dark' | 'light'\n",[13,417,418,465,469],{"__ignoreMap":31},[35,419,420,422,425,427,430,432,435,437,439,441,444,446,449,452,455,457,460,462],{"class":37,"line":38},[35,421,102],{"class":101},[35,423,424],{"class":105}," themes",[35,426,50],{"class":49},[35,428,429],{"class":53}," [",[35,431,155],{"class":147},[35,433,434],{"class":151},"dark",[35,436,155],{"class":147},[35,438,69],{"class":68},[35,440,148],{"class":147},[35,442,443],{"class":151},"light",[35,445,155],{"class":147},[35,447,448],{"class":53},"]",[35,450,451],{"class":49}," as",[35,453,454],{"class":45}," const",[35,456,345],{"class":41},[35,458,459],{"class":101}," readonly",[35,461,65],{"class":64},[35,463,464],{"class":53},"[]\n",[35,466,467],{"class":37,"line":84},[35,468,88],{"emptyLinePlaceholder":87},[35,470,471,473,476,478,481,484,486,489,492,494],{"class":37,"line":91},[35,472,42],{"class":41},[35,474,475],{"class":45}," Theme",[35,477,50],{"class":49},[35,479,480],{"class":53}," (",[35,482,483],{"class":49},"typeof",[35,485,424],{"class":367},[35,487,488],{"class":53},")[",[35,490,491],{"class":64},"number",[35,493,448],{"class":53},[35,495,496],{"class":94}," \u002F\u002F 'dark' | 'light'\n",[10,498,499],{},"联合类型直接从数据里长出来，加一个主题只需要改一处。",[18,501,503],{"id":502},"场景三穷尽检查","场景三：穷尽检查",[10,505,506,509],{},[13,507,508],{},"satisfies never"," 是最便宜的穷尽检查手段：",[26,511,513],{"className":28,"code":512,"language":30,"meta":31,"style":31},"function label(theme: Theme): string {\n  switch (theme) {\n    case 'dark': return '暗色'\n    case 'light': return '亮色'\n    default: {\n      const unreachable = theme satisfies never\n      throw new Error(`未处理的主题: ${unreachable}`)\n    }\n  }\n}\n",[13,514,515,544,556,581,602,611,629,662,667,672],{"__ignoreMap":31},[35,516,517,520,524,527,531,533,535,538,540,542],{"class":37,"line":38},[35,518,519],{"class":41},"function",[35,521,523],{"class":522},"sxkyg"," label",[35,525,526],{"class":53},"(",[35,528,530],{"class":529},"sgjcI","theme",[35,532,61],{"class":49},[35,534,475],{"class":45},[35,536,537],{"class":53},")",[35,539,61],{"class":49},[35,541,65],{"class":64},[35,543,129],{"class":53},[35,545,546,549,551,553],{"class":37,"line":84},[35,547,548],{"class":41},"  switch",[35,550,480],{"class":53},[35,552,530],{"class":367},[35,554,555],{"class":53},") {\n",[35,557,558,561,563,565,567,569,573,575,578],{"class":37,"line":91},[35,559,560],{"class":41},"    case",[35,562,148],{"class":147},[35,564,434],{"class":151},[35,566,155],{"class":147},[35,568,61],{"class":68},[35,570,572],{"class":571},"skDMT"," return",[35,574,148],{"class":147},[35,576,577],{"class":151},"暗色",[35,579,580],{"class":147},"'\n",[35,582,583,585,587,589,591,593,595,597,600],{"class":37,"line":98},[35,584,560],{"class":41},[35,586,148],{"class":147},[35,588,443],{"class":151},[35,590,155],{"class":147},[35,592,61],{"class":68},[35,594,572],{"class":571},[35,596,148],{"class":147},[35,598,599],{"class":151},"亮色",[35,601,580],{"class":147},[35,603,604,607,609],{"class":37,"line":132},[35,605,606],{"class":41},"    default",[35,608,61],{"class":68},[35,610,129],{"class":53},[35,612,613,616,619,621,624,626],{"class":37,"line":174},[35,614,615],{"class":101},"      const",[35,617,618],{"class":105}," unreachable",[35,620,50],{"class":49},[35,622,623],{"class":367}," theme",[35,625,345],{"class":41},[35,627,628],{"class":64}," never\n",[35,630,631,634,637,640,642,645,648,652,655,657,659],{"class":37,"line":199},[35,632,633],{"class":41},"      throw",[35,635,636],{"class":49}," new",[35,638,639],{"class":522}," Error",[35,641,526],{"class":53},[35,643,644],{"class":147},"`",[35,646,647],{"class":151},"未处理的主题: ",[35,649,651],{"class":650},"sLX9A","${",[35,653,654],{"class":367},"unreachable",[35,656,342],{"class":650},[35,658,644],{"class":147},[35,660,661],{"class":53},")\n",[35,663,664],{"class":37,"line":205},[35,665,666],{"class":53},"    }\n",[35,668,669],{"class":37,"line":210},[35,670,671],{"class":53},"  }\n",[35,673,674],{"class":37,"line":216},[35,675,202],{"class":53},[10,677,678,679,682],{},"未来给 ",[13,680,681],{},"themes"," 加了新值却忘记补 case，这里会在编译期亮红灯，而不是在运行时抛异常。",[18,684,685],{"id":685},"一条经验法则",[10,687,688,689,691,692,694],{},"写常量数据时先用推断，发现需要校验形状时加 ",[13,690,15],{},"，需要字面量联合时加 ",[13,693,408],{},"。显式标注留给函数签名和公共 API，那才是它的主场。",[696,697,698],"style",{},"html pre.shiki code .sBxw3, html code.shiki .sBxw3{--shiki-default:#BB9AF7;--shiki-light:#D73A49}html pre.shiki code .sirr2, html code.shiki .sirr2{--shiki-default:#C0CAF5;--shiki-light:#6F42C1}html pre.shiki code .sNP9S, html code.shiki .sNP9S{--shiki-default:#89DDFF;--shiki-light:#D73A49}html pre.shiki code .sjXpA, html code.shiki .sjXpA{--shiki-default:#9ABDF5;--shiki-light:#24292E}html pre.shiki code .surVy, html code.shiki .surVy{--shiki-default:#73DACA;--shiki-light:#E36209}html pre.shiki code .sZlli, html code.shiki .sZlli{--shiki-default:#0DB9D7;--shiki-light:#005CC5}html pre.shiki code .szu1u, html code.shiki .szu1u{--shiki-default:#89DDFF;--shiki-light:#24292E}html pre.shiki code .sSWvG, html code.shiki .sSWvG{--shiki-default:#51597D;--shiki-default-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit}html pre.shiki code .sG9mu, html code.shiki .sG9mu{--shiki-default:#9D7CD8;--shiki-default-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit}html pre.shiki code .sV0ZF, html code.shiki .sV0ZF{--shiki-default:#BB9AF7;--shiki-light:#005CC5}html pre.shiki code .s9SBS, html code.shiki .s9SBS{--shiki-default:#73DACA;--shiki-light:#24292E}html pre.shiki code .sbW-k, html code.shiki .sbW-k{--shiki-default:#89DDFF;--shiki-light:#032F62}html pre.shiki code .snP_1, html code.shiki .snP_1{--shiki-default:#9ECE6A;--shiki-light:#032F62}html pre.shiki code .sjYRd, html code.shiki .sjYRd{--shiki-default:#FF9E64;--shiki-light:#005CC5}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 .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html pre.shiki code .s4do5, html code.shiki .s4do5{--shiki-default:#C0CAF5;--shiki-light:#24292E}html pre.shiki code .sVZrm, html code.shiki .sVZrm{--shiki-default:#7DCFFF;--shiki-light:#24292E}html pre.shiki code .sxkyg, html code.shiki .sxkyg{--shiki-default:#7AA2F7;--shiki-light:#6F42C1}html pre.shiki code .sgjcI, html code.shiki .sgjcI{--shiki-default:#E0AF68;--shiki-light:#E36209}html pre.shiki code .skDMT, html code.shiki .skDMT{--shiki-default:#BB9AF7;--shiki-default-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit}html pre.shiki code .sLX9A, html code.shiki .sLX9A{--shiki-default:#7DCFFF;--shiki-light:#032F62}",{"title":31,"searchDepth":91,"depth":91,"links":700},[701,702,703,704],{"id":20,"depth":84,"text":21},{"id":402,"depth":84,"text":403},{"id":502,"depth":84,"text":503},{"id":685,"depth":84,"text":685},null,"2026-03-21","satisfies 解决的是一个很具体的矛盾：既要校验值符合某个宽类型，又不想丢掉字面量本身的精确类型。三个日常场景说明它和 as const 怎么配合。",false,"md",{},"\u002Fposts\u002Ftypescript-satisfies-in-practice",{"title":5,"description":707},"posts\u002Ftypescript-satisfies-in-practice",[715],"typescript","PDtBxpVC_T_E-Dpv0dYPJGqKI0o2mSDzAOizcoBAPc8",[718,722],{"title":719,"path":720,"stem":721,"children":-1},"Markdown 排版全要素测试","\u002Fposts\u002Fmarkdown-kitchen-sink","posts\u002Fmarkdown-kitchen-sink",{"title":723,"path":724,"stem":725,"children":-1},"本地优先软件笔记：数据先是你的，然后才是云的","\u002Fposts\u002Flocal-first-notes","posts\u002Flocal-first-notes",1786707084540]