TS·01 值、类型、函数:声明、标注与推断
这是「读懂 pi 的 TypeScript」系列的第 1 篇正文。上一篇(入门与心智模型)解释了 TS = JS + 编译期类型检查;这一篇打地基:如何声明变量、给它们贴类型、写带类型的函数。这三样是你读 pi 任何一行代码都绕不开的基础。
我们不空讲语法。三章分别对应 pi 仓库里三个真实的小文件:一个计算器工具 calculate、一个报时工具 getCurrentTime、一个事件流类 EventStream 的 push 方法。读完这篇,你就能逐字读懂这三个函数的签名——它们恰好覆盖了变量、参数、返回值、可选参数、数组、对象字面量这些最核心的形状。后面几篇讲的接口、联合、泛型,都建在这一篇之上。
1. 变量与基本类型:标注 vs 推断
1.1 直觉
Python 里变量一律 x = 1,重新赋值、改类型都随意。TS 有两个声明关键字:const(常量,不可重新绑定)和 let(变量,可重新赋值)。这个区分 Python 没有——最接近的是 Final 标注,但那只是提示,而 TS 的 const 是编译器强制的。
类型方面,TS 有 string、number(不分 int/float,统一一个)、boolean 等基本类型。你可以显式标注 const n: number = 1,也可以不写、让编译器从右侧的值自动推断。两者产生的类型完全一样。什么时候写、什么时候省,是本章的重点。这里要建立的一个关键认知是:TS 里「变量有没有写类型」不影响它「有没有类型」——不写只是把活儿交给编译器,变量始终是静态类型的。这跟 Python「不写类型就是动态」的直觉正好相反,是从 Python 迁移过来最需要扭转的一点。
1.2 最小 demo
// 教学示例 — 非生产代码
const name: string = "pi"; // 显式标注为 string
let count = 0; // 推断为 number,因为右侧是 0
count = count + 1; // let 可以重新赋值,OK
// name = "other"; // 报错:const 不可重新绑定
const enabled = true; // 推断为 boolean
const nothing = null; // 推断为 null
// 函数返回值也能推断:add 的返回类型是 number
function add(a: number, b: number) {
return a + b;
}
const sum = add(1, 2); // sum 被推断为 number
注意 count 和 enabled 没写类型,但它们不是 Python 那种动态变量——编译器已经把它们钉死成 number 和 boolean 了。给 count 赋一个字符串会立刻报错。
1.3 正式化
声明的通用形式是:const | let 名字 [: 类型] = 值。
const:声明后不能再用=重新绑定。注意它管的是「绑定」不是「内容」——const arr = []; arr.push(1)完全合法,因为你没有重新绑定arr。这跟 Python 里Final的语义几乎一致。let:可重新赋值,但类型固定。let x = 0; x = "a"报错。- 还有个
var,是 JS 的历史遗留,作用域规则怪异,pi 全仓库不用,你也不要用。
标注 vs 推断的取舍:局部变量、有初始值时,省略标注,让推断来做——写 const n = 0 而不是 const n: number = 0,更简洁且不会撒谎。需要显式标注的场景主要是:函数参数(推断不了,必须写)、想约束一个比推断结果更窄或更宽的类型、以及导出的 API 边界(下一章的返回值标注)。
基本类型名都是小写:string、number、boolean、null、undefined。写成大写的 String、Number 是另一回事(包装对象),几乎永远不要用。
顺带说一下 null 和 undefined 这对——Python 只有一个 None,TS 却有两个「空」。粗略地说,undefined 是「没赋值 / 没这个东西」(变量声明了没给值、可选参数没传、对象没这个字段),null 是「主动置空」。pi 的代码里两者都用,但更偏好 undefined;你现在只要知道它们都表示「空」、且各自是独立的类型即可,细节留到窄化与类型守卫那篇处理「值可能为空」的判断。
1.4 代码引用
看 pi 里一个真实的工具函数——把数学表达式求值的计算器:
pi/packages/agent/test/utils/calculate.ts:L9-L16 — calculate 工具:string 参数 + 具名返回类型 + catch 里的类型标注
export function calculate(expression: string): CalculateResult {
try {
const result = new Function(`return ${expression}`)();
return { content: [{ type: "text", text: `${expression} = ${result}` }], details: undefined };
} catch (e: any) {
throw new Error(e.message || String(e));
}
}
对照本章讲的点:
expression: string—— 参数必须显式标注,推断在这里无能为力(见下一章)。const result = new Function(...)()—— 局部变量没写类型,靠推断。new Function(...)()的返回是any,所以result是any。catch (e: any)—— TS 里 catch 到的错误默认类型是unknown,这里显式标注成any才能直接访问e.message。这是一个非常常见的写法(严格模式下unknown更安全,但any图省事)。- 反引号字符串
`return ${expression}`是模板字符串,相当于 Python 的 f-string:`...${变量}...`↔f"...{变量}..."。
: CalculateResult 是返回类型标注,属于下一章的内容,这里先放着。
1.5 洞察
- 默认用推断,别到处写
: number。TS 惯例是「参数写类型、局部变量靠推断」;满屏冗余标注反而是新手味。 const是你的默认选择,只有确实要重新赋值时才换let——pi 里绝大多数声明都是const。number不分整数和浮点,1和1.5同类型;没有 Python 那种int/float之分,也没有大整数(除非用bigint)。做数组索引、位运算时要留意这点,因为编译器不会帮你区分「这应该是整数」。const管绑定不管内容这一点值得再强调一次:const obj = { n: 1 }; obj.n = 2是合法的,你改的是对象内部,没有重新绑定obj。想连内容也锁死,得靠readonly或as const(后续篇章),const本身做不到。
2. 函数:参数与返回标注、可选/默认参数、箭头函数
2.1 直觉
函数是 pi 的主体。TS 函数和 Python 的 def f(x: int) -> str: 心智几乎一一对应:参数可标类型,返回值可标类型。差别在语法细节和两种写法:传统的具名函数 function f() {},以及箭头函数 const f = () => {}(类似 Python 的 lambda,但可以有完整函数体)。
可选参数用 param?: 表示「可以不传」,对应 Python 的 x=None 或 Optional;默认参数 param = 值 对应 Python 的默认值。两者是不同的东西,本章讲清楚。
Python 对照:Python 里
def f(x: int = 0)用「有默认值」同时表达了「可以不传」;TS 把这两件事拆成了两个语法——x?: number(可不传,不传就是undefined)和x = 0(不传就用0)。迁移时别把?和= 0当同义词。
2.2 最小 demo
// 教学示例 — 非生产代码
// 具名函数:参数标类型,返回标 string
function greet(who: string): string {
return `hello, ${who}`;
}
// 箭头函数:同样的签名,存进 const
const greet2 = (who: string): string => `hello, ${who}`;
// 可选参数 suffix?:调用时可省略,类型是 string | undefined
function label(name: string, suffix?: string): string {
return suffix ? `${name}.${suffix}` : name;
}
label("pi"); // OK,suffix 为 undefined
label("pi", "ts"); // OK
// 默认参数:不传时用默认值,count 类型仍是 number
function repeat(text: string, count = 2): string {
return text.repeat(count);
}
repeat("ab"); // "abab"
greet 和 greet2 完全等价,只是写法不同。箭头函数省掉了 function 关键字,=> 右边如果是单个表达式还能省掉 { return ... }——上面 greet2 就直接返回模板字符串。
2.3 正式化
- 参数标注是强制的(在 pi 的严格配置下):
function f(x)会报错「x 隐式为 any」,必须写f(x: string)。这跟 Python 的可选类型提示不同——这里不写就编译不过。 - 返回类型标注是可选的:
function add(a: number, b: number)不写返回类型,编译器推断为number。但导出的公开函数建议显式写返回类型,当作契约,也让错误在函数内部就暴露而不是传导到调用处。 - 可选参数
param?::等价于类型param: T | undefined,且必须排在必填参数后面。调用时可以整个省略。 - 默认参数
param = 值:不传时用默认值。有默认值时通常不用再写?,类型从默认值推断(如上count = 2推断为number)。 - 箭头函数
(args) => body:body 是表达式时直接当返回值;是语句块{ ... }时要显式return。它还有个和this绑定相关的重要区别,留到类与模块那篇。
一个坑:可选参数 suffix?: string 和「参数类型为 string | undefined」不完全一样——前者调用时可以不写,后者必须传(哪怕传 undefined)。
2.4 代码引用
pi 里的报时工具用到了可选参数:
pi/packages/agent/test/utils/get-current-time.ts:L6-L22 — getCurrentTime:可选参数 timezone? + async 返回
export async function getCurrentTime(timezone?: string): Promise<GetCurrentTimeResult> {
const date = new Date();
if (timezone) {
try {
const timeStr = date.toLocaleString("en-US", {
timeZone: timezone,
dateStyle: "full",
timeStyle: "long",
});
return {
content: [{ type: "text", text: timeStr }],
details: { utcTimestamp: date.getTime() },
};
} catch (_e) {
throw new Error(`Invalid timezone: ${timezone}. Current UTC time: ${date.toISOString()}`);
}
}
// ...
}
对照本章讲的点:
timezone?: string—— 可选参数。调用方可以getCurrentTime()也可以getCurrentTime("Asia/Shanghai")。函数体里if (timezone)就是在处理「没传」的情况:没传时timezone是undefined,if判为假,走后面的默认分支。const date = new Date()—— 又一个靠推断的局部变量,类型是Date。catch (_e)—— 这次没标类型,变量名前缀_是 pi 的约定,表示「这个参数我不用」(避免 unused 警告)。async和Promise<...>是异步的标记:async函数总是返回Promise。这里只需知道它是异步函数,细节全部留给异步与流式那一篇。
2.5 洞察
- 参数必须标类型,返回值可省——记住这个不对称:pi 里几乎每个函数参数都有类型,但很多内部函数省略返回类型靠推断。
- 可选
?只能放末尾,且不能和默认值语义混淆:?是「可以不传→可能 undefined」,= 值是「不传→用这个值,不会 undefined」。 - 箭头函数和具名函数在「是什么值」上等价,但 pi 里模块顶层的工具函数多用
function(可提升、便于导出),回调和短函数多用箭头。
3. 数组、对象字面量与内联类型、void
3.1 直觉
前两章的值都是标量。真实代码里数据是数组和对象。TS 的数组类型写作 T[](如 string[] 是字符串数组),对应 Python 的 list[str]。对象的类型可以内联写在原地:{ type: string; text: string } 描述「有 type 和 text 两个字符串字段的对象」,类似 Python 的 TypedDict,但可以匿名、就地使用。
还有个特殊返回类型 void:表示「这个函数不返回有意义的值」,对应 Python 里返回 None(或标注 -> None)的函数。真实代码里大量函数属于这一类——它们做的是副作用:打印日志、把数据推进队列、触发一个事件,而不是算一个值返回给你。给这类函数标 void,是在告诉调用方「别指望我的返回值」。
3.2 最小 demo
// 教学示例 — 非生产代码
// 数组类型:T[]
const names: string[] = ["a", "b"];
const nums = [1, 2, 3]; // 推断为 number[]
// 内联对象类型:字段就地声明
const point: { x: number; y: number } = { x: 1, y: 2 };
// 数组套对象:每个元素形如 { type, text }
const items: { type: string; text: string }[] = [
{ type: "text", text: "hello" },
];
// 返回 void 的函数:只做事,不产出值
function log(msg: string): void {
console.log(msg);
// 没有 return,或 return; 都行
}
nums 没写类型,推断成 number[]。items 的类型 { type: string; text: string }[] 读法是「一个数组,元素都是有 type 和 text 字段的对象」——注意 [] 贴在整个对象类型后面。
3.3 正式化
- 数组类型
T[]:string[]、number[],甚至{ ... }[]。还有个等价写法Array<string>(泛型形式,见泛型),pi 两种都用,T[]更常见、更短。 - 内联对象类型:
{ 字段: 类型; 字段: 类型 },字段间用;或,分隔。就地写、不起名,适合一次性的形状;要复用就该抽成interface或type(下一篇 interface 与 type)。 - 对象字面量:
{ type: "text", text: "..." }是值(用,分隔),别和上面的类型混淆——同样的花括号,一个描述形状、一个是具体数据。TS 会检查字面量是否匹配声明的类型,多写一个字段都会报「对象字面量只能指定已知属性」。 void:函数返回类型,表示无有意义返回。函数体可以完全不写return,或写空的return;。它和undefined相关但不完全相同——void表达「别用我的返回值」的意图。
3.4 代码引用
pi 的 ai 包里有个事件流类 EventStream,它的 push 方法就是典型的 : void:
pi/packages/ai/src/utils/event-stream.ts:L21-L36 — push 方法:返回 void 的副作用函数
push(event: T): void {
if (this.done) return;
if (this.isComplete(event)) {
this.done = true;
this.resolveFinalResult(this.extractResult(event));
}
// Deliver to waiting consumer or queue it
const waiter = this.waiting.shift();
if (waiter) {
waiter({ value: event, done: false });
} else {
this.queue.push(event);
}
}
对照本章讲的点:
push(event: T): void—— 返回类型void:push只把事件塞进队列或交给等待者,不产出值。函数体里的return(第一行if (this.done) return;)是提前退出,不带值,正是void的用法。- 这里的
T是泛型类型参数(类定义是class EventStream<T, R = T>),意思是「元素类型待定,用的时候再填」。本篇不展开,只需知道T是个占位类型,详见泛型。
再回头看第一章 calculate 的返回值,那里有数组套对象字面量的真实例子:
pi/packages/agent/test/utils/calculate.ts:L4-L6 — CalculateResult:数组 + 内联对象类型
export interface CalculateResult extends AgentToolResult<undefined> {
content: Array<{ type: "text"; text: string }>;
details: undefined;
}
content: Array<{ type: "text"; text: string }> —— 一个数组,元素是 { type: "text"; text: string }。注意 type: "text" 不是 type: string,而是字符串字面量类型「只能是 "text" 这个值」(见联合与可辨识联合)。它对应的运行时数据,就是 calculate 里返回的 content: [{ type: "text", text: ... }]——类型和值花括号长得一样,一个在 interface 里、一个在 return 里。
3.5 洞察
T[]和Array<T>等价,选短的T[];但元素类型复杂时(如上面的对象),Array<{...}>有时读起来更清楚,pi 里两种都能见到。- 类型的花括号和值的花括号是两码事:
: { x: number }是类型,= { x: 1 }是值。看到:后面就是类型,=后面就是数据。 void是「别用返回值」的信号,不是「返回 undefined」——用在只做副作用(打印、入队、发事件)的函数上;pi 里事件推送、注册回调这类方法大量用它。- 内联对象类型适合一次性形状,复用就抽出来:同一个
{ type: string; text: string }在 pi 里出现了很多次,所以它被抽成了带名字的类型(下一篇讲的interface/type)。看到内联类型开始重复,就是该起名的信号。
讨论 / Comments
评论托管在本仓库的 GitHub Discussions, 需 GitHub 账号。