TypeScript 常见陷阱与高级类型技巧总结

作者:异步漫步

时间:2026-10-24

浏览:208

本文总结了 TypeScript 开发中易踩的坑及高级类型技巧。涵盖 as 断言的兼容性误解、interface 与 type 的行为差异、泛型实现函数重载时的断言需求,以及元组类型、递归类型和联合类型拆分等进阶操作,帮助开发者深入理解 TS 类型系统。

文章目录

本文旨在记录在 TypeScript 开发过程中遇到的一些耗时且值得注意的问题。

(先吐槽一下:StackOverflow 上什么都有,但百度确实不太好用。)

常见陷阱

关于 as 断言的兼容性存在误解,例如 "a" as "b" 这种代码并不会报错。

interface 和 type 的行为存在不一致之处(初次遇到时还以为自己写错了类型,一脸懵逼):


type Type = {
  key: "value"
}
interface Interface {
  key: "value"
}

type 似乎没差别都是true = Type extends Interface ? Type extends Interface ? true : false : false

type 坑点 = {
  [key: string]: 坑点
} | string

type 测试<T> = T extends 坑点 ? true : false
type 这个是true = 测试<Type>
type 这个是false = 测试<Interface>

GitHub 上官方有说明,这是故意留下的一个坑。原因是 interface 具有可扩展性(同名自动合并),因此不便进行检测。

在使用泛型实现函数重载效果时,由于泛型不具备具体约束,在函数实现中经常需要使用 as 强制断言。


//差不多这意思,下面的代码懒得实际测了🙃

//fns是个函数索引表,TFns是索引表的const类型
function 重载失败<T extends keyof TFns>(fn:T, params: Parameters<fns[T]>){
    fns[fn](...params)//在实现中联合类型不会缩小,所以会报错
    //错误应该像是 不能将方法1的参数传给方法2 这种
}
//但外部使用时,符合类型的语义也没啥事

扩展运算符的行为并不符合直观感受:[...string[], number] 这种类型在使用时符合阅读直觉(要求数组末尾是 number 元素),但 [...string[], null, ...object[], number] 这种写法不行,它不会按顺序检查也不会报错。新版 TypeScript 增加了禁止连续解构的规则,这种类型直接不允许编写。

其实这里有解决办法,但是写出来的类型简直没法看(几十行,包含大量 extends 充当类型的 if 判断),就不贴代码了。下面贴代码:


//需要的类型:[...number[], "middle-element", ...boolean[]] 
//上面的写法是无效的,只是示意下面的类型代码是干什么用的(实现上面示意的类型约束)

type Elem = number | boolean | "middle-element";

type Last<T extends any[]> = T extends [infer _]
  ? never
  : T extends [...infer _, infer Tl]
  ? Tl
  : never

type HandleEmpty<T extends any[], Data> = T['length'] extends 0 ? never : Data

type Validation<Params extends any[], Cache extends Elem[] = []> =
  Params extends []
  ? Cache['length'] extends 0
  ? never
  : Cache
  : Params extends [infer Fst, ...infer Rest]
  ? Cache extends []
  ? Fst extends number
  ? HandleEmpty<Rest, Validation<Rest, [...Cache, Fst]>>
  : never
  : Fst extends number
  ? Last<Cache> extends number
  ? HandleEmpty<Rest, Validation<Rest, [...Cache, Fst]>>
  : never
  : Fst extends "middle-element"
  ? Last<Cache> extends number
  ? HandleEmpty<Rest, Validation<Rest, [...Cache, Fst]>>
  : never
  : "middle-element" extends Cache[number]
  ? Fst extends boolean
  ? Validation<Rest, [...Cache, Fst]>
  : never
  : never
  : never

type IsNever<T> = [T] extends [never] ? true : false;

function check<
  Params extends Elem[],
  IsValid extends Validation<Params>
>(...arr: IsNever<IsValid> extends true ? [never] : [...Params]) {
  return arr
}

const 正常 = check(1, 'middle-element', false)
const 报错 = check(false, "middle-element", 2)

进阶操作

对象名重映射:


//{ "new-a":any; "new-b":any }
type 重映射 = {
    [K in "a" | "b" as `new-${K}`]: any
}

联合类型的拆分:使用 infer 关键字可以实现对联合类型的拆分。


//"a1"|"b2"
type 拆分成功<_Keys = keyof { a: 1, b: 2 }> = _Keys extends infer K ?
    `${Extract<K, string>}${{ a: 1, b: 2 }[Extract<K, _Keys>]}`
    : never

//注意:(截止ts4.4.4)直接`keyof Obj extends infer K`无法分割联合类型,原因不明(懒得查😁)。
//结果是"a1"|"a2"|"b1"|"b2"
type 拆分失败 = keyof { a: 1, b: 2 } extends infer K ?
    `${Extract<K, string>}${{ a: 1, b: 2 }[Extract<K, "a" | "b">]}`
    : never

元组类型:

  • 实际(非类型)参数有时候需要通过 as const 明确定义为元组类型。
  • 元组类型可以通过元组["length"]获取准确的长度,而不是 number。
  • 元组类型在通过泛型参数使用时,有时候需要通过加个[]|写成元组 extends []|any[]这种方式来避免被解析为普通的不定长数组类型。

递归类型:使用...infer More可以实现对数组类型的递归。


type 转换器<T> = T extends string ? "str" : null
//进去是个[string,number,string],出来就会是["str",null,"str"]
type 递归<
        输入源 extends any[],
        内部的类型缓存 extends any[] = []
    > = 输入源 extends [any, ...infer 剩余元素] ?
        递归<剩余元素, [...内部的类型缓存, 转换器<输入源[0]>]>
        : 输入源

零碎知识

  • & 可以代替 extends 对 type 使用,interface 除了可以合并同名的类型,其它的没啥差别了。
  • ts具有丰富的内建类型,挑几个例子:
    • ReturnType<函数类型>,获取函数类型的返回值的类型。
    • Uncapitalize<字符串>,将输入的字符串类型的首字母锁定为小写(其它还有首字母大写、全小写、全大写)。

新手建议去官网翻文档。

入了 ts 坑后,可以没事关注下版本更新带来的新特性(玩法)。

总结

到此这篇关于在TypeScript上费过时间的地方总结的文章就介绍到这了,更多相关ts费时间的地方内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章