跳转到内容

02 TypeScript

5,617 字 20 分钟

属性 1
series
Vue3.5 + Electron + 大模型跨平台实战

基础

  • JavaScript 中每个值都有一组可观察的行为(能不能调用、有哪些属性……);类型就是描述「哪些值可以传给某个函数、哪些会崩溃」的概念。
  • JavaScript 只提供动态类型——运行代码才能看到会发生什么;typeof 只能在运行时识别 string / number 等原始类型,函数等复杂结构没有对应的运行时机制。
  • 替代方案:用静态类型系统在代码运行之前预测它的行为。

静态类型检查

  • 静态类型系统描述了程序运行时值的形状和行为;TypeScript 这样的类型检查器利用这些信息,在运行前就告诉你哪里可能出轨。
  • 例:const message = "hello!"; message(); —— TS 直接报 This expression is not callable. Type 'String' has no call signatures.,无需等到运行时的 TypeError
ts
// JavaScript 只提供“动态类型”——运行代码才能看到会发生什么
const 
message
= 'Hello World!'
console
.
log
(
message
.
toLowerCase
()) // hello world!
try { // TS 静态检查本会在编译期拦下这行(This expression is not callable), // 这里用 as any 故意绕过检查,还原纯 JS 的运行时行为 ;(
message
as any)()
} catch (
e
) {
console
.
log
(`${(
e
as Error).
name
}: ${(
e
as Error).
message
}`) // TypeError: message is not a function
} // “非异常失败”:访问不存在的属性,JS 不抛错而是返回 undefined——TS 则会标记为错误 const
user
= {
name
: 'Daniel',
age
: 26 }
console
.
log
((
user
as any).location) // undefined

非异常失败

  • 运行时异常是 ECMAScript 规范明确规定的行为(调用不可调用的东西必须抛错);但访问不存在的属性规范却规定返回 undefined 而不抛错。
  • 静态类型系统要自己决断哪些代码该标记为错误——即使它是不会立即抛错的“合法”JavaScript。TS 对这类代码同样报错,以捕获真正的 bug:
ts
const 
announcement
= "Hello World!";
announcement
.toLocaleLowercase(); // 拼写错误(TS 能立即发现)
Property 'toLocaleLowercase' does not exist on type '"Hello World!"'. Did you mean 'toLocaleLowerCase'?
announcement
.toLocalLowerCase(); // 也是拼写错误
Property 'toLocalLowerCase' does not exist on type '"Hello World!"'. Did you mean 'toLocaleLowerCase'?
function
flipCoin
() {
return Math.random < 0.5; // 忘写括号——报错标注由 twoslash 构建期实时渲染
Operator '<' cannot be applied to types '() => number' and 'number'.
} const
value
=
Math
.
random
() < 0.5 ? "a" : "b";
if (
value
!== "a") {
// ... } else if (value === "b") {
This comparison appears to be unintentional because the types '"a"' and '"b"' have no overlap.
// 不可达 —— 基本逻辑错误 }

类型工具

  • 类型检查器不但能捕获 bug,还能预防 bug:它掌握了「哪些属性可以访问」的信息,因此可以在你输入时建议候选属性。
  • TypeScript 驱动了编辑器的核心体验:代码补全、错误提示、快速修复(quick fixes)、重构、跳转定义、查找引用——全部构建在类型检查器之上且跨平台。

tsc:TypeScript 编译器

  • 安装与运行:npm install -g typescripttsc hello.ts(也可用 npx 从本地 node_modules 运行)。
  • 无错误时 tsc 静默完成并产出编译后的 hello.js——没有输出就是好消息。
  • 带错误时仍然产出 JS:这是 TS 的核心价值观——多数时候你比 TypeScript 更懂。类型检查是一种权衡,不该在迁移旧 JS 项目时挡住本来能跑的代码。
  • 想更严格:tsc --noEmitOnError hello.ts,报错时不更新输出文件。

显式类型与类型推断

  • 类型注解function greet(person: string, date: Date) 读作「greet 接受 string 类型的 person 和 Date 类型的 date」。
  • 经典坑:直接调用 Date() 返回的是 stringnew Date() 才是 Date 对象——TS 会在传参时立刻指出。
  • 类型推断let msg = "hello there!" 自动推断为 string——推断结果相同时就别写注解,这是特性不是偷懒。
ts
// 类型注解:greet 接受 string 类型的 person 和 Date 类型的 date
function 
greet
(
person
: string,
date
: Date) {
console
.
log
(`Hello ${
person
}, today is ${
date
.
toDateString
()}!`)
} // 坑:Date() 直接调用返回 string,new Date() 才返回 Date 对象 // greet('Maddison', Date()) // 编译错误:Argument of type 'string' is not assignable to parameter of type 'Date'
greet
('Maddison', new
Date
())
// 类型推断:没有注解时 TypeScript 也能“推”出类型 let
msg
= 'hello there!' // 推断为 string —— 推断结果相同时就别写注解
console
.
log
(
msg
.
toUpperCase
()) // HELLO THERE!
// msg = 42 // 编译错误:Type 'number' is not assignable to type 'string'

类型擦除与降级

  • 类型注解不是 JavaScript 的一部分,没有运行时能直接执行 TS——所以才需要编译器:编译时类型注解被完全擦除
  • 记住:类型注解永远不会改变程序的运行时行为。
  • 降级(downleveling):TS 能把新版 ECMAScript 语法重写为旧版(如模板字符串 → "Hello ".concat(...));默认 target 是 ES5(非常古老),绝大多数场景可以放心指定 --target es2015 或更高。

严格性

  • 默认体验是宽松的:类型可选、推断取最宽松类型、不检查 null / undefined——适合 JS 迁移的第一步。
  • 严格性设置把类型检查从“开关”变成“旋钮”:拧得越高检查越多;新代码库应始终打开 strict(CLI --strict 或 tsconfig "strict": true 一键全开,也可逐项控制)。
  • 最重要的两个 flag:
    • noImplicitAny:类型被隐式推断为 any 时报错——any 越多,用 TS 的意义越小。
    • strictNullChecks:默认 null / undefined 可赋给任何类型,忘记处理它们是无数 bug 之源(“十亿美元错误”);开启后必须显式处理 null / undefined

日常类型

原始类型、数组与 any

  • 三个最常用的原始类型:stringnumber(JS 没有 int/float 之分,一切都是 number)、boolean——永远用小写;大写的 String / Number / Boolean 指向很少用到的特殊内置类型。
  • 数组:number[](等价于 Array<number>,泛型语法后续章节讲);⚠️ [number]元组,是另一回事。
  • any:不希望某个值引发类型检查错误时使用——访问任意属性、当函数调用、随意赋值都不报错,等于关闭后续所有类型检查noImplicitAny 可把隐式 any 变成错误。
ts
let 
obj
: any = {
x
: 0 };
// 以下都不会报编译错误——使用 any 即假定你比 TS 更了解环境
obj
.foo();
obj
();
obj
.bar = 100;
obj
= "hello";
const
n
: number =
obj
;

变量与函数的类型注解

  • 变量注解写在变量名后面(TS 不用 int x = 0 这种“类型在左”风格);多数情况不需要——TS 会根据初始化器自动推断。刚上手时试着少写注解,你会惊讶 TS 需要的注解有多少。
  • 参数注解function greet(name: string)——传参时被检查;即使参数没有注解,TS 仍会检查实参个数是否正确。
  • 返回值注解:写在参数列表后 (): number——通常也可省略(由 return 语句推断);显式写通常是为了文档化、防止意外变更或个人偏好。异步函数的返回值注解用 Promise<number>
  • 上下文类型化(contextual typing):匿名函数出现在 TS 能确定调用方式的位置时,参数自动获得类型(如 forEach 回调的参数)——函数所处的上下文决定了它的类型。
ts
// 三个最常用的原始类型:string、number、boolean(永远用小写——大写的 String 等是特殊内置类型)
let 
myName
: string = 'Alice' // 显式注解:注解总是写在被注解者“后面”
let
age
= 42 // 推断为 number —— 能推断就别写注解
let
isDone
= false // 推断为 boolean
// 数组:number[](等价于 Array<number>);注意 [number] 是元组,另一回事 const
list
: number[] = [1, 2, 3]
const
names
:
Array
<string> = ['Alice', 'Bob', 'Eve']
// 上下文类型化(contextual typing):TS 根据 forEach 的类型推断参数 s 是 string
names
.
forEach
((
s
) => {
console
.
log
(
s
.
toUpperCase
()) // ALICE / BOB / EVE
})
console
.
log
(
myName
,
age
,
isDone
,
list
.
length
) // Alice 42 false 3

对象类型与可选属性

  • 对象类型直接列出属性和类型:{ x: number; y: number }——分隔符 ,; 均可,末尾分隔符可选;属性不写类型则视为 any
  • 可选属性:属性名后加 ?;JS 里访问不存在的属性得到 undefined 而非报错,因此读取可选属性前必须检查 undefined(或用现代语法 obj.last?.toUpperCase())。
ts
// 对象类型:列出属性和类型(分隔符 , 或 ; 均可;属性不写类型则视为 any)
function 
printCoord
(
pt
: {
x
: number;
y
: number }) {
console
.
log
(`x: ${
pt
.
x
}, y: ${
pt
.
y
}`)
}
printCoord
({
x
: 3,
y
: 7 }) // x: 3, y: 7
// 可选属性:属性名后加 ?;“读取”时必须先处理 undefined function
printName
(
obj
: {
first
: string;
last
?: string }) {
// console.log(obj.last.toUpperCase()) // 编译错误:'obj.last' is possibly 'undefined'
console
.
log
(
obj
.
last
?.
toUpperCase
() ?? '(没有姓氏)') // 现代写法:可选链
}
printName
({
first
: 'Bob' }) // (没有姓氏)
printName
({
first
: 'Alice',
last
: 'Alisson' }) // ALISSON

联合类型与收窄

  • 联合类型 number | string:值可以是成员中的任意一个;提供值容易(匹配任一成员即可),使用值时 TS 只允许对每个成员都合法的操作。
  • 收窄(narrowing):TS 根据代码结构推断出更具体的类型——typeof id === "string" 分支里 id 就是 stringArray.isArray(x) 同理;else 分支自动排除已收窄的成员。
  • 所有成员都有的公共方法(如 string 和数组都有 slice)可以不收窄直接用
  • 为什么类型的联合却只能用属性的交集?——名字来自类型理论:number | string 是对两个类型的值集合取并集;两个集合合并后,对每个成员都成立的事实只剩下两个集合事实的交集(戴帽子的高个子 ∪ 戴帽子的西语者 → 只能确定人人戴帽子)。
ts
// 联合类型:值可以是成员中的“任意一个”;TS 只允许对“每个成员都合法”的操作
function 
printId
(
id
: number | string) {
// console.log(id.toUpperCase()) // 编译错误:Property 'toUpperCase' does not exist on type 'number' if (typeof
id
=== 'string') {
console
.
log
(
id
.
toUpperCase
()) // 这个分支里 id 收窄为 string
} else {
console
.
log
(
id
) // 这里 id 是 number
} }
printId
(101) // 101
printId
('abc202') // ABC202
// Array.isArray 也能收窄;else 分支无需处理——排除 string[] 后必然是 string function
welcomePeople
(
x
: string[] | string) {
if (
Array
.
isArray
(
x
)) {
console
.
log
(`Hello, ${
x
.
join
(' and ')}`)
} else {
console
.
log
(`Welcome lone traveler ${
x
}`)
} }
welcomePeople
(['Alice', 'Bob']) // Hello, Alice and Bob
welcomePeople
('Eve') // Welcome lone traveler Eve
// 所有成员的公共方法可以不收窄直接用(返回类型推断为 number[] | string) function
getFirstThree
(
x
: number[] | string) {
return
x
.
slice
(0, 3)
}
console
.
log
(
getFirstThree
([1, 2, 3, 4, 5])) // [ 1, 2, 3 ]
console
.
log
(
getFirstThree
('abcdef')) // abc

