[{"data":1,"prerenderedAt":2124},["ShallowReactive",2],{"post-en-typescript-interfaces":3},{"id":4,"title":5,"body":6,"date":2109,"description":2110,"excerpt":2111,"extension":2112,"meta":2113,"navigation":346,"path":2114,"readTime":115,"seo":2115,"slug":2116,"stem":2117,"tags":2118,"__hash__":2123},"en_blog/en/blog/typescript-interfaces.md","TypeScript in Practice: Hierarchical Interfaces on a Real API",{"type":7,"value":8,"toc":2099},"minimark",[9,13,22,27,30,322,326,329,511,526,530,811,815,818,1052,1056,1059,1518,1521,1610,1625,1629,1632,1793,1807,1811,1814,2056,2060,2063,2092,2095],[10,11,5],"h1",{"id":12},"typescript-in-practice-hierarchical-interfaces-on-a-real-api",[14,15,16,17,21],"p",{},"Most TypeScript tutorials demonstrate trivial examples: ",[18,19,20],"code",{},"interface User { name: string; age: number }",". Production code is considerably more involved. External APIs return nested structures, conditionally optional fields, and type unions that vary by state. Here is how to model all of that cleanly, starting from a real-world case.",[23,24,26],"h2",{"id":25},"the-starting-point-an-api-with-a-hierarchical-structure","The Starting Point: An API With a Hierarchical Structure",[14,28,29],{},"Consider a certificate management API that returns structures of the following shape:",[31,32,37],"pre",{"className":33,"code":34,"language":35,"meta":36,"style":36},"language-json shiki shiki-themes github-dark github-light","{\n  \"account\": {\n    \"id\": \"ACC-001\",\n    \"name\": \"EDF Production\",\n    \"type\": \"PRODUCER\"\n  },\n  \"certificates\": [\n    {\n      \"id\": \"GO-2024-001\",\n      \"volume\": 1500.5,\n      \"unit\": \"MWh\",\n      \"period\": { \"from\": \"2024-01-01\", \"to\": \"2024-01-31\" },\n      \"status\": \"ACTIVE\",\n      \"metadata\": {\n        \"technology\": \"WIND\",\n        \"country\": \"FR\",\n        \"installation_id\": \"INS-042\"\n      }\n    }\n  ],\n  \"pagination\": {\n    \"page\": 1,\n    \"per_page\": 50,\n    \"total\": 312\n  }\n}\n","json","",[18,38,39,48,58,74,87,98,104,113,119,132,145,158,189,202,210,223,236,247,253,259,265,273,286,299,310,316],{"__ignoreMap":36},[40,41,44],"span",{"class":42,"line":43},"line",1,[40,45,47],{"class":46},"sQ3_J","{\n",[40,49,51,55],{"class":42,"line":50},2,[40,52,54],{"class":53},"s0DvM","  \"account\"",[40,56,57],{"class":46},": {\n",[40,59,61,64,67,71],{"class":42,"line":60},3,[40,62,63],{"class":53},"    \"id\"",[40,65,66],{"class":46},": ",[40,68,70],{"class":69},"sg6BJ","\"ACC-001\"",[40,72,73],{"class":46},",\n",[40,75,77,80,82,85],{"class":42,"line":76},4,[40,78,79],{"class":53},"    \"name\"",[40,81,66],{"class":46},[40,83,84],{"class":69},"\"EDF Production\"",[40,86,73],{"class":46},[40,88,90,93,95],{"class":42,"line":89},5,[40,91,92],{"class":53},"    \"type\"",[40,94,66],{"class":46},[40,96,97],{"class":69},"\"PRODUCER\"\n",[40,99,101],{"class":42,"line":100},6,[40,102,103],{"class":46},"  },\n",[40,105,107,110],{"class":42,"line":106},7,[40,108,109],{"class":53},"  \"certificates\"",[40,111,112],{"class":46},": [\n",[40,114,116],{"class":42,"line":115},8,[40,117,118],{"class":46},"    {\n",[40,120,122,125,127,130],{"class":42,"line":121},9,[40,123,124],{"class":53},"      \"id\"",[40,126,66],{"class":46},[40,128,129],{"class":69},"\"GO-2024-001\"",[40,131,73],{"class":46},[40,133,135,138,140,143],{"class":42,"line":134},10,[40,136,137],{"class":53},"      \"volume\"",[40,139,66],{"class":46},[40,141,142],{"class":53},"1500.5",[40,144,73],{"class":46},[40,146,148,151,153,156],{"class":42,"line":147},11,[40,149,150],{"class":53},"      \"unit\"",[40,152,66],{"class":46},[40,154,155],{"class":69},"\"MWh\"",[40,157,73],{"class":46},[40,159,161,164,167,170,172,175,178,181,183,186],{"class":42,"line":160},12,[40,162,163],{"class":53},"      \"period\"",[40,165,166],{"class":46},": { ",[40,168,169],{"class":53},"\"from\"",[40,171,66],{"class":46},[40,173,174],{"class":69},"\"2024-01-01\"",[40,176,177],{"class":46},", ",[40,179,180],{"class":53},"\"to\"",[40,182,66],{"class":46},[40,184,185],{"class":69},"\"2024-01-31\"",[40,187,188],{"class":46}," },\n",[40,190,192,195,197,200],{"class":42,"line":191},13,[40,193,194],{"class":53},"      \"status\"",[40,196,66],{"class":46},[40,198,199],{"class":69},"\"ACTIVE\"",[40,201,73],{"class":46},[40,203,205,208],{"class":42,"line":204},14,[40,206,207],{"class":53},"      \"metadata\"",[40,209,57],{"class":46},[40,211,213,216,218,221],{"class":42,"line":212},15,[40,214,215],{"class":53},"        \"technology\"",[40,217,66],{"class":46},[40,219,220],{"class":69},"\"WIND\"",[40,222,73],{"class":46},[40,224,226,229,231,234],{"class":42,"line":225},16,[40,227,228],{"class":53},"        \"country\"",[40,230,66],{"class":46},[40,232,233],{"class":69},"\"FR\"",[40,235,73],{"class":46},[40,237,239,242,244],{"class":42,"line":238},17,[40,240,241],{"class":53},"        \"installation_id\"",[40,243,66],{"class":46},[40,245,246],{"class":69},"\"INS-042\"\n",[40,248,250],{"class":42,"line":249},18,[40,251,252],{"class":46},"      }\n",[40,254,256],{"class":42,"line":255},19,[40,257,258],{"class":46},"    }\n",[40,260,262],{"class":42,"line":261},20,[40,263,264],{"class":46},"  ],\n",[40,266,268,271],{"class":42,"line":267},21,[40,269,270],{"class":53},"  \"pagination\"",[40,272,57],{"class":46},[40,274,276,279,281,284],{"class":42,"line":275},22,[40,277,278],{"class":53},"    \"page\"",[40,280,66],{"class":46},[40,282,283],{"class":53},"1",[40,285,73],{"class":46},[40,287,289,292,294,297],{"class":42,"line":288},23,[40,290,291],{"class":53},"    \"per_page\"",[40,293,66],{"class":46},[40,295,296],{"class":53},"50",[40,298,73],{"class":46},[40,300,302,305,307],{"class":42,"line":301},24,[40,303,304],{"class":53},"    \"total\"",[40,306,66],{"class":46},[40,308,309],{"class":53},"312\n",[40,311,313],{"class":42,"line":312},25,[40,314,315],{"class":46},"  }\n",[40,317,319],{"class":42,"line":318},26,[40,320,321],{"class":46},"}\n",[23,323,325],{"id":324},"modelling-the-atomic-types","Modelling the Atomic Types",[14,327,328],{},"Start with the leaf-level types — literal unions rather than bare strings:",[31,330,334],{"className":331,"code":332,"language":333,"meta":36,"style":36},"language-typescript shiki shiki-themes github-dark github-light","// types/api.ts\n\nexport type AccountType = \"PRODUCER\" | \"TRADER\" | \"CONSUMER\"\nexport type CertificateStatus = \"ACTIVE\" | \"CANCELLED\" | \"TRANSFERRED\"\nexport type EnergyTechnology = \"WIND\" | \"SOLAR\" | \"HYDRO\" | \"BIOMASS\"\nexport type CountryCode = \"FR\" | \"DE\" | \"ES\" | \"IT\" | \"BE\"\n\nexport interface DateRange {\n  from: string // ISO 8601\n  to: string\n}\n","typescript",[18,335,336,342,348,378,402,431,465,469,482,497,507],{"__ignoreMap":36},[40,337,338],{"class":42,"line":43},[40,339,341],{"class":340},"sryI4","// types/api.ts\n",[40,343,344],{"class":42,"line":50},[40,345,347],{"emptyLinePlaceholder":346},true,"\n",[40,349,350,354,357,361,364,367,370,373,375],{"class":42,"line":60},[40,351,353],{"class":352},"scx8i","export",[40,355,356],{"class":352}," type",[40,358,360],{"class":359},"s-Z4r"," AccountType",[40,362,363],{"class":352}," =",[40,365,366],{"class":69}," \"PRODUCER\"",[40,368,369],{"class":352}," |",[40,371,372],{"class":69}," \"TRADER\"",[40,374,369],{"class":352},[40,376,377],{"class":69}," \"CONSUMER\"\n",[40,379,380,382,384,387,389,392,394,397,399],{"class":42,"line":76},[40,381,353],{"class":352},[40,383,356],{"class":352},[40,385,386],{"class":359}," CertificateStatus",[40,388,363],{"class":352},[40,390,391],{"class":69}," \"ACTIVE\"",[40,393,369],{"class":352},[40,395,396],{"class":69}," \"CANCELLED\"",[40,398,369],{"class":352},[40,400,401],{"class":69}," \"TRANSFERRED\"\n",[40,403,404,406,408,411,413,416,418,421,423,426,428],{"class":42,"line":89},[40,405,353],{"class":352},[40,407,356],{"class":352},[40,409,410],{"class":359}," EnergyTechnology",[40,412,363],{"class":352},[40,414,415],{"class":69}," \"WIND\"",[40,417,369],{"class":352},[40,419,420],{"class":69}," \"SOLAR\"",[40,422,369],{"class":352},[40,424,425],{"class":69}," \"HYDRO\"",[40,427,369],{"class":352},[40,429,430],{"class":69}," \"BIOMASS\"\n",[40,432,433,435,437,440,442,445,447,450,452,455,457,460,462],{"class":42,"line":100},[40,434,353],{"class":352},[40,436,356],{"class":352},[40,438,439],{"class":359}," CountryCode",[40,441,363],{"class":352},[40,443,444],{"class":69}," \"FR\"",[40,446,369],{"class":352},[40,448,449],{"class":69}," \"DE\"",[40,451,369],{"class":352},[40,453,454],{"class":69}," \"ES\"",[40,456,369],{"class":352},[40,458,459],{"class":69}," \"IT\"",[40,461,369],{"class":352},[40,463,464],{"class":69}," \"BE\"\n",[40,466,467],{"class":42,"line":106},[40,468,347],{"emptyLinePlaceholder":346},[40,470,471,473,476,479],{"class":42,"line":115},[40,472,353],{"class":352},[40,474,475],{"class":352}," interface",[40,477,478],{"class":359}," DateRange",[40,480,481],{"class":46}," {\n",[40,483,484,488,491,494],{"class":42,"line":121},[40,485,487],{"class":486},"sFbx2","  from",[40,489,490],{"class":352},":",[40,492,493],{"class":53}," string",[40,495,496],{"class":340}," // ISO 8601\n",[40,498,499,502,504],{"class":42,"line":134},[40,500,501],{"class":486},"  to",[40,503,490],{"class":352},[40,505,506],{"class":53}," string\n",[40,508,509],{"class":42,"line":147},[40,510,321],{"class":46},[14,512,513,514,517,518,521,522,525],{},"Literal unions rather than raw ",[18,515,516],{},"string"," types: TypeScript will flag ",[18,519,520],{},"'WIND_OFFSHORE'"," immediately wherever ",[18,523,524],{},"EnergyTechnology"," is expected, rather than letting it slip through to a runtime error.",[23,527,529],{"id":528},"hierarchical-interfaces","Hierarchical Interfaces",[31,531,533],{"className":331,"code":532,"language":333,"meta":36,"style":36},"export interface Account {\n  id: string\n  name: string\n  type: AccountType\n}\n\nexport interface CertificateMetadata {\n  technology: EnergyTechnology\n  country: CountryCode\n  installation_id: string\n}\n\nexport interface Certificate {\n  id: string\n  volume: number\n  unit: \"MWh\" | \"kWh\"\n  period: DateRange\n  status: CertificateStatus\n  metadata: CertificateMetadata\n}\n\nexport interface Pagination {\n  page: number\n  per_page: number\n  total: number\n}\n\nexport interface CertificatesResponse {\n  account: Account\n  certificates: Certificate[]\n  pagination: Pagination\n}\n",[18,534,535,546,555,564,574,578,582,593,603,613,622,626,630,641,649,659,674,684,694,704,708,712,723,732,741,750,754,759,771,782,795,806],{"__ignoreMap":36},[40,536,537,539,541,544],{"class":42,"line":43},[40,538,353],{"class":352},[40,540,475],{"class":352},[40,542,543],{"class":359}," Account",[40,545,481],{"class":46},[40,547,548,551,553],{"class":42,"line":50},[40,549,550],{"class":486},"  id",[40,552,490],{"class":352},[40,554,506],{"class":53},[40,556,557,560,562],{"class":42,"line":60},[40,558,559],{"class":486},"  name",[40,561,490],{"class":352},[40,563,506],{"class":53},[40,565,566,569,571],{"class":42,"line":76},[40,567,568],{"class":486},"  type",[40,570,490],{"class":352},[40,572,573],{"class":359}," AccountType\n",[40,575,576],{"class":42,"line":89},[40,577,321],{"class":46},[40,579,580],{"class":42,"line":100},[40,581,347],{"emptyLinePlaceholder":346},[40,583,584,586,588,591],{"class":42,"line":106},[40,585,353],{"class":352},[40,587,475],{"class":352},[40,589,590],{"class":359}," CertificateMetadata",[40,592,481],{"class":46},[40,594,595,598,600],{"class":42,"line":115},[40,596,597],{"class":486},"  technology",[40,599,490],{"class":352},[40,601,602],{"class":359}," EnergyTechnology\n",[40,604,605,608,610],{"class":42,"line":121},[40,606,607],{"class":486},"  country",[40,609,490],{"class":352},[40,611,612],{"class":359}," CountryCode\n",[40,614,615,618,620],{"class":42,"line":134},[40,616,617],{"class":486},"  installation_id",[40,619,490],{"class":352},[40,621,506],{"class":53},[40,623,624],{"class":42,"line":147},[40,625,321],{"class":46},[40,627,628],{"class":42,"line":160},[40,629,347],{"emptyLinePlaceholder":346},[40,631,632,634,636,639],{"class":42,"line":191},[40,633,353],{"class":352},[40,635,475],{"class":352},[40,637,638],{"class":359}," Certificate",[40,640,481],{"class":46},[40,642,643,645,647],{"class":42,"line":204},[40,644,550],{"class":486},[40,646,490],{"class":352},[40,648,506],{"class":53},[40,650,651,654,656],{"class":42,"line":212},[40,652,653],{"class":486},"  volume",[40,655,490],{"class":352},[40,657,658],{"class":53}," number\n",[40,660,661,664,666,669,671],{"class":42,"line":225},[40,662,663],{"class":486},"  unit",[40,665,490],{"class":352},[40,667,668],{"class":69}," \"MWh\"",[40,670,369],{"class":352},[40,672,673],{"class":69}," \"kWh\"\n",[40,675,676,679,681],{"class":42,"line":238},[40,677,678],{"class":486},"  period",[40,680,490],{"class":352},[40,682,683],{"class":359}," DateRange\n",[40,685,686,689,691],{"class":42,"line":249},[40,687,688],{"class":486},"  status",[40,690,490],{"class":352},[40,692,693],{"class":359}," CertificateStatus\n",[40,695,696,699,701],{"class":42,"line":255},[40,697,698],{"class":486},"  metadata",[40,700,490],{"class":352},[40,702,703],{"class":359}," CertificateMetadata\n",[40,705,706],{"class":42,"line":261},[40,707,321],{"class":46},[40,709,710],{"class":42,"line":267},[40,711,347],{"emptyLinePlaceholder":346},[40,713,714,716,718,721],{"class":42,"line":275},[40,715,353],{"class":352},[40,717,475],{"class":352},[40,719,720],{"class":359}," Pagination",[40,722,481],{"class":46},[40,724,725,728,730],{"class":42,"line":288},[40,726,727],{"class":486},"  page",[40,729,490],{"class":352},[40,731,658],{"class":53},[40,733,734,737,739],{"class":42,"line":301},[40,735,736],{"class":486},"  per_page",[40,738,490],{"class":352},[40,740,658],{"class":53},[40,742,743,746,748],{"class":42,"line":312},[40,744,745],{"class":486},"  total",[40,747,490],{"class":352},[40,749,658],{"class":53},[40,751,752],{"class":42,"line":318},[40,753,321],{"class":46},[40,755,757],{"class":42,"line":756},27,[40,758,347],{"emptyLinePlaceholder":346},[40,760,762,764,766,769],{"class":42,"line":761},28,[40,763,353],{"class":352},[40,765,475],{"class":352},[40,767,768],{"class":359}," CertificatesResponse",[40,770,481],{"class":46},[40,772,774,777,779],{"class":42,"line":773},29,[40,775,776],{"class":486},"  account",[40,778,490],{"class":352},[40,780,781],{"class":359}," Account\n",[40,783,785,788,790,792],{"class":42,"line":784},30,[40,786,787],{"class":486},"  certificates",[40,789,490],{"class":352},[40,791,638],{"class":359},[40,793,794],{"class":46},"[]\n",[40,796,798,801,803],{"class":42,"line":797},31,[40,799,800],{"class":486},"  pagination",[40,802,490],{"class":352},[40,804,805],{"class":359}," Pagination\n",[40,807,809],{"class":42,"line":808},32,[40,810,321],{"class":46},[23,812,814],{"id":813},"utility-types-for-each-use-case","Utility Types for Each Use Case",[14,816,817],{},"The API always returns the complete structure, but the application rarely needs all of it at once. TypeScript's utility types allow deriving purpose-built types from the canonical interfaces without duplicating definitions:",[31,819,821],{"className":331,"code":820,"language":333,"meta":36,"style":36},"// For list display — metadata is not needed\nexport type CertificateSummary = Pick\u003C\n  Certificate,\n  \"id\" | \"volume\" | \"unit\" | \"status\" | \"period\"\n>\n\n// For updates — only status is mutable\nexport type CertificateUpdate = Pick\u003CCertificate, \"id\"> & {\n  status: CertificateStatus\n}\n\n// For search filters — all fields are optional\nexport type CertificateFilters = Partial\u003CPick\u003CCertificate, \"status\">> & {\n  period?: Partial\u003CDateRange>\n  technology?: EnergyTechnology\n  country?: CountryCode\n}\n\n// For forms — exclude API-generated fields\nexport type CertificateForm = Omit\u003CCertificate, \"id\" | \"status\">\n",[18,822,823,828,845,852,877,882,886,891,923,931,935,939,944,979,995,1003,1011,1015,1019,1024],{"__ignoreMap":36},[40,824,825],{"class":42,"line":43},[40,826,827],{"class":340},"// For list display — metadata is not needed\n",[40,829,830,832,834,837,839,842],{"class":42,"line":50},[40,831,353],{"class":352},[40,833,356],{"class":352},[40,835,836],{"class":359}," CertificateSummary",[40,838,363],{"class":352},[40,840,841],{"class":359}," Pick",[40,843,844],{"class":46},"\u003C\n",[40,846,847,850],{"class":42,"line":60},[40,848,849],{"class":359},"  Certificate",[40,851,73],{"class":46},[40,853,854,857,859,862,864,867,869,872,874],{"class":42,"line":76},[40,855,856],{"class":69},"  \"id\"",[40,858,369],{"class":352},[40,860,861],{"class":69}," \"volume\"",[40,863,369],{"class":352},[40,865,866],{"class":69}," \"unit\"",[40,868,369],{"class":352},[40,870,871],{"class":69}," \"status\"",[40,873,369],{"class":352},[40,875,876],{"class":69}," \"period\"\n",[40,878,879],{"class":42,"line":89},[40,880,881],{"class":46},">\n",[40,883,884],{"class":42,"line":100},[40,885,347],{"emptyLinePlaceholder":346},[40,887,888],{"class":42,"line":106},[40,889,890],{"class":340},"// For updates — only status is mutable\n",[40,892,893,895,897,900,902,904,907,910,912,915,918,921],{"class":42,"line":115},[40,894,353],{"class":352},[40,896,356],{"class":352},[40,898,899],{"class":359}," CertificateUpdate",[40,901,363],{"class":352},[40,903,841],{"class":359},[40,905,906],{"class":46},"\u003C",[40,908,909],{"class":359},"Certificate",[40,911,177],{"class":46},[40,913,914],{"class":69},"\"id\"",[40,916,917],{"class":46},"> ",[40,919,920],{"class":352},"&",[40,922,481],{"class":46},[40,924,925,927,929],{"class":42,"line":121},[40,926,688],{"class":486},[40,928,490],{"class":352},[40,930,693],{"class":359},[40,932,933],{"class":42,"line":134},[40,934,321],{"class":46},[40,936,937],{"class":42,"line":147},[40,938,347],{"emptyLinePlaceholder":346},[40,940,941],{"class":42,"line":160},[40,942,943],{"class":340},"// For search filters — all fields are optional\n",[40,945,946,948,950,953,955,958,960,963,965,967,969,972,975,977],{"class":42,"line":191},[40,947,353],{"class":352},[40,949,356],{"class":352},[40,951,952],{"class":359}," CertificateFilters",[40,954,363],{"class":352},[40,956,957],{"class":359}," Partial",[40,959,906],{"class":46},[40,961,962],{"class":359},"Pick",[40,964,906],{"class":46},[40,966,909],{"class":359},[40,968,177],{"class":46},[40,970,971],{"class":69},"\"status\"",[40,973,974],{"class":46},">> ",[40,976,920],{"class":352},[40,978,481],{"class":46},[40,980,981,983,986,988,990,993],{"class":42,"line":204},[40,982,678],{"class":486},[40,984,985],{"class":352},"?:",[40,987,957],{"class":359},[40,989,906],{"class":46},[40,991,992],{"class":359},"DateRange",[40,994,881],{"class":46},[40,996,997,999,1001],{"class":42,"line":212},[40,998,597],{"class":486},[40,1000,985],{"class":352},[40,1002,602],{"class":359},[40,1004,1005,1007,1009],{"class":42,"line":225},[40,1006,607],{"class":486},[40,1008,985],{"class":352},[40,1010,612],{"class":359},[40,1012,1013],{"class":42,"line":238},[40,1014,321],{"class":46},[40,1016,1017],{"class":42,"line":249},[40,1018,347],{"emptyLinePlaceholder":346},[40,1020,1021],{"class":42,"line":255},[40,1022,1023],{"class":340},"// For forms — exclude API-generated fields\n",[40,1025,1026,1028,1030,1033,1035,1038,1040,1042,1044,1046,1048,1050],{"class":42,"line":261},[40,1027,353],{"class":352},[40,1029,356],{"class":352},[40,1031,1032],{"class":359}," CertificateForm",[40,1034,363],{"class":352},[40,1036,1037],{"class":359}," Omit",[40,1039,906],{"class":46},[40,1041,909],{"class":359},[40,1043,177],{"class":46},[40,1045,914],{"class":69},[40,1047,369],{"class":352},[40,1049,871],{"class":69},[40,1051,881],{"class":46},[23,1053,1055],{"id":1054},"generic-composables-in-vue","Generic Composables in Vue",[14,1057,1058],{},"A generic API composable eliminates the need to rewrite the same loading/error logic for every endpoint:",[31,1060,1062],{"className":331,"code":1061,"language":333,"meta":36,"style":36},"// composables/useApiQuery.ts\nimport { ref, Ref } from \"vue\"\n\ninterface ApiQueryState\u003CT> {\n  data: Ref\u003CT | null>\n  loading: Ref\u003Cboolean>\n  error: Ref\u003Cstring | null>\n  execute: () => Promise\u003Cvoid>\n}\n\nexport function useApiQuery\u003CT>(\n  fetcher: () => Promise\u003CT>,\n  options?: { immediate?: boolean },\n): ApiQueryState\u003CT> {\n  const data = ref\u003CT | null>(null) as Ref\u003CT | null>\n  const loading = ref(false)\n  const error = ref\u003Cstring | null>(null)\n\n  const execute = async () => {\n    loading.value = true\n    error.value = null\n    try {\n      data.value = await fetcher()\n    } catch (e) {\n      error.value = e instanceof Error ? e.message : \"Unknown error\"\n    } finally {\n      loading.value = false\n    }\n  }\n\n  if (options?.immediate) execute()\n\n  return { data, loading, error, execute }\n}\n",[18,1063,1064,1069,1083,1087,1103,1124,1140,1159,1182,1186,1190,1207,1227,1247,1262,1307,1327,1352,1356,1374,1385,1395,1402,1418,1429,1456,1465,1475,1479,1483,1487,1500,1504,1513],{"__ignoreMap":36},[40,1065,1066],{"class":42,"line":43},[40,1067,1068],{"class":340},"// composables/useApiQuery.ts\n",[40,1070,1071,1074,1077,1080],{"class":42,"line":50},[40,1072,1073],{"class":352},"import",[40,1075,1076],{"class":46}," { ref, Ref } ",[40,1078,1079],{"class":352},"from",[40,1081,1082],{"class":69}," \"vue\"\n",[40,1084,1085],{"class":42,"line":60},[40,1086,347],{"emptyLinePlaceholder":346},[40,1088,1089,1092,1095,1097,1100],{"class":42,"line":76},[40,1090,1091],{"class":352},"interface",[40,1093,1094],{"class":359}," ApiQueryState",[40,1096,906],{"class":46},[40,1098,1099],{"class":359},"T",[40,1101,1102],{"class":46},"> {\n",[40,1104,1105,1108,1110,1113,1115,1117,1119,1122],{"class":42,"line":89},[40,1106,1107],{"class":486},"  data",[40,1109,490],{"class":352},[40,1111,1112],{"class":359}," Ref",[40,1114,906],{"class":46},[40,1116,1099],{"class":359},[40,1118,369],{"class":352},[40,1120,1121],{"class":53}," null",[40,1123,881],{"class":46},[40,1125,1126,1129,1131,1133,1135,1138],{"class":42,"line":100},[40,1127,1128],{"class":486},"  loading",[40,1130,490],{"class":352},[40,1132,1112],{"class":359},[40,1134,906],{"class":46},[40,1136,1137],{"class":53},"boolean",[40,1139,881],{"class":46},[40,1141,1142,1145,1147,1149,1151,1153,1155,1157],{"class":42,"line":106},[40,1143,1144],{"class":486},"  error",[40,1146,490],{"class":352},[40,1148,1112],{"class":359},[40,1150,906],{"class":46},[40,1152,516],{"class":53},[40,1154,369],{"class":352},[40,1156,1121],{"class":53},[40,1158,881],{"class":46},[40,1160,1161,1164,1166,1169,1172,1175,1177,1180],{"class":42,"line":115},[40,1162,1163],{"class":359},"  execute",[40,1165,490],{"class":352},[40,1167,1168],{"class":46}," () ",[40,1170,1171],{"class":352},"=>",[40,1173,1174],{"class":359}," Promise",[40,1176,906],{"class":46},[40,1178,1179],{"class":53},"void",[40,1181,881],{"class":46},[40,1183,1184],{"class":42,"line":121},[40,1185,321],{"class":46},[40,1187,1188],{"class":42,"line":134},[40,1189,347],{"emptyLinePlaceholder":346},[40,1191,1192,1194,1197,1200,1202,1204],{"class":42,"line":147},[40,1193,353],{"class":352},[40,1195,1196],{"class":352}," function",[40,1198,1199],{"class":359}," useApiQuery",[40,1201,906],{"class":46},[40,1203,1099],{"class":359},[40,1205,1206],{"class":46},">(\n",[40,1208,1209,1212,1214,1216,1218,1220,1222,1224],{"class":42,"line":160},[40,1210,1211],{"class":359},"  fetcher",[40,1213,490],{"class":352},[40,1215,1168],{"class":46},[40,1217,1171],{"class":352},[40,1219,1174],{"class":359},[40,1221,906],{"class":46},[40,1223,1099],{"class":359},[40,1225,1226],{"class":46},">,\n",[40,1228,1229,1232,1234,1237,1240,1242,1245],{"class":42,"line":191},[40,1230,1231],{"class":486},"  options",[40,1233,985],{"class":352},[40,1235,1236],{"class":46}," { ",[40,1238,1239],{"class":486},"immediate",[40,1241,985],{"class":352},[40,1243,1244],{"class":53}," boolean",[40,1246,188],{"class":46},[40,1248,1249,1252,1254,1256,1258,1260],{"class":42,"line":204},[40,1250,1251],{"class":46},")",[40,1253,490],{"class":352},[40,1255,1094],{"class":359},[40,1257,906],{"class":46},[40,1259,1099],{"class":359},[40,1261,1102],{"class":46},[40,1263,1264,1267,1270,1272,1275,1277,1279,1281,1283,1286,1289,1292,1295,1297,1299,1301,1303,1305],{"class":42,"line":212},[40,1265,1266],{"class":352},"  const",[40,1268,1269],{"class":53}," data",[40,1271,363],{"class":352},[40,1273,1274],{"class":359}," ref",[40,1276,906],{"class":46},[40,1278,1099],{"class":359},[40,1280,369],{"class":352},[40,1282,1121],{"class":53},[40,1284,1285],{"class":46},">(",[40,1287,1288],{"class":53},"null",[40,1290,1291],{"class":46},") ",[40,1293,1294],{"class":352},"as",[40,1296,1112],{"class":359},[40,1298,906],{"class":46},[40,1300,1099],{"class":359},[40,1302,369],{"class":352},[40,1304,1121],{"class":53},[40,1306,881],{"class":46},[40,1308,1309,1311,1314,1316,1318,1321,1324],{"class":42,"line":225},[40,1310,1266],{"class":352},[40,1312,1313],{"class":53}," loading",[40,1315,363],{"class":352},[40,1317,1274],{"class":359},[40,1319,1320],{"class":46},"(",[40,1322,1323],{"class":53},"false",[40,1325,1326],{"class":46},")\n",[40,1328,1329,1331,1334,1336,1338,1340,1342,1344,1346,1348,1350],{"class":42,"line":238},[40,1330,1266],{"class":352},[40,1332,1333],{"class":53}," error",[40,1335,363],{"class":352},[40,1337,1274],{"class":359},[40,1339,906],{"class":46},[40,1341,516],{"class":53},[40,1343,369],{"class":352},[40,1345,1121],{"class":53},[40,1347,1285],{"class":46},[40,1349,1288],{"class":53},[40,1351,1326],{"class":46},[40,1353,1354],{"class":42,"line":249},[40,1355,347],{"emptyLinePlaceholder":346},[40,1357,1358,1360,1363,1365,1368,1370,1372],{"class":42,"line":255},[40,1359,1266],{"class":352},[40,1361,1362],{"class":359}," execute",[40,1364,363],{"class":352},[40,1366,1367],{"class":352}," async",[40,1369,1168],{"class":46},[40,1371,1171],{"class":352},[40,1373,481],{"class":46},[40,1375,1376,1379,1382],{"class":42,"line":261},[40,1377,1378],{"class":46},"    loading.value ",[40,1380,1381],{"class":352},"=",[40,1383,1384],{"class":53}," true\n",[40,1386,1387,1390,1392],{"class":42,"line":267},[40,1388,1389],{"class":46},"    error.value ",[40,1391,1381],{"class":352},[40,1393,1394],{"class":53}," null\n",[40,1396,1397,1400],{"class":42,"line":275},[40,1398,1399],{"class":352},"    try",[40,1401,481],{"class":46},[40,1403,1404,1407,1409,1412,1415],{"class":42,"line":288},[40,1405,1406],{"class":46},"      data.value ",[40,1408,1381],{"class":352},[40,1410,1411],{"class":352}," await",[40,1413,1414],{"class":359}," fetcher",[40,1416,1417],{"class":46},"()\n",[40,1419,1420,1423,1426],{"class":42,"line":301},[40,1421,1422],{"class":46},"    } ",[40,1424,1425],{"class":352},"catch",[40,1427,1428],{"class":46}," (e) {\n",[40,1430,1431,1434,1436,1439,1442,1445,1448,1451,1453],{"class":42,"line":312},[40,1432,1433],{"class":46},"      error.value ",[40,1435,1381],{"class":352},[40,1437,1438],{"class":46}," e ",[40,1440,1441],{"class":352},"instanceof",[40,1443,1444],{"class":359}," Error",[40,1446,1447],{"class":352}," ?",[40,1449,1450],{"class":46}," e.message ",[40,1452,490],{"class":352},[40,1454,1455],{"class":69}," \"Unknown error\"\n",[40,1457,1458,1460,1463],{"class":42,"line":318},[40,1459,1422],{"class":46},[40,1461,1462],{"class":352},"finally",[40,1464,481],{"class":46},[40,1466,1467,1470,1472],{"class":42,"line":756},[40,1468,1469],{"class":46},"      loading.value ",[40,1471,1381],{"class":352},[40,1473,1474],{"class":53}," false\n",[40,1476,1477],{"class":42,"line":761},[40,1478,258],{"class":46},[40,1480,1481],{"class":42,"line":773},[40,1482,315],{"class":46},[40,1484,1485],{"class":42,"line":784},[40,1486,347],{"emptyLinePlaceholder":346},[40,1488,1489,1492,1495,1498],{"class":42,"line":797},[40,1490,1491],{"class":352},"  if",[40,1493,1494],{"class":46}," (options?.immediate) ",[40,1496,1497],{"class":359},"execute",[40,1499,1417],{"class":46},[40,1501,1502],{"class":42,"line":808},[40,1503,347],{"emptyLinePlaceholder":346},[40,1505,1507,1510],{"class":42,"line":1506},33,[40,1508,1509],{"class":352},"  return",[40,1511,1512],{"class":46}," { data, loading, error, execute }\n",[40,1514,1516],{"class":42,"line":1515},34,[40,1517,321],{"class":46},[14,1519,1520],{},"Usage in a Vue component:",[31,1522,1524],{"className":331,"code":1523,"language":333,"meta":36,"style":36},"const {\n  data: certificates,\n  loading,\n  error,\n  execute,\n} = useApiQuery\u003CCertificatesResponse>(\n  () => $fetch(\"/api/certificates\", { params: filters.value }),\n  { immediate: true },\n)\n",[18,1525,1526,1533,1544,1550,1556,1562,1578,1596,1606],{"__ignoreMap":36},[40,1527,1528,1531],{"class":42,"line":43},[40,1529,1530],{"class":352},"const",[40,1532,481],{"class":46},[40,1534,1535,1537,1539,1542],{"class":42,"line":50},[40,1536,1107],{"class":486},[40,1538,66],{"class":46},[40,1540,1541],{"class":53},"certificates",[40,1543,73],{"class":46},[40,1545,1546,1548],{"class":42,"line":60},[40,1547,1128],{"class":53},[40,1549,73],{"class":46},[40,1551,1552,1554],{"class":42,"line":76},[40,1553,1144],{"class":53},[40,1555,73],{"class":46},[40,1557,1558,1560],{"class":42,"line":89},[40,1559,1163],{"class":53},[40,1561,73],{"class":46},[40,1563,1564,1567,1569,1571,1573,1576],{"class":42,"line":100},[40,1565,1566],{"class":46},"} ",[40,1568,1381],{"class":352},[40,1570,1199],{"class":359},[40,1572,906],{"class":46},[40,1574,1575],{"class":359},"CertificatesResponse",[40,1577,1206],{"class":46},[40,1579,1580,1583,1585,1588,1590,1593],{"class":42,"line":106},[40,1581,1582],{"class":46},"  () ",[40,1584,1171],{"class":352},[40,1586,1587],{"class":359}," $fetch",[40,1589,1320],{"class":46},[40,1591,1592],{"class":69},"\"/api/certificates\"",[40,1594,1595],{"class":46},", { params: filters.value }),\n",[40,1597,1598,1601,1604],{"class":42,"line":115},[40,1599,1600],{"class":46},"  { immediate: ",[40,1602,1603],{"class":53},"true",[40,1605,188],{"class":46},[40,1607,1608],{"class":42,"line":121},[40,1609,1326],{"class":46},[14,1611,1612,1613,1616,1617,1620,1621,1624],{},"TypeScript infers ",[18,1614,1615],{},"data"," as ",[18,1618,1619],{},"Ref\u003CCertificatesResponse | null>"," automatically — no manual casting, no ",[18,1622,1623],{},"as any",".",[23,1626,1628],{"id":1627},"typing-vue-props-against-api-interfaces","Typing Vue Props Against API Interfaces",[14,1630,1631],{},"A common mistake is defining Vue props with local types that duplicate the API interfaces. The correct approach uses the canonical types directly:",[31,1633,1635],{"className":331,"code":1634,"language":333,"meta":36,"style":36},"// components/CertificateCard.vue\n\u003Cscript setup lang=\"ts\">\nimport type { CertificateSummary, CertificateStatus } from '@/types/api'\n\nconst props = defineProps\u003C{\n  certificate: CertificateSummary\n  selected?: boolean\n}>()\n\nconst emit = defineEmits\u003C{\n  select: [id: string]\n  statusChange: [id: string, status: CertificateStatus]\n}>()\n\u003C/script>\n",[18,1636,1637,1642,1656,1670,1674,1689,1699,1709,1714,1718,1732,1752,1779,1783],{"__ignoreMap":36},[40,1638,1639],{"class":42,"line":43},[40,1640,1641],{"class":340},"// components/CertificateCard.vue\n",[40,1643,1644,1646,1649,1651,1654],{"class":42,"line":50},[40,1645,906],{"class":352},[40,1647,1648],{"class":46},"script setup lang",[40,1650,1381],{"class":352},[40,1652,1653],{"class":69},"\"ts\"",[40,1655,881],{"class":352},[40,1657,1658,1660,1662,1665,1667],{"class":42,"line":60},[40,1659,1073],{"class":352},[40,1661,356],{"class":352},[40,1663,1664],{"class":46}," { CertificateSummary, CertificateStatus } ",[40,1666,1079],{"class":352},[40,1668,1669],{"class":69}," '@/types/api'\n",[40,1671,1672],{"class":42,"line":76},[40,1673,347],{"emptyLinePlaceholder":346},[40,1675,1676,1678,1681,1683,1686],{"class":42,"line":89},[40,1677,1530],{"class":352},[40,1679,1680],{"class":53}," props",[40,1682,363],{"class":352},[40,1684,1685],{"class":359}," defineProps",[40,1687,1688],{"class":46},"\u003C{\n",[40,1690,1691,1694,1696],{"class":42,"line":100},[40,1692,1693],{"class":486},"  certificate",[40,1695,490],{"class":352},[40,1697,1698],{"class":359}," CertificateSummary\n",[40,1700,1701,1704,1706],{"class":42,"line":106},[40,1702,1703],{"class":486},"  selected",[40,1705,985],{"class":352},[40,1707,1708],{"class":53}," boolean\n",[40,1710,1711],{"class":42,"line":115},[40,1712,1713],{"class":46},"}>()\n",[40,1715,1716],{"class":42,"line":121},[40,1717,347],{"emptyLinePlaceholder":346},[40,1719,1720,1722,1725,1727,1730],{"class":42,"line":134},[40,1721,1530],{"class":352},[40,1723,1724],{"class":53}," emit",[40,1726,363],{"class":352},[40,1728,1729],{"class":359}," defineEmits",[40,1731,1688],{"class":46},[40,1733,1734,1737,1739,1742,1745,1747,1749],{"class":42,"line":147},[40,1735,1736],{"class":486},"  select",[40,1738,490],{"class":352},[40,1740,1741],{"class":46}," [",[40,1743,1744],{"class":359},"id",[40,1746,66],{"class":46},[40,1748,516],{"class":53},[40,1750,1751],{"class":46},"]\n",[40,1753,1754,1757,1759,1761,1763,1765,1767,1769,1772,1774,1777],{"class":42,"line":160},[40,1755,1756],{"class":486},"  statusChange",[40,1758,490],{"class":352},[40,1760,1741],{"class":46},[40,1762,1744],{"class":359},[40,1764,66],{"class":46},[40,1766,516],{"class":53},[40,1768,177],{"class":46},[40,1770,1771],{"class":359},"status",[40,1773,66],{"class":46},[40,1775,1776],{"class":359},"CertificateStatus",[40,1778,1751],{"class":46},[40,1780,1781],{"class":42,"line":191},[40,1782,1713],{"class":46},[40,1784,1785,1788,1791],{"class":42,"line":204},[40,1786,1787],{"class":352},"\u003C/",[40,1789,1790],{"class":46},"script",[40,1792,881],{"class":352},[14,1794,1795,1798,1799,1802,1803,1806],{},[18,1796,1797],{},"defineProps\u003CT>()"," and ",[18,1800,1801],{},"defineEmits\u003CT>()"," with generic types: no ",[18,1804,1805],{},"PropType\u003CT>"," import required, and TypeScript validates props at compile time and within the template.",[23,1808,1810],{"id":1809},"type-guards-for-api-responses","Type Guards for API Responses",[14,1812,1813],{},"Real-world APIs do not always honour their contracts. A type guard enables runtime validation without sacrificing static typing:",[31,1815,1817],{"className":331,"code":1816,"language":333,"meta":36,"style":36},"function isCertificate(value: unknown): value is Certificate {\n  return (\n    typeof value === \"object\" &&\n    value !== null &&\n    \"id\" in value &&\n    \"volume\" in value &&\n    \"status\" in value &&\n    [\"ACTIVE\", \"CANCELLED\", \"TRANSFERRED\"].includes(\n      (value as Certificate).status,\n    )\n  )\n}\n\n// Usage\nconst raw = await $fetch(\"/api/certificates/GO-2024-001\")\nif (!isCertificate(raw)) {\n  throw new Error(\"Invalid API response\")\n}\n// TypeScript now knows raw is of type Certificate\nconsole.log(raw.volume)\n",[18,1818,1819,1851,1858,1875,1887,1899,1910,1921,1947,1959,1964,1969,1973,1977,1982,2002,2019,2036,2040,2045],{"__ignoreMap":36},[40,1820,1821,1824,1827,1829,1832,1834,1837,1839,1841,1844,1847,1849],{"class":42,"line":43},[40,1822,1823],{"class":352},"function",[40,1825,1826],{"class":359}," isCertificate",[40,1828,1320],{"class":46},[40,1830,1831],{"class":486},"value",[40,1833,490],{"class":352},[40,1835,1836],{"class":53}," unknown",[40,1838,1251],{"class":46},[40,1840,490],{"class":352},[40,1842,1843],{"class":486}," value",[40,1845,1846],{"class":352}," is",[40,1848,638],{"class":359},[40,1850,481],{"class":46},[40,1852,1853,1855],{"class":42,"line":50},[40,1854,1509],{"class":352},[40,1856,1857],{"class":46}," (\n",[40,1859,1860,1863,1866,1869,1872],{"class":42,"line":60},[40,1861,1862],{"class":352},"    typeof",[40,1864,1865],{"class":46}," value ",[40,1867,1868],{"class":352},"===",[40,1870,1871],{"class":69}," \"object\"",[40,1873,1874],{"class":352}," &&\n",[40,1876,1877,1880,1883,1885],{"class":42,"line":76},[40,1878,1879],{"class":46},"    value ",[40,1881,1882],{"class":352},"!==",[40,1884,1121],{"class":53},[40,1886,1874],{"class":352},[40,1888,1889,1891,1894,1896],{"class":42,"line":89},[40,1890,63],{"class":69},[40,1892,1893],{"class":352}," in",[40,1895,1865],{"class":46},[40,1897,1898],{"class":352},"&&\n",[40,1900,1901,1904,1906,1908],{"class":42,"line":100},[40,1902,1903],{"class":69},"    \"volume\"",[40,1905,1893],{"class":352},[40,1907,1865],{"class":46},[40,1909,1898],{"class":352},[40,1911,1912,1915,1917,1919],{"class":42,"line":106},[40,1913,1914],{"class":69},"    \"status\"",[40,1916,1893],{"class":352},[40,1918,1865],{"class":46},[40,1920,1898],{"class":352},[40,1922,1923,1926,1928,1930,1933,1935,1938,1941,1944],{"class":42,"line":115},[40,1924,1925],{"class":46},"    [",[40,1927,199],{"class":69},[40,1929,177],{"class":46},[40,1931,1932],{"class":69},"\"CANCELLED\"",[40,1934,177],{"class":46},[40,1936,1937],{"class":69},"\"TRANSFERRED\"",[40,1939,1940],{"class":46},"].",[40,1942,1943],{"class":359},"includes",[40,1945,1946],{"class":46},"(\n",[40,1948,1949,1952,1954,1956],{"class":42,"line":121},[40,1950,1951],{"class":46},"      (value ",[40,1953,1294],{"class":352},[40,1955,638],{"class":359},[40,1957,1958],{"class":46},").status,\n",[40,1960,1961],{"class":42,"line":134},[40,1962,1963],{"class":46},"    )\n",[40,1965,1966],{"class":42,"line":147},[40,1967,1968],{"class":46},"  )\n",[40,1970,1971],{"class":42,"line":160},[40,1972,321],{"class":46},[40,1974,1975],{"class":42,"line":191},[40,1976,347],{"emptyLinePlaceholder":346},[40,1978,1979],{"class":42,"line":204},[40,1980,1981],{"class":340},"// Usage\n",[40,1983,1984,1986,1989,1991,1993,1995,1997,2000],{"class":42,"line":212},[40,1985,1530],{"class":352},[40,1987,1988],{"class":53}," raw",[40,1990,363],{"class":352},[40,1992,1411],{"class":352},[40,1994,1587],{"class":359},[40,1996,1320],{"class":46},[40,1998,1999],{"class":69},"\"/api/certificates/GO-2024-001\"",[40,2001,1326],{"class":46},[40,2003,2004,2007,2010,2013,2016],{"class":42,"line":225},[40,2005,2006],{"class":352},"if",[40,2008,2009],{"class":46}," (",[40,2011,2012],{"class":352},"!",[40,2014,2015],{"class":359},"isCertificate",[40,2017,2018],{"class":46},"(raw)) {\n",[40,2020,2021,2024,2027,2029,2031,2034],{"class":42,"line":238},[40,2022,2023],{"class":352},"  throw",[40,2025,2026],{"class":352}," new",[40,2028,1444],{"class":359},[40,2030,1320],{"class":46},[40,2032,2033],{"class":69},"\"Invalid API response\"",[40,2035,1326],{"class":46},[40,2037,2038],{"class":42,"line":249},[40,2039,321],{"class":46},[40,2041,2042],{"class":42,"line":255},[40,2043,2044],{"class":340},"// TypeScript now knows raw is of type Certificate\n",[40,2046,2047,2050,2053],{"class":42,"line":261},[40,2048,2049],{"class":46},"console.",[40,2051,2052],{"class":359},"log",[40,2054,2055],{"class":46},"(raw.volume)\n",[23,2057,2059],{"id":2058},"what-this-changes-in-practice","What This Changes in Practice",[14,2061,2062],{},"Investing in rigorous TypeScript modelling from the outset delivers:",[2064,2065,2066,2074,2080,2086],"ul",{},[2067,2068,2069,2073],"li",{},[2070,2071,2072],"strong",{},"Reliable autocomplete"," across all API objects in the IDE",[2067,2075,2076,2079],{},[2070,2077,2078],{},"Errors caught at compile time"," rather than discovered in production",[2067,2081,2082,2085],{},[2070,2083,2084],{},"Safe refactoring"," — renaming a field in an interface propagates the error everywhere it is used",[2067,2087,2088,2091],{},[2070,2089,2090],{},"Implicit documentation"," — the types are the source of truth for the shape of the data",[14,2093,2094],{},"The upfront cost is real, particularly on an existing project with JavaScript to migrate. However, on a new Vue.js project consuming an external API, starting with strict TypeScript from the first commit is consistently the most cost-effective decision over the medium term.",[2096,2097,2098],"style",{},"html pre.shiki code .sQ3_J, html code.shiki .sQ3_J{--shiki-dark:#E1E4E8;--shiki-default:#24292E}html pre.shiki code .s0DvM, html code.shiki .s0DvM{--shiki-dark:#79B8FF;--shiki-default:#005CC5}html pre.shiki code .sg6BJ, html code.shiki .sg6BJ{--shiki-dark:#9ECBFF;--shiki-default:#032F62}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}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 .sryI4, html code.shiki .sryI4{--shiki-dark:#6A737D;--shiki-default:#6A737D}html pre.shiki code .scx8i, html code.shiki .scx8i{--shiki-dark:#F97583;--shiki-default:#D73A49}html pre.shiki code .s-Z4r, html code.shiki .s-Z4r{--shiki-dark:#B392F0;--shiki-default:#6F42C1}html pre.shiki code .sFbx2, html code.shiki .sFbx2{--shiki-dark:#FFAB70;--shiki-default:#E36209}",{"title":36,"searchDepth":50,"depth":50,"links":2100},[2101,2102,2103,2104,2105,2106,2107,2108],{"id":25,"depth":50,"text":26},{"id":324,"depth":50,"text":325},{"id":528,"depth":50,"text":529},{"id":813,"depth":50,"text":814},{"id":1054,"depth":50,"text":1055},{"id":1627,"depth":50,"text":1628},{"id":1809,"depth":50,"text":1810},{"id":2058,"depth":50,"text":2059},"2025-06-09","Most TypeScript tutorials demonstrate trivial examples: interface User { name: string; age: number }. Production code is considerably more involved. External APIs return nested structures, conditionally optional fields, and type unions that vary by state. Here is how to model all of that cleanly, starting from a real-world case.",null,"md",{},"/en/blog/typescript-interfaces",{"title":5,"description":2110},"typescript-interfaces","en/blog/typescript-interfaces",[2119,2120,2121,2122],"TypeScript","Vue.js","API","Frontend","u8YrnjoAkeDweoJODzg5Jabq2lI1WgO2vLZk1gcLA90",1774645635810]