搜索 K
主题
typeof 只能在运行时识别 string / number 等原始类型,函数等复杂结构没有对应的运行时机制。const message = "hello!"; message(); —— TS 直接报 This expression is not callable. Type 'String' has no call signatures.,无需等到运行时的 TypeError。// 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) // undefinedundefined 而不抛错。const announcement = "Hello World!";
announcement.toLocaleLowercase(); // 拼写错误(TS 能立即发现)announcement.toLocalLowerCase(); // 也是拼写错误
function flipCoin() {
return Math.random < 0.5; // 忘写括号——报错标注由 twoslash 构建期实时渲染}
const value = Math.random() < 0.5 ? "a" : "b";
if (value !== "a") {
// ...
} else if (value === "b") { // 不可达 —— 基本逻辑错误
}npm install -g typescript → tsc hello.ts(也可用 npx 从本地 node_modules 运行)。tsc 静默完成并产出编译后的 hello.js——没有输出就是好消息。tsc --noEmitOnError hello.ts,报错时不更新输出文件。function greet(person: string, date: Date) 读作「greet 接受 string 类型的 person 和 Date 类型的 date」。Date() 返回的是 string,new Date() 才是 Date 对象——TS 会在传参时立刻指出。let msg = "hello there!" 自动推断为 string——推断结果相同时就别写注解,这是特性不是偷懒。// 类型注解: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'"Hello ".concat(...));默认 target 是 ES5(非常古老),绝大多数场景可以放心指定 --target es2015 或更高。null / undefined——适合 JS 迁移的第一步。strict(CLI --strict 或 tsconfig "strict": true 一键全开,也可逐项控制)。noImplicitAny:类型被隐式推断为 any 时报错——any 越多,用 TS 的意义越小。strictNullChecks:默认 null / undefined 可赋给任何类型,忘记处理它们是无数 bug 之源(“十亿美元错误”);开启后必须显式处理 null / undefined。string、number(JS 没有 int/float 之分,一切都是 number)、boolean——永远用小写;大写的 String / Number / Boolean 指向很少用到的特殊内置类型。number[](等价于 Array<number>,泛型语法后续章节讲);⚠️ [number] 是元组,是另一回事。any:不希望某个值引发类型检查错误时使用——访问任意属性、当函数调用、随意赋值都不报错,等于关闭后续所有类型检查;noImplicitAny 可把隐式 any 变成错误。let obj: any = { x: 0 };
// 以下都不会报编译错误——使用 any 即假定你比 TS 更了解环境
obj.foo();
obj();
obj.bar = 100;
obj = "hello";
const n: number = obj;int x = 0 这种“类型在左”风格);多数情况不需要——TS 会根据初始化器自动推断。刚上手时试着少写注解,你会惊讶 TS 需要的注解有多少。function greet(name: string)——传参时被检查;即使参数没有注解,TS 仍会检查实参个数是否正确。(): number——通常也可省略(由 return 语句推断);显式写通常是为了文档化、防止意外变更或个人偏好。异步函数的返回值注解用 Promise<number>。forEach 回调的参数)——函数所处的上下文决定了它的类型。// 三个最常用的原始类型: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())。// 对象类型:列出属性和类型(分隔符 , 或 ; 均可;属性不写类型则视为 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' }) // ALISSONnumber | string:值可以是成员中的任意一个;提供值容易(匹配任一成员即可),使用值时 TS 只允许对每个成员都合法的操作。typeof id === "string" 分支里 id 就是 string;Array.isArray(x) 同理;else 分支自动排除已收窄的成员。string 和数组都有 slice)可以不收窄直接用。number | string 是对两个类型的值集合取并集;两个集合合并后,对每个成员都成立的事实只剩下两个集合事实的交集(戴帽子的高个子 ∪ 戴帽子的西语者 → 只能确定人人戴帽子)。// 联合类型:值可以是成员中的“任意一个”;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')) // abctype:给任何类型起名字(对象类型、联合类型都行);别名只是别名——不会创造“同一类型的不同版本”,用别名与直接写被别名的类型完全等价。interface:命名对象类型的另一种方式;TS 只关心传给函数的值的结构——这就是结构化类型系统(structurally typed)。extends,type 用交叉类型 &(extends 对编译器通常更高效)。type UserInputSanitizedString = string 只有 type 能做)。interface,直到需要 type 的特性。// 类型别名:给“任何类型”起名字——对象类型、联合类型都行
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 abcdocument.getElementById("main_canvas") as HTMLCanvasElement(等价的尖括号写法 <HTMLCanvasElement>expr 在 .tsx 文件中不可用)。null,只会在后续使用时爆雷。"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"(属性之后可能被改写——类型要同时约束读和写);解法: method: "GET" as "GET" 或调用处 req.method as "GET"。as const:把整个对象转为字面量类型——“类型系统层面的 const”。// 字面量联合:只接受一组已知的值
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.comnull / undefined 类型,行为取决于 strictNullChecks: !(后缀):不做任何显式检查地移除 null / undefined——它也是类型断言,不改变运行时行为,只在确定值不可能为空时使用。// 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 全局唯一bigint(ES2020+):超大整数,BigInt(100) 或字面量 100n。symbol:Symbol("name") 创建全局唯一引用——两个同描述的 Symbol 也永不相等(TS 会直接报"no overlap")。if/else、三元、循环、真值检查……);这些特殊检查叫类型守卫(type guard),把类型精炼到比声明更具体的过程叫收窄(narrowing)。typeof 返回固定的字符串集合:"string" / "number" / "bigint" / "boolean" / "symbol" / "undefined" / "object" / "function"——注意没有 "null"。typeof null === "object"!TS 知道这个怪癖——只用 typeof strs === "object" 检查时,strs 只能收窄到 string[] | null。if 会先把条件强转为 boolean;假值共 7 个(0、NaN、""、0n、null、undefined、false)。if (strs && typeof strs === "object") 可补上 null 检查。if (strs) 会漏掉空字符串的处理——TS 不会拦你,这类问题交给 linter。! 从否定分支过滤:if (!values) return values; 之后 values 就是 number[]。// 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)) // undefinedswitch 与 === / !== / == / != 都能收窄——x === y 成立时二者只能是共同的成员类型。!= null 一举排除 null 和 undefined(== undefined 同理)。in 操作符:"swim" in animal 按“是否拥有该属性”划分联合成员;可选属性会同时出现在两个分支(可游可飞的 Human 两边都在)。instanceof:检查原型链(x instanceof Foo 即 Foo.prototype 是否在 x 的原型链上),适合 new 出来的值(如 Date)。// 相等性收窄: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') // HELLOlet x = 10 或 "hello" 声明为 string | number,之后赋 string 或 number 都合法,赋 boolean 报错。if 块内 return 后,后续代码中该类型成员被移除;控制流可以分裂又合流,同一变量在不同位置观察到不同类型。// 赋值收窄:观察到的类型随赋值变化,但“可赋值性”始终对照声明类型
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 分支里 pet 是 Fish,else 分支里 TS 也知道必然是 Bird。filter 得到 Fish[];类还能用 this is Type 收窄自身;此外还有断言函数(assertion functions)。// 类型谓词(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 一定存在,只能用易错的 ! 断言硬压。interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;if 或 switch)即可把 shape 收窄到具体成员——把你知道的信息传达给类型检查器,就能写出与 JS 无异但类型安全的代码。never——表示不应存在的状态。never 可赋给任何类型;但除 never 自身外没有类型能赋给 never——利用这一点在 switch 的 default 里做穷尽性检查:const _exhaustiveCheck: never = shape——将来给联合新增成员而忘了加 case,这行立刻编译报错。// 可辨识联合:每个成员都带“字面量类型的公共属性” 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 都能调)。// 函数类型表达式:语法类似箭头函数——参数名是必需的!
// (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"])。<Type>(arr: Type[]) 优于 <Type extends any[]>(arr: Type)——后者返回类型只能解析成 any。Func extends (arg: Type) => boolean)是红旗信号。// 泛型:用“类型参数”描述输入与输出之间的对应关系
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 也可能是数组"的值,重载版本解析不了这种调用(一次调用只能命中一个重载)。// 可选参数 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 的类型:JS 规定参数不能叫 this,TS 借用这个语法位——filter: (this: User) => boolean;回调方要用 function 而非箭头函数(箭头函数捕获外层 this)。void:无返回值函数的返回类型;void ≠ undefined。object:一切非原始类型的值——不同于空对象类型 {},更不同于全局的 Object(永远用小写 object);函数值也是 object。unknown:与 any 一样能代表任何值,但不允许对它做任何操作——描述"接受任何值"的函数时用它替代 any(函数体不被污染);返回 unknown 的 safeParse 提醒调用方必须先收窄。never(返回值位置):函数抛异常或终止程序,返回值永远观察不到;也出现在联合被收窄到一无所剩时。Function:全局类型,带 bind / call / apply,可以调用但返回 any——非类型化函数调用,尽量避免;只想接受"任意函数但不调用"时用 () => void 更安全。void 返回类型的怪但合理:上下文函数类型 () => void 的实现可以返回任何值(类型上被忽略)——正因如此 forEach((el) => dst.push(el)) 才合法(push 返回 number);但字面函数定义标注 (): void 就必须真的不返回东西。// 声明 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> 或元组。const args = [8, 5] 推断为 number[](长度不定),展开给 Math.atan2 这类固定参数函数会报错;最直接的修复是 as const(推断为二元组)。function sum({ a, b, c }: ABC),可抽成命名类型减少啰嗦。// 剩余参数:... 写在所有参数之后,类型注解必须是 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{ name: string; age: number }),也可以用 interface 或 type 命名。?:可选性只是说"如果设了这个属性,它最好是指定的类型";strictNullChecks 下读取时是 number | undefined。设默认值的惯用法是解构 + 默认值:function paintShape({ shape, xPos = 0 }: PaintOptions)——函数体内一定有值,调用方仍可不传。{ shape: Shape } 在 JS 里意味着"取 shape 属性并重命名为局部变量 Shape"。readonly 属性:类型检查期间不可写,运行时无影响;⚠️ 两个要点: home.resident.age++ 合法)。readonly 不参与类型兼容性检查——可写类型可以赋给 readonly 类型,再通过可写别名修改。[index: string]: number——不知道属性名但知道值的形状(字典模式);索引键只能是 string / number / symbol / 模板字符串模式及其联合。 obj[100] 就是 obj["100"])。obj.property 也算 obj["property"]);不同类型的属性可以用联合类型的索引签名容纳。readonly。// 可选属性 + 解构默认值:调用方可不传,函数体内一定有值
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 dictcolour vs color 的拼写错误在纯 JS 里会静默失败)。{ width: 100, opacity: 0.5 } as SquareConfig。[propName: string]: unknown(确定对象就是会有额外属性时)。extends:复制被扩展类型的成员再添加新成员——减少样板、传达"这些声明相关"的意图;interface 可多重扩展。&:组合已有对象类型,拥有全部成员。string & number = never——能编译,但没有值能满足,用的时候才爆。// 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 | null、type OneOrMany<T> = T | T[]。number[] 只是 Array<number> 的简写;Map<K, V>、Set<T>、Promise<T> 都是泛型数据结构。// 泛型对象类型: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) // 2ReadonlyArray<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[])。as const 的数组字面量就推断为 readonly 元组。// 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)) // 5keyof 类型操作符——从对象类型取键。typeof 类型操作符——从值取类型。Type['a']——取类型的子集。number 失去复用性;用 any 虽然"通用"但丢失类型信息(传入 number,只知道返回"任意类型")。function identity<Type>(arg: Type): Type 用 Type 捕获实参类型并"运"到返回值,既通用又精确。identity<string>("myString");类型实参推断(最常用)——编译器根据实参自动填 Type。arg: Type[] 表示"Type 的数组",.length 自然可用。// 泛型 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——类型参数名可以不同,只要数量和用法对得上;也可写成对象字面量类型的调用签名。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 自动推断变型;只在极罕见的循环类型场景下用,且必须与结构行为一致,不能用来"强迫"某种变型。// 泛型接口:类型参数放在“接口”上会锁定底层调用签名的类型
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) // truekeyof 取对象类型的键,产出字符串或数字字面量联合:keyof { x: number; y: number } = "x" | "y"。{ [n: number]: unknown } → number;{ [k: string]: boolean } → string | number(JS 对象键总被强转成字符串,obj[0] ≡ obj["0"])。typeof;TS 增加了类型上下文的 typeof——引用变量或属性的类型。ReturnType<typeof f>——先 typeof 从值 f 拿到函数类型,再取返回类型(直接写 ReturnType<f> 报错:"f 是值不是类型")。typeof 只能用在标识符(变量名)或其属性上——不能写 typeof msgbox(...)(避免"以为在执行代码"的陷阱)。// 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" 可以。// 索引访问类型: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) // 18SomeType extends OtherType ? TrueType : FalseType——左边可赋给右边就取 true 分支,语法与 JS 三元一致。createLabel 若用重载要写 3 个(string / number / 联合),每支持一种新类型重载数就指数增长;一个条件类型 NameOrId<T> = T extends number ? IdLabel : NameLabel 全部搞定。T 满足检查条件——T extends { message: unknown } ? T["message"] : never 既接受任意类型又能兜底。// 条件类型: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 3infer:在 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)[]。// 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] —— 解除可选(全部必填)。// 映射类型:基于索引签名语法,用 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 子句重映射键:[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。// 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 }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>。// 模板字面量类型:拼接字面量类型;联合出现在插值位时“交叉相乘”
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 Helloany;初始化器会用于推断类型(如 x = 0 → number)。strictPropertyInitialization:字段必须在构造函数本身初始化——TS 不分析构造函数调用的方法(派生类可能覆盖它们导致漏初始化);确定由外部初始化(如注入库)时用明确赋值断言 !:name!: string。readonly 字段:只允许在构造函数内赋值。this. 之前必须先 super()。this. 访问字段——裸名字永远指向外层作用域。readonly;不注解 setter 参数时从 getter 返回类型推断;TS 4.3+ 允许 get/set 类型不同(如 get 返回 number、set 接受 string | number | boolean)。无额外逻辑的 get/set 对没什么用——直接暴露公共字段即可。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) // 0implements:只检查类能否被当作接口类型对待——完全不改变类的类型或推断!常见错误:以为 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)。// 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) // derivedpublic(默认,可不写)/ protected(自身 + 子类可见)/ private(仅自身,子类也不行)。protected);不允许跨层级访问兄弟类的 protected 成员。private / protected 只在类型检查期间强制——运行时 in、属性查找、甚至类型检查内的方括号访问 s["secretKey"] 都能碰到;JS 的 # 字段才是硬私有(编译后仍私有,ES2021 以下用 WeakMap 实现)。需要真隐私就用闭包 / WeakMap / # 字段。name / length / call 等 Function 原型属性不能用作静态成员名;TS 不需要"静态类"语法——普通对象或顶层函数就够了;静态块可以访问静态私有字段做初始化。Box.defaultValue 槽位。public / private / protected / readonly,自动变成同名字段——省掉声明 + 赋值样板。// 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) // 编译错误:privatethis 取决于函数怎么被调用:obj.getName = c.getName; obj.getName() 打印 obj 而非类名——TS 不改变 JS 的这个行为,但提供两种缓解: getName = () => this.name——不会丢 this;代价是每实例一份内存、子类无法 super.getName。this 参数:getName(this: MyClass)——编译期擦除,静态强制正确的调用方式;代价是 JS 调用方仍可能用错。this 类型:动态指向当前类——返回 this 的方法在子类实例上链式调用时保持子类类型;参数写 other: this 比 other: Box 更严格(派生类的 sameAs 只接受同派生类实例)。this is Type 守卫:方法返回位置写 this is FileRep,配合 if 收窄对象自身;经典用法是惰性校验字段:hasValue(): this is { value: T } 校验后移除 undefined。// 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、{}、函数……不要写空类。// 抽象类:不能实例化,作为基类存在;抽象成员必须被具体(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
// 空类没有成员,在结构化类型系统里是“万物的超类型”——不要写空类!import 或 export(或顶层 await)的文件都是模块;反之是脚本——内容进入共享的全局作用域(配合 outFile 或多个 <script> 标签使用)。export {};(导出空对象,任何模块目标都适用)。// @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"; // 仅执行副作用,不引入任何变量import { Cat, Dog } from "./animal.js"。import type:整条语句只能导入类型——用它导入的东西不能当值用。type 前缀(TS 4.5+):import { createCatName, type Cat, type Dog }——值和类型混在一条语句里,仍标明哪些是类型。import fs = require("fs"):与 CommonJS require 一一对应的 TS 语法(保证 TS 源码与 CJS 输出严格对应时用)。// @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 导入不能当值用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 生成两头兼容的包装。