类型别名与接口

  • 类型别名 type:给任何类型起名字(对象类型、联合类型都行);别名只是别名——不会创造“同一类型的不同版本”,用别名与直接写被别名的类型完全等价。
  • 接口 interface:命名对象类型的另一种方式;TS 只关心传给函数的值的结构——这就是结构化类型系统(structurally typed)。
  • 两者几乎可以自由互换,关键区别:
    • type 创建后不能再改interface 永远可以扩展(同名声明自动合并,可给已有接口加新字段)。
    • 扩展语法:interface 用 extends,type 用交叉类型 &extends 对编译器通常更高效)。
    • interface 只能声明对象形状,不能重命名原始类型type UserInputSanitizedString = string 只有 type 能做)。
    • 错误信息中 interface 永远显示原名;type 别名有时会被展开成匿名类型。
  • 经验法则:先用 interface,直到需要 type 的特性
ts
// 类型别名:给“任何类型”起名字——对象类型、联合类型都行
type 
ID
= number | string;
type
Point
= {
x
: number;
y
: number };
// 接口声明:命名对象类型的另一种方式 interface Animal {
name
: string;
} // TS 是“结构化类型系统”:只关心传入值的结构是否符合 function
printPoint
(
pt
:
Point
) {
console
.
log
(`x: ${
pt
.
x
}, y: ${
pt
.
y
}`);
}
printPoint
({
x
: 100,
y
: 100 }); // x: 100, y: 100
// 扩展:interface 用 extends,type 用交叉类型 & interface Bear extends Animal {
honey
: boolean;
} type
Fish
= Animal & {
swim
: boolean };
const
bear
: Bear = {
name
: "Winnie",
honey
: true };
const
fish
:
Fish
= {
name
: "Nemo",
swim
: true };
console
.
log
(
bear
); // { name: 'Winnie', honey: true }
console
.
log
(
fish
); // { name: 'Nemo', swim: true }
// 别名只是别名:不会创造“同类型的不同版本” const
id1
:
ID
= 101;
const
id2
:
ID
= "abc";
console
.
log
(
id1
,
id2
); // 101 abc

类型断言

  • 当你比 TS 更了解某个值的类型时用类型断言document.getElementById("main_canvas") as HTMLCanvasElement(等价的尖括号写法 <HTMLCanvasElement>expr.tsx 文件中不可用)。
  • 断言与注解一样编译期被移除,不影响运行时——断言错了不会抛异常也不会得到 null,只会在后续使用时爆雷。
  • TS 只允许断言到更具体更不具体的类型,阻止 "hello" as number 这类“不可能”的强转;确实需要复杂强转时用双重断言 expr as any as T(或经由 unknown)。

字面量类型

  • 除了宽泛的 string / number,类型位置还能写具体的字符串和数字——对应 const(只能是一个值)与 let / var(可变)在类型系统的映照。
  • 单个字面量类型没什么用,组合成联合才见威力:alignment: "left" | "right" | "center"、返回值 -1 | 0 | 1、混搭非字面量 Options | "auto"
  • boolean 本身其实就是 true | false 的联合别名。
  • 字面量推断:对象属性会被推断为 string 而非 "GET"(属性之后可能被改写——类型要同时约束);解法:
    1. 单点断言:method: "GET" as "GET" 或调用处 req.method as "GET"
    2. as const:把整个对象转为字面量类型——“类型系统层面的 const”。
ts
// 字面量联合:只接受一组已知的值
function 
printText
(
s
: string,
alignment
: 'left' | 'right' | 'center') {
console
.
log
(`[${
alignment
}] ${
s
}`)
}
printText
('Hello, world', 'left') // [left] Hello, world
// printText("G'day, mate", 'centre') // 编译错误:'"centre"' 不可赋给 '"left" | "right" | "center"' // 数字字面量:约束返回值只能是 -1 | 0 | 1 function
compare
(
a
: string,
b
: string): -1 | 0 | 1 {
return
a
===
b
? 0 :
a
>
b
? 1 : -1
}
console
.
log
(
compare
('a', 'b')) // -1
// 字面量推断:对象属性被推断为 string 而非 "GET"(因为属性之后可能被改写) function
handleRequest
(
url
: string,
method
: 'GET' | 'POST') {
console
.
log
(
method
,
url
)
} // const req1 = { url: 'https://example.com', method: 'GET' } // handleRequest(req1.url, req1.method) // 编译错误:string 不可赋给 "GET" | "POST" // 解法:as const 把整个对象转为字面量类型(“类型系统层面的 const”) const
req
= {
url
: 'https://example.com',
method
: 'GET' } as
const
handleRequest
(
req
.
url
,
req
.
method
) // GET https://example.com

null、undefined 与非空断言

  • TS 有同名的 null / undefined 类型,行为取决于 strictNullChecks
    • off:null/undefined 可赋给任何类型、可随意访问——bug 大源头,能开就开
    • on:使用前必须检验(与检查可选属性一样,靠收窄)。
  • 非空断言 !(后缀):不做任何显式检查地移除 null / undefined——它也是类型断言,不改变运行时行为,只在确定值不可能为空时使用。
ts
// strictNullChecks 开启时:使用前必须先检验 null / undefined(同样靠收窄)
function doSomething(x: string | null) {
  if (x === null) {
    console.log('得到 null,什么都不做')
  } else {
    console.log(`Hello, ${x.toUpperCase()}`)
  }
}
doSomething('world') // Hello, WORLD
doSomething(null) // 得到 null,什么都不做

// 非空断言 !:不做任何检查地移除 null / undefined —— 仅在确定不为空时使用
function liveDangerously(x?: number | null) {
  console.log(x!.toFixed()) // 断言错了不会有类型异常,而是运行时崩溃
}
liveDangerously(3.14159) // 3

// 不常见的原始类型:bigint 与 symbol
const oneHundred: bigint = 100n
const firstName = Symbol('name')
const secondName = Symbol('name')
console.log(oneHundred) // 100n
console.log(firstName === (secondName as symbol)) // false —— 每个 Symbol 全局唯一

枚举与不常见的原始类型

  • 枚举(Enums):TS 给 JS 额外添加的语言和运行时特性(不是纯类型层面的扩展)——知道它存在即可,没想清楚之前先别用
  • bigint(ES2020+):超大整数,BigInt(100) 或字面量 100n
  • symbolSymbol("name") 创建全局唯一引用——两个同描述的 Symbol 也永不相等(TS 会直接报"no overlap")。

收窄

  • TS 把类型分析叠加在 JS 的运行时控制流结构上if/else、三元、循环、真值检查……);这些特殊检查叫类型守卫(type guard),把类型精炼到比声明更具体的过程叫收窄(narrowing)
  • 设计哲学:写起来就像普通 JavaScript——不用为了类型安全扭曲代码。

typeof 守卫与真值收窄

  • typeof 返回固定的字符串集合:"string" / "number" / "bigint" / "boolean" / "symbol" / "undefined" / "object" / "function"——注意没有 "null"
  • 历史坑:typeof null === "object"!TS 知道这个怪癖——只用 typeof strs === "object" 检查时,strs 只能收窄到 string[] | null
  • 真值收窄if 会先把条件强转为 boolean;假值共 7 个(0NaN""0nnullundefinedfalse)。if (strs && typeof strs === "object") 可补上 null 检查。
  • ⚠️ 对原始类型做真值检查容易出错:把整个函数体包进 if (strs)漏掉空字符串的处理——TS 不会拦你,这类问题交给 linter。
  • 布尔否定 ! 从否定分支过滤:if (!values) return values; 之后 values 就是 number[]
ts
// typeof 类型守卫:收窄后每个分支里 padding 的类型都是确定的
function 
padLeft
(
padding
: number | string,
input
: string): string {
if (typeof
padding
=== 'number') {
return ' '.
repeat
(
padding
) +
input
// 这里 padding: number
} return
padding
+
input
// 这里 padding: string(控制流分析已排除 number)
}
console
.
log
(
padLeft
(4, 'hi')) // " hi"
console
.
log
(
padLeft
('>> ', 'hi')) // ">> hi"
// 历史坑:typeof null === "object" —— TS 知道这个怪癖 function
printAll
(
strs
: string | string[] | null) {
if (
strs
&& typeof
strs
=== 'object') {
// 只用 typeof 检查时 strs 仅能收窄到 string[] | null;真值检查再排除 null for (const
s
of
strs
)
console
.
log
(
s
)
} else if (typeof
strs
=== 'string') {
console
.
log
(
strs
)
} }
printAll
(['a', 'b']) // a / b
printAll
('single') // single
printAll
(null) // (无输出,安全跳过)
// 布尔否定 ! 从“否定分支”里过滤 function
multiplyAll
(
values
: number[] | undefined,
factor
: number) {
if (!
values
) return
values
// 这里 values: undefined
return
values
.
map
((
x
) =>
x
*
factor
) // 这里 values: number[]
}
console
.
log
(
multiplyAll
([1, 2, 3], 10)) // [ 10, 20, 30 ]
console
.
log
(
multiplyAll
(
undefined
, 10)) // undefined

相等性、in 与 instanceof 收窄

  • 相等性收窄switch=== / !== / == / != 都能收窄——x === y 成立时二者只能是共同的成员类型。
  • 宽松相等的妙用:!= null 一举排除 nullundefined== undefined 同理)。
  • in 操作符"swim" in animal 按“是否拥有该属性”划分联合成员;可选属性会同时出现在两个分支(可游可飞的 Human 两边都在)。
  • instanceof:检查原型链(x instanceof FooFoo.prototype 是否在 x 的原型链上),适合 new 出来的值(如 Date)。
ts
// 相等性收窄:x === y 成立时,二者只可能是共同的 string
function 
example
(
x
: string | number,
y
: string | boolean) {
if (
x
===
y
) {
console
.
log
(
x
.
toUpperCase
(),
y
.
toLowerCase
()) // 都收窄为 string
} else {
console
.
log
(
x
,
y
) // x: string | number / y: string | boolean
} }
example
('Hi', 'Hi') // HI hi
example
(1, true) // 1 true
// 宽松相等 != null 一举排除 null 和 undefined interface Container {
value
: number | null | undefined
} function
multiplyValue
(
container
: Container,
factor
: number) {
if (
container
.
value
!= null) {
console
.
log
(
container
.
value
*
factor
) // value 收窄为 number
} }
multiplyValue
({
value
: 5 }, 2) // 10
multiplyValue
({
value
: null }, 2) // (无输出)
// in 操作符收窄:检查对象(或原型链)上是否有某属性 type
Fish
= {
swim
: () => void }
type
Bird
= {
fly
: () => void }
function
move
(
animal
:
Fish
|
Bird
) {
if ('swim' in
animal
) return
animal
.
swim
()
return
animal
.
fly
() // 可选属性会同时出现在两个分支(如可游可飞的 Human)
}
move
({
swim
: () =>
console
.
log
('游泳') }) // 游泳
move
({
fly
: () =>
console
.
log
('飞行') }) // 飞行
// instanceof 收窄:检查原型链,适合 new 出来的值 function
logValue
(
x
: Date | string) {
if (
x
instanceof
Date
) {
console
.
log
(
x
.
toISOString
().
slice
(0, 10)) // x: Date
} else {
console
.
log
(
x
.
toUpperCase
()) // x: string
} }
logValue
(new
Date
('2026-07-25')) // 2026-07-25
logValue
('hello') // HELLO

赋值与控制流分析

  • 赋值收窄:赋值后 TS 按右侧收窄左侧的观察类型;但可赋值性始终对照声明类型——let x = 10 或 "hello" 声明为 string | number,之后赋 stringnumber 都合法,赋 boolean 报错。
  • 控制流分析:基于可达性——if 块内 return 后,后续代码中该类型成员被移除;控制流可以分裂又合流,同一变量在不同位置观察到不同类型。
ts
// 赋值收窄:观察到的类型随赋值变化,但“可赋值性”始终对照声明类型
let 
x
=
Math
.
random
() < 0.5 ? 10 : 'hello world!' // 声明类型:string | number
x
= 1
console
.
log
(
x
) // 这里观察到 x: number
x
= 'goodbye!'
console
.
log
(
x
) // 这里观察到 x: string —— 仍合法,因为声明类型包含 string
// x = true // 编译错误:boolean 不在声明类型 string | number 里 // 控制流分析:控制流分裂又合流,同一变量在不同位置可以观察到不同类型 function
example2
() {
let
y
: string | number | boolean
y
=
Math
.
random
() < 0.5
console
.
log
(
y
) // y: boolean
if (
Math
.
random
() < 0.5) {
y
= 'hello'
console
.
log
(
y
.
toUpperCase
()) // y: string
} else {
y
= 100
console
.
log
(
y
.
toFixed
()) // y: number
} return
y
// 合流后 y: string | number(boolean 已不可能到达这里)
}
example2
()

类型谓词

  • 直接控制类型变化时,定义返回类型为类型谓词的函数:function isFish(pet: Fish | Bird): pet is Fish——参数名 is 类型,参数名必须来自当前函数签名。
  • 调用 isFish(pet) 后:if 分支里 petFishelse 分支里 TS 也知道必然是 Bird
  • 类型守卫可直接喂给 filter 得到 Fish[];类还能用 this is Type 收窄自身;此外还有断言函数(assertion functions)。
ts
// 类型谓词(type predicate):返回类型写成“参数名 is 类型”的自定义类型守卫
type 
Fish
= {
name
: string;
swim
: () => void }
type
Bird
= {
name
: string;
fly
: () => void }
function
isFish
(
pet
:
Fish
|
Bird
):
pet
is
Fish
{
return (
pet
as
Fish
).
swim
!==
undefined
} const
zoo
: (
Fish
|
Bird
)[] = [
{
name
: 'sharkey',
swim
: () =>
console
.
log
('sharkey 在游') },
{
name
: 'sparrow',
fly
: () =>
console
.
log
('sparrow 在飞') },
{
name
: 'goldie',
swim
: () =>
console
.
log
('goldie 在游') },
] for (const
pet
of
zoo
) {
if (
isFish
(
pet
))
pet
.
swim
() // if 分支:Fish
else
pet
.
fly
() // else 分支:TS 知道“不是 Fish 就必然是 Bird”
} // 类型守卫还能喂给 filter:直接得到 Fish[] const
underWater
:
Fish
[] =
zoo
.
filter
(
isFish
)
console
.
log
(
underWater
.
map
((
f
) =>
f
.
name
)) // [ 'sharkey', 'goldie' ]

可辨识联合

  • 单类型的可选属性编码(kind: "circle" | "square" + radius? + sideLength?)的问题:检查了 kind === "circle" 后 TS 仍不知道 radius 一定存在,只能用易错的 ! 断言硬压。
  • 正确编码:拆成独立类型再联合——每个成员都带字面量类型的公共属性(判别式 discriminant),各自的字段声明为必需:
ts
interface Circle {
  
kind
: "circle";
radius
: number;
} interface Square {
kind
: "square";
sideLength
: number;
} type
Shape
= Circle | Square;
  • 检查判别式(ifswitch)即可把 shape 收窄到具体成员——把你知道的信息传达给类型检查器,就能写出与 JS 无异但类型安全的代码。
  • 适用场景远不止图形:网络消息收发(客户端/服务端通信)、状态管理框架中的 mutation 编码等任何“消息方案”。

never 与穷尽性检查

  • 收窄到排除了所有可能性时,剩下的就是 never——表示不应存在的状态。
  • never 可赋给任何类型;但never 自身外没有类型能赋给 never——利用这一点在 switchdefault 里做穷尽性检查const _exhaustiveCheck: never = shape——将来给联合新增成员而忘了加 case,这行立刻编译报错。
ts
// 可辨识联合:每个成员都带“字面量类型的公共属性” kind(判别式)
interface Circle {
  
kind
: 'circle'
radius
: number // 拆成独立类型后,radius 是必需属性,不再需要 ?
} interface Square {
kind
: 'square'
sideLength
: number
} type
Shape
= Circle | Square
// 检查判别式即可收窄到具体成员——完全不需要非空断言 ! function
getArea
(
shape
:
Shape
): number {
switch (
shape
.
kind
) {
case 'circle': return
Math
.
PI
*
shape
.
radius
** 2 // shape: Circle
case 'square': return
shape
.
sideLength
** 2 // shape: Square
default: { // 穷尽性检查:所有成员都处理过时 shape 收窄为 never,才允许赋给 never // 将来给 Shape 新增成员而忘了加 case,这一行会立刻编译报错 const
_exhaustiveCheck
: never =
shape
return
_exhaustiveCheck
} } }
console
.
log
(
getArea
({
kind
: 'circle',
radius
: 1 }).
toFixed
(2)) // 3.14
console
.
log
(
getArea
({
kind
: 'square',
sideLength
: 3 })) // 9

函数进阶

函数类型表达式与签名

  • 函数类型表达式(a: string) => void——语法类似箭头函数;⚠️ 参数名是必需的(string) => void 表示"参数名为 string、类型为 any",完全不是一回事。
  • 调用签名:函数还能带属性时,把签名写进对象类型——参数表与返回类型之间用 : 而不是 =>
  • 构造签名new (s: string): SomeObject——描述"可以被 new 的东西";调用签名与构造签名可以在同一类型里任意组合(如 Date 加不加 new 都能调)。
ts
// 函数类型表达式:语法类似箭头函数——参数名是必需的!
// (a: string) => void 表示“一个参数 a 为 string、无返回值的函数”
// ⚠️ (string) => void 则表示“参数名为 string、类型为 any”——完全不是一回事
type 
GreetFunction
= (
a
: string) => void
function
greeter
(
fn
:
GreetFunction
) {
fn
('Hello, World')
}
greeter
((
s
) =>
console
.
log
(
s
)) // Hello, World
// 调用签名:函数除了可调用还能带属性——写在对象类型里,用 : 而不是 => type
DescribableFunction
= {
description
: string
(
someArg
: number): boolean
} function
doSomething
(
fn
:
DescribableFunction
) {
console
.
log
(`${
fn
.
description
} returned ${
fn
(6)}`)
} const
myFunc
= (
someArg
: number) =>
someArg
> 3
myFunc
.
description
= 'default description'
doSomething
(
myFunc
) // default description returned true
// 构造签名:new 关键字 + 调用签名,描述“可以被 new 的东西” class
SomeObject
{
constructor(public
s
: string) {}
} type
SomeConstructor
= {
new (
s
: string):
SomeObject
} function
create
(
ctor
:
SomeConstructor
) {
return new
ctor
('hello')
}
console
.
log
(
create
(
SomeObject
).
s
) // hello

泛型函数

  • 泛型描述两个值之间的对应关系:在签名里声明类型参数 <Type>,在输入(数组)和输出(返回值)两处使用,就建立了关联;调用时类型实参自动推断
  • 约束<Type extends { length: number }> 限制类型参数能接受的类型——有了约束才能访问 .length
  • 约束的常见错误:函数承诺返回与传入相同Type,却返回了一个只是满足约束的对象({ length: minimum })——TS 报错是对的,否则调用方拿到的"数组"会缺 slice 方法。
  • 手动指定类型实参:推断失败时兜底,combine<string | number>([1,2,3], ["hello"])
  • 写好泛型函数的三条准则:
    1. 能直接用类型参数就别约束它<Type>(arr: Type[]) 优于 <Type extends any[]>(arr: Type)——后者返回类型只能解析成 any
    2. 类型参数越少越好:不关联两个值的类型参数(如 Func extends (arg: Type) => boolean)是红旗信号。
    3. 类型参数应该出现两次:只出现一次就没在"关联"任何东西——它可能根本不需要泛型。
ts
// 泛型:用“类型参数”描述输入与输出之间的对应关系
function 
firstElement
<
Type
>(
arr
:
Type
[]):
Type
| undefined {
return
arr
[0]
} const
s
=
firstElement
(['a', 'b', 'c']) // s: string | undefined —— 类型实参自动推断
const
n
=
firstElement
([1, 2, 3]) // n: number | undefined
console
.
log
(
s
,
n
) // a 1
// 多个类型参数:Input 从数组推断,Output 从回调返回值推断 function
map
<
Input
,
Output
>(
arr
:
Input
[],
func
: (
arg
:
Input
) =>
Output
):
Output
[] {
return
arr
.
map
(
func
)
} const
parsed
=
map
(['1', '2', '3'], (
x
) =>
parseInt
(
x
, 10)) // parsed: number[]
console
.
log
(
parsed
) // [ 1, 2, 3 ]
// 约束:extends 限制类型参数能接受的类型——才能安全访问 .length function
longest
<
Type
extends {
length
: number }>(
a
:
Type
,
b
:
Type
) {
return
a
.
length
>=
b
.
length
?
a
:
b
}
console
.
log
(
longest
([1, 2], [1, 2, 3])) // [ 1, 2, 3 ](类型 number[])
console
.
log
(
longest
('alice', 'bob')) // alice(类型 "alice" | "bob")
// longest(10, 100) // 编译错误:number 没有 length 属性 // 手动指定类型实参:推断失败(两数组元素类型不一致)时的兜底 function
combine
<
Type
>(
arr1
:
Type
[],
arr2
:
Type
[]):
Type
[] {
return
arr1
.
concat
(
arr2
)
} // combine([1, 2, 3], ['hello']) // 编译错误:string 不可赋给 number
console
.
log
(
combine
<string | number>([1, 2, 3], ['hello'])) // [ 1, 2, 3, 'hello' ]

可选参数与函数重载

  • 可选参数 x?: number 的实际类型是 number | undefined;默认值参数 x = 10 在函数体内是 number;调用方永远可以显式传 undefined(等于"缺参")。
  • ⚠️ 回调的可选参数规则callback: (arg: any, index?: number) => void 的含义是"实现可能只传一个参数给回调",而不是"调用方可以写少一个参数的回调"(后者本来就合法——参数更少的函数总能顶替参数更多的)。除非你打算调用回调时不传该实参,否则别写可选参数
  • 函数重载:若干重载签名 + 一个兼容的实现签名实现签名从外部不可见——写重载至少要有两个重载签名,实现签名必须与所有重载兼容。
  • 重载准则:能用联合类型就别用重载——len(x: any[] | string) 还能接受"可能是 string 也可能是数组"的值,重载版本解析不了这种调用(一次调用只能命中一个重载)。
ts
// 可选参数 x?: number 的实际类型是 number | undefined;默认值参数在函数体内则是 number
function 
f
(
x
= 10) {
return
x
.
toFixed
() // x: number —— undefined 已被默认值替换
}
console
.
log
(
f
(),
f
(3.14),
f
(
undefined
)) // 10 3 10 —— 传 undefined 等于“缺参”
// 函数重载:若干“重载签名” + 一个兼容的“实现签名”(实现签名从外部不可见!) function
makeDate
(
timestamp
: number): Date
function
makeDate
(
m
: number,
d
: number,
y
: number): Date
function
makeDate
(
mOrTimestamp
: number,
d
?: number,
y
?: number): Date {
return
d
!==
undefined
&&
y
!==
undefined
? new
Date
(
y
,
mOrTimestamp
,
d
) : new
Date
(
mOrTimestamp
)
}
console
.
log
(
makeDate
(0).
getTime
()) // 0
console
.
log
(
makeDate
(5, 5, 2026).
getMonth
()) // 5
// makeDate(1, 3) // 编译错误:没有接受 2 个参数的重载(尽管实现签名看起来可以) // 能用联合类型就别用重载:这个版本还能接受“可能是 string 也可能是数组”的值 function
len
(
x
: any[] | string) {
return
x
.
length
}
console
.
log
(
len
('hello'),
len
([0, 1])) // 5 2
console
.
log
(
len
(
Math
.
random
() > 0.5 ? 'hello' : [0])) // 重载版本无法处理这种调用

this 声明与函数相关的特殊类型

  • 声明 this 的类型:JS 规定参数不能叫 this,TS 借用这个语法位——filter: (this: User) => boolean;回调方要用 function 而非箭头函数(箭头函数捕获外层 this)。
  • void:无返回值函数的返回类型;voidundefined
  • object:一切非原始类型的值——不同于空对象类型 {},更不同于全局的 Object永远用小写 object);函数值也是 object
  • unknown:与 any 一样能代表任何值,但不允许对它做任何操作——描述"接受任何值"的函数时用它替代 any(函数体不被污染);返回 unknownsafeParse 提醒调用方必须先收窄。
  • never(返回值位置):函数抛异常或终止程序,返回值永远观察不到;也出现在联合被收窄到一无所剩时。
  • Function:全局类型,带 bind / call / apply,可以调用但返回 any——非类型化函数调用,尽量避免;只想接受"任意函数但不调用"时用 () => void 更安全。
  • void 返回类型的怪但合理上下文函数类型 () => void 的实现可以返回任何值(类型上被忽略)——正因如此 forEach((el) => dst.push(el)) 才合法(push 返回 number);但字面函数定义标注 (): void 就必须真的不返回东西。
ts
// 声明 this 的类型:占用参数表第一位(JS 规定参数不能叫 this,TS 借用了这个语法位)
interface User {
  
name
: string
admin
: boolean
} function
filterUsers
(
list
: User[],
filter
: (
this
: User) => boolean): User[] {
return
list
.
filter
((
u
) =>
filter
.
call
(
u
))
} const
users
: User[] = [
{
name
: 'Ada',
admin
: true },
{
name
: 'Bob',
admin
: false },
] // 回调要用 function 而非箭头函数,才能拿到调用方指定的 this const
admins
=
filterUsers
(
users
, function (
this
: User) {
return this.
admin
})
console
.
log
(
admins
) // [ { name: 'Ada', admin: true } ]
// unknown:与 any 一样能接任何值,但“不允许对它做任何操作”——更安全 function
safeParse
(
text
: string): unknown {
return
JSON
.
parse
(
text
) // 函数体里不引入 any
} const
obj
=
safeParse
('{"x":1}')
// obj.x // 编译错误:'obj' is of type 'unknown' —— 使用前必须先收窄 if (typeof
obj
=== 'object' &&
obj
!== null)
console
.
log
('解析得到对象')
// never(在返回值位置):函数抛异常或终止程序,永远观察不到返回值 function
fail
(
msg
: string): never {
throw new
Error
(
msg
)
} try {
fail
('boom')
} catch (
e
) {
console
.
log
((
e
as Error).
message
) // boom
} // void 的“怪但合理”:上下文函数类型 () => void 的实现可以返回任何值——但类型上被忽略 type
voidFunc
= () => void
const
fv
:
voidFunc
= () => true
const
v
=
fv
() // v 的类型是 void(运行时值还在)
console
.
log
(
v
) // true
// 正因如此,forEach(期望 void 回调)里写 push(返回 number)才是合法的 const
src
= [1, 2, 3]
const
dst
= [0]
src
.
forEach
((
el
) =>
dst
.
push
(
el
))
console
.
log
(
dst
) // [ 0, 1, 2, 3 ]

剩余参数、展开与参数解构

  • 剩余参数...m: number[] 写在所有参数之后;类型注解必须是 T[] / Array<T> 或元组。
  • 展开实参:TS 不假定数组不可变——const args = [8, 5] 推断为 number[](长度不定),展开给 Math.atan2 这类固定参数函数会报错;最直接的修复是 as const(推断为二元组)。
  • 参数解构:类型注解写在解构语法之后 function sum({ a, b, c }: ABC),可抽成命名类型减少啰嗦。
ts
// 剩余参数:... 写在所有参数之后,类型注解必须是 T[] / Array<T>(或元组)
function 
multiply
(
n
: number, ...
m
: number[]) {
return
m
.
map
((
x
) =>
n
*
x
)
}
console
.
log
(
multiply
(10, 1, 2, 3, 4)) // [ 10, 20, 30, 40 ]
// 展开实参:TS 不假定数组不可变——number[] 长度不定,塞给固定参数个数的函数会报错 // const badArgs = [8, 5] // Math.atan2(...badArgs) // 编译错误:展开实参必须是元组类型或传给剩余参数 const
args
= [8, 5] as
const
// as const 推断为二元组 readonly [8, 5]
console
.
log
(
Math
.
atan2
(...
args
).
toFixed
(4)) // 1.0122
// 参数解构:类型注解写在解构语法之后(也可以抽成命名类型) type
ABC
= {
a
: number;
b
: number;
c
: number }
function
sum
({
a
,
b
,
c
}:
ABC
) {
return
a
+
b
+
c
}
console
.
log
(
sum
({
a
: 10,
b
: 3,
c
: 9 })) // 22

对象类型

  • 对象是 JS 组织和传递数据的基本方式,TS 用对象类型表示——可以匿名(内联 { name: string; age: number }),也可以用 interfacetype 命名。

属性修饰符

  • 每个属性可以指定三件事:类型、是否可选、是否可写
  • 可选属性 ?:可选性只是说"如果设了这个属性,它最好是指定的类型";strictNullChecks 下读取时是 number | undefined。设默认值的惯用法是解构 + 默认值function paintShape({ shape, xPos = 0 }: PaintOptions)——函数体内一定有值,调用方仍可不传。
  • ⚠️ 解构模式里不能放类型注解{ shape: Shape } 在 JS 里意味着"取 shape 属性并重命名为局部变量 Shape"。
  • readonly 属性:类型检查期间不可写,运行时无影响;⚠️ 两个要点:
    1. 不代表完全不可变——属性本身不能重写,但内部内容可以改home.resident.age++ 合法)。
    2. readonly 不参与类型兼容性检查——可写类型可以赋给 readonly 类型,再通过可写别名修改。
  • 索引签名[index: string]: number——不知道属性名但知道值的形状(字典模式);索引键只能是 string / number / symbol / 模板字符串模式及其联合。
    • 同时用 number 和 string 索引器时,数字索引的返回类型必须是字符串索引返回类型的子类型(JS 里 obj[100] 就是 obj["100"])。
    • 字符串索引签名强制所有属性都匹配它的返回类型obj.property 也算 obj["property"]);不同类型的属性可以用联合类型的索引签名容纳。
    • 索引签名也可以 readonly
ts
// 可选属性 + 解构默认值:调用方可不传,函数体内一定有值
interface PaintOptions {
  
shape
: string
xPos
?: number
yPos
?: number
} function
paintShape
({
shape
,
xPos
= 0,
yPos
= 0 }: PaintOptions) {
console
.
log
(`${
shape
} at (${
xPos
}, ${
yPos
})`) // xPos / yPos 都是 number
}
paintShape
({
shape
: 'circle' }) // circle at (0, 0)
paintShape
({
shape
: 'square',
xPos
: 100 }) // square at (100, 0)
// readonly:属性本身不能被重写,但“内部内容”可以改 interface Home { readonly
resident
: {
name
: string;
age
: number }
} const
home
: Home = {
resident
: {
name
: 'Ada',
age
: 41 } }
home
.
resident
.
age
++ // 合法:改的是 resident 里面的属性
// home.resident = { name: 'X', age: 1 } // 编译错误:不能对 readonly 属性赋值
console
.
log
(
home
.
resident
) // { name: 'Ada', age: 42 }
// readonly 不参与类型兼容性检查——可以通过“可写别名”改变 interface Person {
name
: string
age
: number
} interface ReadonlyPerson { readonly
name
: string
readonly
age
: number
} const
writablePerson
: Person = {
name
: 'Person McPersonface',
age
: 42 }
const
readonlyPerson
: ReadonlyPerson =
writablePerson
// 合法!
writablePerson
.
age
++
console
.
log
(
readonlyPerson
.
age
) // 43 —— 通过可写别名改掉了
// 索引签名:不知道属性名、但知道值的形状;所有属性都必须匹配索引签名的返回类型 interface NumberOrStringDictionary { [
index
: string]: number | string
length
: number // ok:number 是联合的成员
name
: string // ok:string 也是
} const
dict
: NumberOrStringDictionary = {
length
: 2,
name
: 'dict',
extra
: 42 }
console
.
log
(
dict
['extra'],
dict
.
name
) // 42 dict

多余属性检查

  • 对象字面量在赋值给变量或作为实参传递时会受到多余属性检查——有目标类型没有的属性就报错(colour vs color 的拼写错误在纯 JS 里会静默失败)。
  • 绕过的三种方式(但多数多余属性错误确实是 bug,优先修类型声明而不是绕过):
    1. 类型断言:{ width: 100, opacity: 0.5 } as SquareConfig
    2. 加字符串索引签名 [propName: string]: unknown(确定对象就是会有额外属性时)。
    3. 先赋给中间变量再传(变量不做多余属性检查)——但要求与目标类型至少有一个公共属性

扩展与交叉

  • extends:复制被扩展类型的成员再添加新成员——减少样板、传达"这些声明相关"的意图;interface 可多重扩展
  • 交叉类型 &:组合已有对象类型,拥有全部成员。
  • 两者的主要区别在冲突处理(这也是选型依据):
    • 同名 interface 合并:属性类型不兼容时直接报错
    • 交叉类型:冲突属性静默合并string & number = never——能编译,但没有值能满足,用的时候才爆。
ts
// interface 扩展:extends 复制成员、支持多重扩展
interface Colorful {
  
color
: string
} interface Circle {
radius
: number
} interface ColorfulCircle extends Colorful, Circle {} const
cc
: ColorfulCircle = {
color
: 'red',
radius
: 42 }
console
.
log
(
cc
) // { color: 'red', radius: 42 }
// 交叉类型:& 组合已有对象类型 type
ColorfulCircle2
= Colorful & Circle
function
draw
(
circle
:
ColorfulCircle2
) {
console
.
log
(`Color: ${
circle
.
color
}, Radius: ${
circle
.
radius
}`)
}
draw
({
color
: 'blue',
radius
: 42 }) // Color: blue, Radius: 42
// draw({ color: 'red', raidus: 42 }) // 编译错误:多余属性检查抓到拼写错误 raidus // 冲突处理差异(选择 extends 还是 & 的主要理由): // 同名 interface 合并时属性类型不兼容 → 直接报错: // interface P { name: string } // interface P { name: number } // Duplicate identifier / 合并失败 // 交叉类型则“静默合并”,冲突属性变成 never: interface Person1 {
name
: string
} interface Person2 {
name
: number
} type
Staff
= Person1 & Person2 // 能编译,但 name: string & number = never
// const staffer: Staff = { name: ??? } // 没有任何值能满足 never
console
.
log
('Staff["name"] 的类型是 never —— 无法构造这样的值')

泛型对象类型

  • interface Box<Type> { contents: Type }——"Box<Type> 是真类型的模板,Type 是占位符";Box<string> 与手写 StringBox 完全等价,但免去为每种内容类型造一个 Box + 一套重载
  • 配合泛型函数彻底告别重载:function setContents<Type>(box: Box<Type>, newContents: Type)
  • 类型别名同样能泛型,而且能描述非对象的泛型助手类型:type OrNull<T> = T | nulltype OneOrMany<T> = T | T[]
  • number[] 只是 Array<number> 的简写;Map<K, V>Set<T>Promise<T> 都是泛型数据结构。
ts
// 泛型对象类型:Box<Type> 是“真类型的模板”,Type 是占位符
interface 
Box
<
Type
> {
contents
:
Type
} const
boxA
:
Box
<string> = {
contents
: 'hello' } // 等同于 { contents: string }
console
.
log
(
boxA
.
contents
.
toUpperCase
()) // HELLO
// 一个泛型函数替代整套重载(StringBox / NumberBox / ... + N 个重载) function
setContents
<
Type
>(
box
:
Box
<
Type
>,
newContents
:
Type
) {
box
.
contents
=
newContents
} const
numBox
:
Box
<number> = {
contents
: 0 }
setContents
(
numBox
, 42)
console
.
log
(
numBox
.
contents
) // 42
// 类型别名也能泛型,还能描述“非对象”的泛型助手类型 type
OrNull
<
Type
> =
Type
| null
type
OneOrMany
<
Type
> =
Type
|
Type
[]
type
OneOrManyOrNull
<
Type
> =
OrNull
<
OneOrMany
<
Type
>>
const
a
:
OneOrManyOrNull
<string> = 'one'
const
b
:
OneOrManyOrNull
<string> = ['o', 'n', 'e']
const
c
:
OneOrManyOrNull
<string> = null
console
.
log
(
a
,
b
,
c
) // one [ 'o', 'n', 'e' ] null
// number[] 只是 Array<number> 的简写;Map<K,V>、Set<T>、Promise<T> 同为泛型结构 const
strs
:
Array
<string> = ['hello', 'world']
console
.
log
(
strs
.
length
) // 2

ReadonlyArray 与元组

  • ReadonlyArray<T>(简写 readonly T[]):描述"不应被改变"的数组——返回它表示"别改内容",接受它表示"放心传,不会被改";没有构造函数(不能 new),把普通数组赋给它即可。
  • ⚠️ 与 readonly 属性修饰符不同,可赋值性是单向的Array → ReadonlyArray 可以,反过来不行。
  • 元组类型 [string, number]:确切知道元素个数和每个位置的类型;越界索引报错;解构自动获得对应类型。适合约定俗成的 API——但如果元素含义并非人人"显然",考虑用带描述性属性名的对象。
  • 可选元素 [number, number, number?]:只能在末尾,length 变成 2 | 3
  • 剩余元素 [string, number, ...boolean[]]:元组不再有固定 length;与函数参数表对应——(...args: [string, number, ...boolean[]]) 基本等价于 (name: string, version: number, ...input: boolean[])
  • readonly 元组是好默认:多数元组创建后不会修改;且 as const 的数组字面量就推断为 readonly 元组。
ts
// ReadonlyArray / readonly T[]:只读数组——意图声明,没有运行时表示
function 
doStuff
(
values
: readonly string[]) {
console
.
log
(`第一个值:${
values
[0]}`)
// values.push('hello!') // 编译错误:readonly string[] 上没有 push return
values
.
slice
() // 读操作都可以
}
console
.
log
(
doStuff
(['red', 'green', 'blue'])) // [ 'red', 'green', 'blue' ]
// 可赋值性是单向的:Array → ReadonlyArray 可以,反过来不行 const
y
: string[] = ['a']
const
x
: readonly string[] =
y
// OK
// const z: string[] = x // 编译错误:readonly 不能赋给可变类型
console
.
log
(
x
) // [ 'a' ]
// 元组:确切知道元素个数与每个位置的类型 type
StringNumberPair
= [string, number]
function
logPair
(
pair
:
StringNumberPair
) {
const [
inputString
,
hash
] =
pair
// 解构自动获得对应类型
console
.
log
(
inputString
.
toUpperCase
(),
hash
.
toFixed
(1))
}
logPair
(['hello', 42]) // HELLO 42.0
// 可选元素(只能在末尾,会影响 length 的类型) type
Either2dOr3d
= [number, number, number?]
function
setCoordinate
(
coord
:
Either2dOr3d
) {
const [
xPos
,
yPos
,
zPos
] =
coord
// zPos: number | undefined
console
.
log
(`${
coord
.
length
} 维坐标:`,
xPos
,
yPos
,
zPos
?? '(无 z)') // length: 2 | 3
}
setCoordinate
([3, 4]) // 2 维坐标: 3 4 (无 z)
setCoordinate
([3, 4, 5]) // 3 维坐标: 3 4 5
// 剩余元素:元组不再有固定 length,可对应函数参数表 type
StringNumberBooleans
= [string, number, ...boolean[]]
const
snb
:
StringNumberBooleans
= ['world', 3, true, false]
console
.
log
(
snb
.
length
) // 4
// readonly 元组是好默认:as const 的数组字面量就推断为 readonly 元组 const
point
= [3, 4] as
const
// readonly [3, 4]
function
distanceFromOrigin
([
px
,
py
]: readonly [number, number]) {
return
Math
.
sqrt
(
px
** 2 +
py
** 2)
}
console
.
log
(
distanceFromOrigin
(
point
)) // 5

从类型创建类型

  • TS 类型系统的强大之处:可以用其他类型来表达新类型,甚至用已有的来表达类型。
  • 类型操纵(Type Manipulation)系列的工具箱:
    • 泛型——带参数的类型(最简单的形式)。
    • keyof 类型操作符——从对象类型取键。
    • typeof 类型操作符——从值取类型。
    • 索引访问类型 Type['a']——取类型的子集。
    • 条件类型——类型系统里的 if 语句。
    • 映射类型——把已有类型的每个属性映射成新类型。
    • 模板字面量类型——经模板字符串改变属性的映射类型。
  • 组合这些操作符可以用简洁、可维护的方式表达复杂的类型运算。

泛型

泛型的 Hello World:identity

  • 不用泛型的两难:写死 number 失去复用性;用 any 虽然"通用"但丢失类型信息(传入 number,只知道返回"任意类型")。
  • 类型变量:作用于类型而非值的特殊变量——function identity<Type>(arg: Type): TypeType 捕获实参类型并"运"到返回值,既通用又精确。
  • 两种调用:显式 identity<string>("myString")类型实参推断(最常用)——编译器根据实参自动填 Type
  • 类型变量可以作为类型的一部分arg: Type[] 表示"Type 的数组",.length 自然可用。
ts
// 泛型 Hello World:identity —— 类型变量捕获实参类型,输入输出同型(any 则会丢失这一信息)
function 
identity
<
Type
>(
arg
:
Type
):
Type
{
return
arg
} const
out1
=
identity
<string>('myString') // 显式指定类型实参
const
out2
=
identity
('myString') // 类型实参推断(最常用)
console
.
log
(
out1
,
out2
) // myString myString
// 类型变量可以作为“类型的一部分”使用:Type[] 保证 .length 可用 function
loggingIdentity
<
Type
>(
arg
:
Type
[]):
Type
[] {
console
.
log
(
arg
.
length
)
return
arg
}
loggingIdentity
([1, 2, 3]) // 3
// 泛型约束:extends 要求类型至少具备某些能力 interface Lengthwise {
length
: number
} function
constrainedIdentity
<
Type
extends Lengthwise>(
arg
:
Type
):
Type
{
console
.
log
(
arg
.
length
) // 有约束才能访问 .length
return
arg
}
constrainedIdentity
({
length
: 10,
value
: 3 }) // 10
// constrainedIdentity(3) // 编译错误:number 没有 length 属性 // 类型参数约束另一个类型参数:类型安全的属性取值 function
getProperty
<
Type
,
Key
extends keyof
Type
>(
obj
:
Type
,
key
:
Key
) {
return
obj
[
key
]
} const
x
= {
a
: 1,
b
: 2,
c
: 3,
d
: 4 }
console
.
log
(
getProperty
(
x
, 'a')) // 1
// getProperty(x, 'm') // 编译错误:'m' 不在 "a" | "b" | "c" | "d" 中

泛型类型、泛型类与泛型约束

  • 泛型函数的类型<Type>(arg: Type) => Type——类型参数名可以不同,只要数量和用法对得上;也可写成对象字面量类型的调用签名。
  • 泛型接口的两种姿势:类型参数放在调用签名上(描述泛型函数)vs 放在接口本身上(GenericIdentityFn<number>——锁定底层签名,且对接口所有成员可见)。
  • 泛型类class GenericNumber<NumType>——类型参数只作用于实例侧,静态成员不能使用;不能创建泛型枚举和泛型命名空间。
  • 泛型约束<Type extends Lengthwise> 要求类型至少具备约束所描述的能力——换来的是可以安全访问 .length,代价是不再接受任意类型。
  • 类型参数约束类型参数<Type, Key extends keyof Type>——getProperty(obj, key) 保证 key 一定存在于 obj。
  • 在泛型里使用类类型:工厂函数用构造函数引用类 c: { new (): Type }createInstance<A extends Animal>(c: new () => A): A 还能约束构造函数与实例侧的关系(mixin 模式的基石)。
  • 泛型参数默认值<T extends HTMLElement = HTMLDivElement>——让类型实参变为可选,替代一串重载。
  • 变型注解(in / out:极少需要——TS 自动推断变型;只在极罕见的循环类型场景下用,且必须与结构行为一致,不能用来"强迫"某种变型。
ts
// 泛型接口:类型参数放在“接口”上会锁定底层调用签名的类型
interface 
GenericIdentityFn
<
Type
> {
(
arg
:
Type
):
Type
} function
identity
<
Type
>(
arg
:
Type
):
Type
{
return
arg
} const
myIdentity
:
GenericIdentityFn
<number> =
identity
// 锁定为 number
console
.
log
(
myIdentity
(42)) // 42
// 泛型类:类型参数跟在类名后;只作用于“实例侧”,静态成员不能用类型参数 class
GenericNumber
<
NumType
> {
constructor( public
zeroValue
:
NumType
,
public
add
: (
x
:
NumType
,
y
:
NumType
) =>
NumType
,
) {} } const
num
= new
GenericNumber
<number>(0, (
a
,
b
) =>
a
+
b
)
console
.
log
(
num
.
add
(
num
.
zeroValue
, 5)) // 5
const
str
= new
GenericNumber
<string>('', (
a
,
b
) =>
a
+
b
)
console
.
log
(
str
.
add
(
str
.
zeroValue
, 'test')) // test —— 没有限制只能用 number
// 在泛型里使用“类类型”:工厂函数通过构造函数引用类,并约束实例侧关系 class
BeeKeeper
{
hasMask
= true
} class
ZooKeeper
{
nametag
= 'Mikle'
} class
Animal
{
numLegs
= 4
} class
Bee
extends
Animal
{
numLegs
= 6
keeper
= new
BeeKeeper
()
} class
Lion
extends
Animal
{
keeper
= new
ZooKeeper
()
} function
createInstance
<
A
extends
Animal
>(
c
: new () =>
A
):
A
{
return new
c
()
}
console
.
log
(
createInstance
(
Lion
).
keeper
.
nametag
) // Mikle
console
.
log
(
createInstance
(
Bee
).
keeper
.
hasMask
) // true

keyof 与 typeof 类型操作符

keyof

  • keyof 取对象类型的键,产出字符串或数字字面量联合keyof { x: number; y: number } = "x" | "y"
  • 有索引签名时返回索引类型:{ [n: number]: unknown }number{ [k: string]: boolean }string | number(JS 对象键总被强转成字符串,obj[0]obj["0"])。
  • 与映射类型组合时最见威力。

typeof

  • JS 已有表达式上下文的 typeof;TS 增加了类型上下文的 typeof——引用变量或属性的类型
  • 对基本类型不太有用,与其他操作符组合才见效:ReturnType<typeof f>——先 typeof 从值 f 拿到函数类型,再取返回类型(直接写 ReturnType<f> 报错:"f 是值不是类型")。
  • 限制:typeof 只能用在标识符(变量名)或其属性上——不能写 typeof msgbox(...)(避免"以为在执行代码"的陷阱)。
ts
// keyof:取对象类型的键,产出字符串/数字字面量联合
type 
Point
= {
x
: number;
y
: number }
type
P
= keyof
Point
// "x" | "y"
const
p1
:
P
= 'x'
console
.
log
(
p1
) // x
// 有索引签名时 keyof 返回索引类型 type
Arrayish
= { [
n
: number]: unknown }
type
A
= keyof
Arrayish
// number
type
Mapish
= { [
k
: string]: boolean }
type
M
= keyof
Mapish
// string | number —— obj[0] 等价于 obj["0"],键总被强转成字符串
const
a1
:
A
= 0
const
m1
:
M
= 'anything'
console
.
log
(typeof
a1
, typeof
m1
) // number string
// typeof(类型上下文):引用变量或属性的“类型”——与表达式里的 typeof 是两回事 const
s
= 'hello'
const
n
: typeof
s
= 'hello' // n 的类型是 "hello"(const 推断为字面量类型)
console
.
log
(
n
)
// 与 ReturnType<T> 组合最常用:先 typeof 拿到函数类型,再取返回类型 function
f
() {
return {
x
: 10,
y
: 3 }
} // type Bad = ReturnType<f> // 编译错误:f 是“值”不是“类型”——Did you mean 'typeof f'? type
FRet
=
ReturnType
<typeof
f
> // { x: number; y: number }
const
point
:
FRet
= {
x
: 1,
y
: 2 }
console
.
log
(
point
) // { x: 1, y: 2 }
// 限制:typeof 只能用在标识符(变量名)或其属性上,不能写 typeof fn(...)

索引访问类型

  • Person["age"] 查出属性的类型;索引本身也是类型——联合、keyof、别的类型都能放进去:Person[keyof Person]Person["age" | "name"]
  • 索引不存在的属性直接报错(拼写错误立即被抓)。
  • 经典组合:typeof 数组字面量 + [number] 取出数组元素的类型——(typeof MyArray)[number]
  • ⚠️ 索引位置只能放类型const key = "age"; Person[key] 不行(key 是值);type key = "age" 可以。
ts
// 索引访问类型:Type["key"] 查出某个属性的类型
type 
Person
= {
age
: number;
name
: string;
alive
: boolean }
type
Age
=
Person
['age'] // number
const
myAge
:
Age
= 30
console
.
log
(
myAge
) // 30
// 索引本身也是类型:联合、keyof 都能放进去 type
I1
=
Person
['age' | 'name'] // string | number
type
I2
=
Person
[keyof
Person
] // string | number | boolean
const
v1
:
I1
= 'Alice'
const
v2
:
I2
= true
console
.
log
(
v1
,
v2
) // Alice true
// type Bad = Person['alve'] // 编译错误:属性不存在(拼写错误立即被抓) // 经典组合:typeof 数组字面量 + [number] 取出“元素类型” const
MyArray
= [
{
name
: 'Alice',
age
: 15 },
{
name
: 'Bob',
age
: 23 },
{
name
: 'Eve',
age
: 38 },
] type
ArrPerson
= (typeof
MyArray
)[number] // { name: string; age: number }
type
ArrAge
= (typeof
MyArray
)[number]['age'] // number
const
someone
:
ArrPerson
= {
name
: 'Carol',
age
: 20 }
const
anAge
:
ArrAge
= 42
console
.
log
(
someone
,
anAge
) // { name: 'Carol', age: 20 } 42
// 索引位置只能放“类型”:const key = "age" 不行,type key = "age" 可以 type
key
= 'age'
type
Age2
=
Person
[
key
]
const
age3
:
Age2
= 18
console
.
log
(
age3
) // 18

条件类型

基本形式与泛型联用

  • SomeType extends OtherType ? TrueType : FalseType——左边可赋给右边就取 true 分支,语法与 JS 三元一致。
  • 真正的威力在与泛型联用createLabel 若用重载要写 3 个(string / number / 联合),每支持一种新类型重载数就指数增长;一个条件类型 NameOrId<T> = T extends number ? IdLabel : NameLabel 全部搞定。
  • 条件类型内的约束:true 分支里 TS 知道 T 满足检查条件——T extends { message: unknown } ? T["message"] : never 既接受任意类型又能兜底。
ts
// 条件类型:SomeType extends OtherType ? TrueType : FalseType
interface Animal {
  
live
(): void
} interface Dog extends Animal {
woof
(): void
} type
Example1
= Dog extends Animal ? number : string // number
type
Example2
= RegExp extends Animal ? number : string // string
const
e1
:
Example1
= 1
const
e2
:
Example2
= 's'
console
.
log
(
e1
,
e2
) // 1 s
// 威力在于与泛型联用:一个条件类型替代成倍增长的重载 interface IdLabel {
id
: number
} interface NameLabel {
name
: string
} type
NameOrId
<
T
extends number | string> =
T
extends number ? IdLabel : NameLabel
function
createLabel
<
T
extends number | string>(
idOrName
:
T
):
NameOrId
<
T
> {
return (typeof
idOrName
=== 'number' ? {
id
:
idOrName
} : {
name
:
idOrName
}) as
NameOrId
<
T
>
} const
a
=
createLabel
('typescript') // a: NameLabel
const
b
=
createLabel
(2.8) // b: IdLabel
console
.
log
(
a
,
b
) // { name: 'typescript' } { id: 2.8 }
// 条件类型内的约束:true 分支里 TS 知道 T 一定有 message type
MessageOf
<
T
> =
T
extends {
message
: unknown } ?
T
['message'] : never
interface Email {
message
: string
} type
EmailContents
=
MessageOf
<Email> // string
type
DogContents
=
MessageOf
<Dog> // never —— 没有 message 属性时兜底
const
msg
:
EmailContents
= 'hello'
console
.
log
(
msg
) // hello
// Flatten:数组降到元素类型(索引访问版),其他类型保持原样 type
Flatten
<
T
> =
T
extends any[] ?
T
[number] :
T
type
Str
=
Flatten
<string[]> // string
type
Num
=
Flatten
<number> // number
const
fs
:
Str
= 'flat'
const
fn2
:
Num
= 3
console
.
log
(
fs
,
fn2
) // flat 3

infer 与分布式条件类型

  • infer:在 true 分支声明式引入类型变量,免去手动挖掘类型结构——Type extends Array<infer Item> ? Item : Type;提取函数返回类型:Type extends (...args: never[]) => infer Return ? Return : never
  • 从多调用签名(重载函数)推断时,取最后一个签名。
  • 分布式条件类型:条件类型作用于泛型、传入联合时,会逐成员应用再取联合——ToArray<string | number> = ToArray<string> | ToArray<number> = string[] | number[]
  • 通常这正是想要的行为;要关闭它,用方括号包住 extends 两侧[Type] extends [any] → 得到 (string | number)[]
ts
// infer:在 true 分支“声明式”引入类型变量,免去手动挖掘类型结构
type 
Flatten
<
Type
> =
Type
extends
Array
<infer
Item
> ?
Item
:
Type
type
S
=
Flatten
<string[]> // string —— Item 被推断出来
const
s2
:
S
= 'x'
console
.
log
(
s2
) // x
// 提取函数返回类型(简化版 ReturnType) type
GetReturnType
<
Type
> =
Type
extends (...
args
: never[]) => infer
Return
?
Return
: never
type
NumRet
=
GetReturnType
<() => number> // number
type
StrRet
=
GetReturnType
<(
x
: string) => string> // string
type
Bools
=
GetReturnType
<(
a
: boolean,
b
: boolean) => boolean[]> // boolean[]
const
nr
:
NumRet
= 1
const
sr
:
StrRet
= 'r'
const
br
:
Bools
= [true]
console
.
log
(
nr
,
sr
,
br
) // 1 r [ true ]
// 从“多调用签名”(重载函数)推断时,取“最后一个”签名 // 分布式条件类型:作用于泛型时,联合会被“逐成员”应用再取联合 type
ToArray
<
Type
> =
Type
extends any ?
Type
[] : never
type
StrArrOrNumArr
=
ToArray
<string | number> // string[] | number[]
const
saa
:
StrArrOrNumArr
= ['a', 'b'] // 只能装同一种
console
.
log
(
saa
) // [ 'a', 'b' ]
// 用 [方括号] 包住 extends 两侧即可关闭分布式行为 type
ToArrayNonDist
<
Type
> = [
Type
] extends [any] ?
Type
[] : never
type
ArrOfStrOrNum
=
ToArrayNonDist
<string | number> // (string | number)[]
const
mixed
:
ArrOfStrOrNum
= ['a', 1] // 可以混装
console
.
log
(
mixed
) // [ 'a', 1 ]

映射类型

基本映射与修饰符

  • 映射类型建立在索引签名语法之上:[Property in keyof Type]: boolean——用键联合遍历生成新类型(不重复自己)。
  • 映射修饰符readonly? 可在映射时用 - / + 前缀移除或添加(不写默认 +):
    • -readonly [P in keyof T]: T[P] —— 解除只读。
    • [P in keyof T]-?: T[P] —— 解除可选(全部必填)。
ts
// 映射类型:基于索引签名语法,用 keyof 产生的键联合遍历生成新类型
type 
Features
= {
darkMode
: () => void
newUserProfile
: () => void
} type
OptionsFlags
<
Type
> = {
[
Property
in keyof
Type
]: boolean
} type
FeatureOptions
=
OptionsFlags
<
Features
> // { darkMode: boolean; newUserProfile: boolean }
const
flags
:
FeatureOptions
= {
darkMode
: true,
newUserProfile
: false }
console
.
log
(
flags
) // { darkMode: true, newUserProfile: false }
// 映射修饰符:readonly 与 ? 可用 - / + 前缀移除或添加(不写前缀默认 +) type
LockedAccount
= {
readonly
id
: string
readonly
name
: string
} type
CreateMutable
<
Type
> = {
-readonly [
Property
in keyof
Type
]:
Type
[
Property
]
} const
acct
:
CreateMutable
<
LockedAccount
> = {
id
: '1',
name
: 'a' }
acct
.
name
= 'b' // -readonly 之后可写
console
.
log
(
acct
) // { id: '1', name: 'b' }
type
MaybeUser
= {
id
: string
name
?: string
age
?: number
} type
Concrete
<
Type
> = {
[
Property
in keyof
Type
]-?:
Type
[
Property
]
} const
user
:
Concrete
<
MaybeUser
> = {
id
: '1',
name
: 'n',
age
: 3 } // -? 之后全部必填
console
.
log
(
user
) // { id: '1', name: 'n', age: 3 }

as 键重映射

  • TS 4.1+ 可用 as 子句重映射键[P in keyof T as NewKeyType]: T[P]
  • 配合模板字面量类型从旧属性名造新名:[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P]Getters<Person>
  • 经由 never 过滤键as Exclude<Property, "kind">——产生 never 的键被丢弃。
  • 可以映射任意类型的联合(不限于键联合):[E in Events as E["kind"]]: (event: E) => void——按判别式生成事件配置表。
  • 与条件类型联用:T[P] extends { pii: true } ? true : false
ts
// as 键重映射(TS 4.1+):配合模板字面量类型从旧属性名造新属性名
interface Person {
  
name
: string
age
: number
location
: string
} type
Getters
<
Type
> = {
[
Property
in keyof
Type
as `get${
Capitalize
<string &
Property
>}`]: () =>
Type
[
Property
]
} const
lazyPerson
:
Getters
<Person> = {
getName
: () => 'Saoirse',
getAge
: () => 26,
getLocation
: () => 'Dublin',
}
console
.
log
(
lazyPerson
.
getName
(),
lazyPerson
.
getAge
()) // Saoirse 26
// 经由 never 过滤键(Exclude 产生 never 的键会被丢弃) interface Circle {
kind
: 'circle'
radius
: number
} type
RemoveKindField
<
Type
> = {
[
Property
in keyof
Type
as
Exclude
<
Property
, 'kind'>]:
Type
[
Property
]
} const
kindless
:
RemoveKindField
<Circle> = {
radius
: 42 }
console
.
log
(
kindless
) // { radius: 42 }
// 可以映射“任意类型的联合”,不限于 string | number | symbol 的键 type
SquareEvent
= {
kind
: 'square';
x
: number;
y
: number }
type
CircleEvent
= {
kind
: 'circle';
radius
: number }
type
EventConfig
<
Events
extends {
kind
: string }> = {
[
E
in
Events
as
E
['kind']]: (
event
:
E
) => void
} const
config
:
EventConfig
<
SquareEvent
|
CircleEvent
> = {
square
: (
e
) =>
console
.
log
('square',
e
.
x
,
e
.
y
),
circle
: (
e
) =>
console
.
log
('circle',
e
.
radius
),
}
config
.
square
({
kind
: 'square',
x
: 1,
y
: 2 }) // square 1 2
// 与条件类型联用:标记含 pii: true 的字段 type
DBFields
= {
id
: {
format
: 'incrementing' }
name
: {
type
: string;
pii
: true }
} type
ExtractPII
<
Type
> = {
[
Property
in keyof
Type
]:
Type
[
Property
] extends {
pii
: true } ? true : false
} const
gdpr
:
ExtractPII
<
DBFields
> = {
id
: false,
name
: true }
console
.
log
(
gdpr
) // { id: false, name: true }

模板字面量类型

  • 建立在字符串字面量类型之上,语法与 JS 模板字符串相同但用在类型位置type Greeting = `hello ${World}`
  • 联合在插值位会交叉相乘:每个插值位的联合成员两两组合——`${Lang}_${AllLocaleIDs}` = 3 × 4 = 12 个成员(大字符串联合建议提前生成,小场景用它很合适)。
  • 杀手级应用:从类型内部的信息定义新字符串——on(eventName: `${string & keyof Type}Changed`, ...) 把"属性名 + Changed"的约定写进类型,拼错立即报错。
  • 与推断结合on<Key extends string & keyof Type>(eventName: `${Key}Changed`, callback: (newValue: Type[Key]) => void)——TS 从 "firstNameChanged" 反推 Key = "firstName",再用索引访问拿到属性类型,让回调参数自动获得正确类型。
  • 内建字符串操纵类型(编译器内置,.d.ts 里找不到,直接用 JS 字符串方法实现、不感知 locale):Uppercase<S> / Lowercase<S> / Capitalize<S> / Uncapitalize<S>
ts
// 模板字面量类型:拼接字面量类型;联合出现在插值位时“交叉相乘”
type 
World
= 'world'
type
Greeting
= `hello ${
World
}` // "hello world"
const
g
:
Greeting
= 'hello world'
console
.
log
(
g
)
type
EmailLocaleIDs
= 'welcome_email' | 'email_heading'
type
FooterLocaleIDs
= 'footer_title' | 'footer_sendoff'
type
AllLocaleIDs
= `${
EmailLocaleIDs
|
FooterLocaleIDs
}_id` // 4 个成员
type
Lang
= 'en' | 'ja' | 'pt'
type
LocaleMessageIDs
= `${
Lang
}_${
AllLocaleIDs
}` // 3 × 4 = 12 个成员
const
id
:
LocaleMessageIDs
= 'ja_footer_title_id'
console
.
log
(
id
)
// 类型内的字符串操纵:on 只接受 "属性名Changed",并通过 Key 推断关联回调参数类型 type
PropEventSource
<
Type
> = {
on
<
Key
extends string & keyof
Type
>(
eventName
: `${
Key
}Changed`,
callback
: (
newValue
:
Type
[
Key
]) => void,
): void } function
makeWatchedObject
<
Type
extends object>(
obj
:
Type
):
Type
&
PropEventSource
<
Type
> {
return { ...
obj
,
on
(
eventName
: string,
callback
: (
newValue
: never) => void) {
const
key
=
eventName
.
replace
(/Changed$/, '') as keyof
Type
;(
callback
as (
v
: unknown) => void)(
obj
[
key
]) // 简化实现:立即用当前值回调一次
}, } as
Type
&
PropEventSource
<
Type
>
} const
person
=
makeWatchedObject
({
firstName
: 'Saoirse',
lastName
: 'Ronan',
age
: 26 })
person
.
on
('firstNameChanged', (
newName
) =>
console
.
log
(
newName
.
toUpperCase
())) // SAOIRSE —— newName: string
person
.
on
('ageChanged', (
newAge
) =>
console
.
log
(
newAge
.
toFixed
(1))) // 26.0 —— newAge: number
// person.on('firstName', () => {}) // 编译错误:必须是 "...Changed" 形式 // person.on('frstNameChanged', () => {}) // 编译错误:拼写错误也能抓住 // 内建字符串操纵类型(编译器内置,.d.ts 里找不到) type
Shouty
=
Uppercase
<'Hello, world'> // "HELLO, WORLD"
type
Quiet
=
Lowercase
<'Hello, WORLD'> // "hello, world"
type
Cap
=
Capitalize
<'hello'> // "Hello"
type
Uncap
=
Uncapitalize
<'HELLO'> // "hELLO"
const
su
:
Shouty
= 'HELLO, WORLD'
const
cu
:
Cap
= 'Hello'
console
.
log
(
su
,
cu
) // HELLO, WORLD Hello

类成员

  • 字段:默认创建公共可写属性;不注解则隐式 any;初始化器会用于推断类型(如 x = 0number)。
  • strictPropertyInitialization:字段必须在构造函数本身初始化——TS 不分析构造函数调用的方法(派生类可能覆盖它们导致漏初始化);确定由外部初始化(如注入库)时用明确赋值断言 !name!: string
  • readonly 字段:只允许在构造函数内赋值。
  • 构造函数:可带参数注解、默认值、重载;与函数签名的差别——不能有类型参数(那属于类)、不能注解返回类型(永远返回实例)。派生类构造函数this. 之前必须先 super()
  • 方法体内必须用 this. 访问字段——裸名字永远指向外层作用域。
  • getter / setter:只有 get 没有 set 时属性自动 readonly;不注解 setter 参数时从 getter 返回类型推断;TS 4.3+ 允许 get/set 类型不同(如 get 返回 number、set 接受 string | number | boolean)。无额外逻辑的 get/set 对没什么用——直接暴露公共字段即可。
  • 类也可以声明索引签名,但要同时容纳方法类型,很难用好——索引数据通常放在别处更佳。
ts
class 
Point
{
x
= 0 // 字段初始化器:实例化时自动运行,并用于推断类型(number)
y
= 0
} const
pt
= new
Point
()
console
.
log
(`${
pt
.
x
}, ${
pt
.
y
}`) // 0, 0
// pt.x = '0' // 编译错误:string 不能赋给 number // strictPropertyInitialization:字段必须在“构造函数本身”初始化 //(TS 不分析构造函数调用的方法——派生类可能覆盖它们导致漏初始化) class
GoodGreeter
{
name
: string
injected
!: string // 明确赋值断言 !:初始化交给外部(如注入库),跳过检查
constructor() { this.
name
= 'hello'
} }
console
.
log
(new
GoodGreeter
().
name
) // hello
// readonly:只允许在构造函数里赋值 class
Greeter
{
readonly
name
: string = 'world'
constructor(
otherName
?: string) {
if (
otherName
!==
undefined
) this.
name
=
otherName
} }
console
.
log
(new
Greeter
('reader').
name
) // reader
// new Greeter().name = 'x' // 编译错误:只读属性 // 方法体内必须用 this. 访问字段——裸名字永远指向“外层作用域” // getter / setter:TS 4.3+ 允许读写不同类型 class
Thing
{
_size
= 0
get
size
(): number {
return this.
_size
} set
size
(
value
: string | number | boolean) {
const
num
=
Number
(
value
)
this.
_size
=
Number
.
isFinite
(
num
) ?
num
: 0 // 拒绝 NaN / Infinity 等
} } const
t
= new
Thing
()
t
.
size
= '42' // setter 接受 string
console
.
log
(
t
.
size
) // 42 —— getter 返回 number
t
.
size
= 'oops'
console
.
log
(
t
.
size
) // 0

类继承

  • implements:只检查类能否被当作接口类型对待——完全不改变类的类型或推断!常见错误:以为 implements Checkable 会让 check(s)s 获得 string 类型(不会,仍是隐式 any);实现带可选属性的接口也不会创建该属性。可实现多个接口。
  • extends:派生类拥有基类全部属性方法,还可以添加新成员;覆盖方法用 super. 访问基类版本。
  • 派生类必须遵守基类契约——通过基类引用使用派生实例是常见且永远合法的(const b: Base = d);把可选参数改成必选就是违约(编译期报错,否则运行时崩溃)。
  • 纯类型字段声明target >= ES2022 时派生类字段初始化会覆盖基类构造函数设的值——只想重新声明更精确的类型时用 declare resident: Dog(不产生运行时代码)。
  • 初始化顺序:基类字段 → 基类构造函数 → 派生类字段 → 派生类构造函数——所以基类构造函数里看到的是自己的字段值(打印 base 而不是 derived)。
  • 继承 Error / Array 等内置类型且 target 为 ES5 时原型链会断(instanceof 失效、方法 undefined)——在 super() 之后手动 Object.setPrototypeOf(this, MsgError.prototype)
ts
// implements:只是“检查”类能否当接口用——完全不改变类的类型或推断!
interface Pingable {
  
ping
(): void
} class
Sonar
implements Pingable {
ping
() {
console
.
log
('ping!')
} } new
Sonar
().
ping
() // ping!
// 常见误解:implements 不会让方法参数获得接口里的类型(参数仍是隐式 any) // 实现带可选属性的接口也“不会自动创建”该属性 // extends 覆盖方法:派生类必须遵守基类契约(经基类引用调用必须始终合法) class
Base
{
greet
() {
console
.
log
('Hello, world!')
} } class
Derived
extends
Base
{
greet
(
name
?: string) {
// 参数“可选”是合法覆盖;写成必选 (name: string) 就违约报错 if (
name
===
undefined
) super.
greet
()
else
console
.
log
(`Hello, ${
name
.
toUpperCase
()}`)
} } const
d
= new
Derived
()
d
.
greet
() // Hello, world!
d
.
greet
('reader') // Hello, READER
const
b
:
Base
=
d
// 经基类引用使用派生实例:常见且永远合法
b
.
greet
() // Hello, world!
// 初始化顺序:基类字段 → 基类构造函数 → 派生类字段 → 派生类构造函数 class
Base2
{
name
= 'base'
constructor() {
console
.
log
('My name is ' + this.
name
) // base —— 派生类字段此时还没初始化!
} } class
Derived2
extends
Base2
{
name
= 'derived'
} const
d2
= new
Derived2
() // My name is base
console
.
log
(
d2
.
name
) // derived

成员可见性

  • public(默认,可不写)/ protected(自身 + 子类可见)/ private(仅自身,子类也不行)。
  • protected 细节:派生类可以把 protected 成员提升为 public(不写修饰符即公开——不想暴露就要记得重写 protected);不允许跨层级访问兄弟类的 protected 成员。
  • private 细节:派生类不能提升其可见性;TS 允许跨实例访问同类其他实例的 private(与 Java/C# 一致)。
  • ⚠️ 软私有private / protected 只在类型检查期间强制——运行时 in、属性查找、甚至类型检查内的方括号访问 s["secretKey"] 都能碰到;JS 的 # 字段才是硬私有(编译后仍私有,ES2021 以下用 WeakMap 实现)。需要真隐私就用闭包 / WeakMap / # 字段。
  • 静态成员:属于类本身,可用三种可见性修饰符、可被继承;name / length / callFunction 原型属性不能用作静态成员名;TS 不需要"静态类"语法——普通对象或顶层函数就够了;静态块可以访问静态私有字段做初始化。
  • 泛型类的静态成员不能引用类型参数——类型被完全擦除后运行时只有一个 Box.defaultValue 槽位。
  • 参数属性:构造参数前加 public / private / protected / readonly,自动变成同名字段——省掉声明 + 赋值样板。
ts
// public(默认)/ protected(自身 + 子类)/ private(仅自身,子类也不行)
class 
Greeter
{
public
greet
() {
console
.
log
('Hello, ' + this.
getName
())
} protected
getName
() {
return 'hi' } } class
SpecialGreeter
extends
Greeter
{
public
howdy
() {
console
.
log
('Howdy, ' + this.
getName
()) // 子类可访问 protected
} } new
SpecialGreeter
().
howdy
() // Howdy, hi
// new SpecialGreeter().getName() // 编译错误:protected 成员 // TS 允许“跨实例”访问 private(与 Java/C# 一致;Ruby 不允许) class
A
{
private
x
= 10
public
sameAs
(
other
:
A
) {
return
other
.
x
=== this.
x
// 合法:同类的其他实例
} }
console
.
log
(new
A
().
sameAs
(new
A
())) // true
// ⚠️ private / protected 只在“类型检查期间”强制——软私有 class
MySafe
{
private
secretKey
= 12345
} const
s
= new
MySafe
()
// console.log(s.secretKey) // 编译错误
console
.
log
(
s
['secretKey']) // 12345 —— 方括号访问被放行(方便单测,但不是真隐私)
// JS 的 #字段 才是硬私有:编译后仍私有、无逃生舱(ES2021 以下用 WeakMap 实现) // 静态成员:属于类本身;可用可见性修饰符、可被继承 // name / length / call 等 Function 原型属性不能用作静态成员名 class
Counter
{
static
count
= 0
static
increment
() {
return ++
Counter
.
count
} }
Counter
.
increment
()
console
.
log
(
Counter
.
count
) // 1
// 不需要“静态类”语法:普通对象或顶层函数就能胜任 // 参数属性:构造参数前加修饰符,自动变成同名字段 class
Params
{
constructor( public readonly
x
: number,
protected
y
: number,
private
z
: number,
) {} // 不需要函数体 } const
p
= new
Params
(1, 2, 3)
console
.
log
(
p
.
x
) // 1
// console.log(p.z) // 编译错误:private

类中的 this

  • 运行时 this 取决于函数怎么被调用obj.getName = c.getName; obj.getName() 打印 obj 而非类名——TS 不改变 JS 的这个行为,但提供两种缓解:
    1. 箭头函数属性getName = () => this.name——不会丢 this;代价是每实例一份内存、子类无法 super.getName
    2. this 参数getName(this: MyClass)——编译期擦除,静态强制正确的调用方式;代价是 JS 调用方仍可能用错。
  • this 类型:动态指向当前类——返回 this 的方法在子类实例上链式调用时保持子类类型;参数写 other: thisother: Box 更严格(派生类的 sameAs 只接受同派生类实例)。
  • this is Type 守卫:方法返回位置写 this is FileRep,配合 if 收窄对象自身;经典用法是惰性校验字段hasValue(): this is { value: T } 校验后移除 undefined
ts
// this 的运行时行为:取决于“函数怎么被调用”——TS 不改变 JS 的这一行为
class 
MyClass
{
name
= 'MyClass'
getName
() {
return this.
name
} } const
c
= new
MyClass
()
const
obj
= {
name
: 'obj',
getName
:
c
.
getName
}
console
.
log
(
obj
.
getName
()) // obj —— 不是 MyClass!
// 修复一:箭头函数属性(代价:每实例一份内存、子类无法 super. 调用) class
ArrowClass
{
name
= 'ArrowClass'
getName
= () => this.
name
} const
g
= new
ArrowClass
().
getName
console
.
log
(
g
()) // ArrowClass —— 不会丢 this
// 修复二:this 参数(编译期被擦除;静态强制正确调用方式) class
ThisParam
{
name
= 'ThisParam'
getName
(
this
:
ThisParam
) {
return this.
name
} } const
tp
= new
ThisParam
()
console
.
log
(
tp
.
getName
()) // ThisParam
// const g2 = tp.getName; g2() // 编译错误:this 上下文不匹配 // this 类型:动态指向“当前类”——链式调用在子类上保持子类类型 class
Box
{
contents
= ''
set
(
value
: string) {
this.
contents
=
value
return this // 返回类型推断为 this,而非 Box }
sameAs
(
other
: this) {
return
other
.
contents
=== this.
contents
// 参数类型也可以是 this
} } class
ClearableBox
extends
Box
{
clear
() {
this.
contents
= ''
return this } } const
cb
= new
ClearableBox
().
set
('hello').
clear
().
set
('again') // 一路保持 ClearableBox
console
.
log
(
cb
.
contents
) // again
// this is Type:基于 this 的类型守卫——惰性校验字段 class
ValueBox
<
T
> {
value
?:
T
hasValue
(): this is {
value
:
T
} {
return this.
value
!==
undefined
} } const
vb
= new
ValueBox
<string>()
vb
.
value
= 'Gameboy'
if (
vb
.
hasValue
()) {
console
.
log
(
vb
.
value
.
toUpperCase
()) // GAMEBOY —— value 的 undefined 已被移除
}

抽象类与类的关系

  • 抽象类不能 new,作为基类存在;抽象成员没有实现,必须由具体(concrete)子类实现——忘了实现会报错。
  • 抽象构造签名:想接受"产出某抽象类实例的构造函数"时,写 ctor: new () => Base(而不是 ctor: typeof Base——后者允许把抽象类本身传进来)。
  • InstanceType<typeof C> 工具类型:从类本身的类型建模 new 出来的实例类型。
  • 类之间按结构比较(与其他类型一样):同构的两个类可以互换;即使没有显式继承也存在子类型关系(Employee 多个 salary 字段就是 Person 的子类型)。
  • ⚠️ 空类没有成员,是万物的超类型——fn(x: Empty) 可以传 window{}、函数……不要写空类
ts
// 抽象类:不能实例化,作为基类存在;抽象成员必须被具体(concrete)子类实现
abstract class 
Base
{
abstract
getName
(): string
printName
() {
console
.
log
('Hello, ' + this.
getName
())
} } // new Base() // 编译错误:不能实例化抽象类 class
Derived
extends
Base
{
getName
() {
return 'world' } } new
Derived
().
printName
() // Hello, world
// 抽象构造签名:接受“产出 Base 实例的具体构造函数” function
greet
(
ctor
: new () =>
Base
) {
const
instance
= new
ctor
()
instance
.
printName
()
}
greet
(
Derived
) // Hello, world
// greet(Base) // 编译错误:抽象构造类型不能赋给非抽象构造类型 // 若写成 ctor: typeof Base,new ctor() 会因“可能是抽象类”而报错 // InstanceType<typeof C>:从“类本身的类型”建模 new 出来的实例类型 class
Point
{
constructor( public
x
: number,
public
y
: number,
) {} } type
PointInstance
=
InstanceType
<typeof
Point
>
function
moveRight
(
point
:
PointInstance
) {
point
.
x
+= 5
} const
point
= new
Point
(3, 4)
moveRight
(
point
)
console
.
log
(
point
.
x
) // 8
// 类之间的关系:结构化比较——同构的类可以互换,甚至无继承也存在子类型关系 class
Point1
{
x
= 0
y
= 0
} class
Point2
{
x
= 0
y
= 0
} const
pp
:
Point1
= new
Point2
() // OK:结构相同
console
.
log
(
pp
.
x
) // 0
// 空类没有成员,在结构化类型系统里是“万物的超类型”——不要写空类!

模块

模块的定义

  • 任何包含顶层 importexport(或顶层 await)的文件都是模块;反之是脚本——内容进入共享的全局作用域(配合 outFile 或多个 <script> 标签使用)。
  • 模块在自己的作用域内执行:不 export 的声明外部不可见;使用别的模块的导出必须 import。
  • 想把没有 import/export 的文件变成模块:加一行 export {};(导出空对象,任何模块目标都适用)。

ES Module 语法

ts
// @filename: maths.ts
export default class 
RandomNumberGenerator
{} // 主导出(每文件至多一个)
export const
pi
= 3.14; // 具名导出(可多个)
export function
absolute
(
num
: number) {
return
num
< 0 ?
num
* -1 :
num
;
} // @filename: app.ts import
RandomNumberGenerator
, {
pi
as
π
,
absolute
} from "./maths.js"; // 默认 + 具名 + 重命名混用
import * as
math
from "./maths.js"; // 全部导出收进一个命名空间
import "./maths.js"; // 仅执行副作用,不引入任何变量

TypeScript 特有的模块语法

  • 类型与值用同样的语法导入导出import { Cat, Dog } from "./animal.js"
  • import type:整条语句只能导入类型——用它导入的东西不能当值用
  • 内联 type 前缀(TS 4.5+):import { createCatName, type Cat, type Dog }——值和类型混在一条语句里,仍标明哪些是类型。
  • 意义:让 Babel / swc / esbuild 这类单文件转译器知道哪些 import 可以安全删除。
  • import fs = require("fs"):与 CommonJS require 一一对应的 TS 语法(保证 TS 源码与 CJS 输出严格对应时用)。
ts
// @filename: animal.ts
export type 
Cat
= {
breed
: string;
yearOfBirth
: number };
export const
createCatName
= () => "fluffy";
// @filename: app.ts import {
createCatName
, type
Cat
} from "./animal.js"; // 内联 type 前缀
const
name
=
createCatName
();
// import type { createCatName } … 后再调用 createCatName() 会报错:type 导入不能当值用

CommonJS 与互操作

  • CommonJS 是 npm 上大多数模块的交付格式:module.exports = { ... } 导出、const maths = require("./maths") 或解构导入——即使写 ESM 语法,懂 CJS 也有助于调试。
  • esModuleInterop:抹平 CJS 与 ESM 在"默认导入 vs 模块命名空间对象导入"上的差异。

解析与输出选项

  • 模块解析:把 import 的字符串定位到具体文件;两种策略——Classic(兼容旧代码)与 Node(复刻 Node.js 的 CJS 解析,外加 .ts / .d.ts 检查);受 moduleResolution / baseUrl / paths / rootDirs 影响。
  • 输出由两个选项决定:
    • target:由代码要运行的最老运行时决定(最老的浏览器 / 最低的 Node 版本 / Electron 等约束)。
    • module:决定模块加载器——ES2020 输出与源码几乎相同;CommonJS 转成 exports.x = ...UMD 生成两头兼容的包装。
  • namespaces:TS 自有的早期模块格式,未废弃(定义文件、DefinitelyTyped 仍在用),但大部分能力 ES Modules 都有——新代码用 ES Modules 对齐 JavaScript 的方向。