Appearance
TypeScript 语法细节
一. 联合类型和交叉类型
联合类型
TypeScript 的类型系统允许我们使用多种运算符,从现有类型中构建新类型。
我们来使用第一种组合类型的方法: 联合类型(Union Type)
- 联合类型是由两个或者多个其他类型组成的类型;
- 表示可以是这些类型中的任何一个值;
- 联合类型中的每一个类型被称之为联合成员(union's members);
typescript
function printId(id: number | string) {
console.log("你的id是:", id);
}
printId(10);
printId("10");
传入给一个联合类型的值是非常简单的: 只要保证是联合类型中的某一个类型的值即可
- 但是我们拿到这个值之后,我们应该如何使用它呢?因为它可能是其中任何一种类型。
- 比如我们拿到的值可能是 string 或者 number,我们就不能直接对其调用 string 上的一些方法;
那么我们怎么处理这样的问题呢?
- 我们需要使用 类型收窄(narrow)(后续我们还会专门讲解缩小相关的功能);
- TypeScript 可以根据我们缩小的代码结构,推断出更加具体的类型;
typescript
function printId(id: number | string) {
if (typeof id === "string") {
// 在此代码块内,id已经被确定为string类型,因此可以访问toUpperCase方法
console.log("你的id是", id.toUpperCase());
} else {
// 因为只有两个联合成员,因此id被确认为number类型
console.log("你的id是", id);
}
}
交叉类型
前面我们学习了联合类型,联合类型表示多个类型中一个即可
还有一种类型合并,就是交叉类型
- 交叉类型表示需要同时满足多个类型的条件;
- 交叉类型使用
&符号;
我们来看下面的交叉类型:
- 表达的含义是id 需要同时满足 number 和 string 类型;
- 但是有同时满足是一个 number 又是一个 string 的值吗?其实是没有的,所以 id 其实是一个never类型;
typescript
const id: number & string;
- 所以,在开发中,我们进行交叉时,通常是对对象类型进行交叉的:
typescript
const obj: {
height: number;
} & {
age: number;
} = {
height: 1.88,
age: 18,
};
二. 类型别名和接口
类型别名
- 在前面,我们通过在类型注解中编写 对象类型 和 联合类型
- 如果类型很长,写在一起很臃肿
- 或者当我们想要多次在其他地方使用时,可能要编写多次重复代码
- 所以我们可以给对象类型起一个别名:
typescript
type Point = {
x: number
y: number
}
function printPoint(point: Point) {
console.log(point.x, point.y)
}
printPoint({ x:20, y: 30 })
type ID = number | string
function printId(id: ID) {
console.log("您的id:", id)
}
接口的声明
- 在前面我们通过 type 可以用来声明一个对象类型:
- 类型别名的写法和用法思路类似于赋值
typescript
type Point = {
x: number;
y: number;
};
- 对象的另外一种声明方式就是通过接口来声明:
- 行位符号的规则和 type 一样
typescript
interface Point {
x: number;
y: number;
}
- 那么它们有什么区别呢?
- 类型别名和接口非常相似,在定义对象类型时,大部分时候,你可以任意选择使用。
- 接口的几乎所有特性都可以在 type 中使用(后续我们还会学习 interface 的很多特性);
- 别的语言里接口一般只定义行为,而 ts 里的接口可以像对象一样定义属性,注意这个细微的区别
interface 和 type 区别
- 我们会发现 interface 和 type 都可以用来定义对象类型,那么在开发中定义对象类型时,到底选择哪一个呢?
- 如果是定义非对象类型,通常推荐使用 type
- 如果是定义对象类型,那么他们是有区别的:
- interface 可以重复的定义某个接口,来添加属性和方法,表现为最终属性和方法是所有定义体里的属性方法之和
- 而 type 定义的是别名,别名是不能重复定义的;
typescript
interface IPerson {
name: string;
running: () => void;
}
interface IPerson {
age: number;
}
const person: IPerson = {
name: "why",
running: () => {
console.log("running");
},
age: 18,
};
// ❌ error: Duplicate identifier 'Person'
type Person = {
name: string;
running: () => void;
};
// ❌ error: Duplicate identifier 'Person'
type Person = {
age: number;
};
- 除此之外,interface 还支持继承
- 所以,interface 可以为现有的接口提供更多的扩展。
typescript
interface IPerson {
name: string;
age: number;
}
interface IKun extends IPerson {
kouhao: string;
}
- 接口还有很多其他的用法,我们会在后续详细学习
- 总结:interface 属于对象专精,只能声明对象,如果声明对象,推荐使用 interface;如果不是对象类型,使用 type
三. 类型断言和非空断言
类型断言 as
- 有时候 TypeScript无法获取具体的类型信息,这个我们需要使用类型断言(Type Assertions)。
- 比如我们通过
document.getElementById,TypeScript 只知道该函数会返回HTMLElement,但并不知道它具体的类型: - 断言前
myEl是HTMLElement || null, 就算不是 null,HTMLElement上也没有 src 属性,因此无法访问 - 断言后,编译器直接相信
myEl是更具体的HTMLImageElement,它上面有 src 属性,因此可以访问 src
- 比如我们通过


- TypeScript 只允许类型断言转换为 更具体 或者 不太具体 的类型版本,此规则可防止不可能的强制转换:
- 也就是说只能转换成父类型或者子类型,不能同级类型转换
- 但是可以通过一些技巧再次绕过这个限制,但是一定要谨慎!
typescript
const name = "coderwhy" as number;
// ❌ string类型和number类型是同级的类型,不能进行断言
const name = "coderwhy" as unknown as number;
// ✅ 每一步都是正确的类型断言,因此不会报错
- 类型断言是对编译器的“欺骗”,使他忽视可能存在的问题,不能真正的直接解决问题
- 必须谨慎使用,确认没问题再用
非空类型断言 !
- 当我们编写下面的代码时,在执行 ts 的编译阶段会报错:
- 这是因为传入的 message 有可能是为 undefined 的,这个时候是不能执行方法的;
typescript
function printMsg(msg?: string) {
// ❌ error TS2532: Object is possibly 'undefined'
console.log(msg.toUpperCase());
}
- 但是,我们确定传入的参数是有值的,这个时候我们可以使用非空类型断言:
- 非空断言使用的是
!,表示可以确定某个标识符是有值的,跳过 ts 在编译阶段对它的检测;
- 非空断言使用的是
- 与 es6 可选链语法的区别是
- 可选链是如果不存在就不访问,而非空类型断言是确定一个有值直接访问
- 可选链不能进行赋值操作,而非空断言可以
- 二选一的话,访问属性一般使用可选链,属性赋值只能选择非空断言
- 也可以用朴实无华的 if 判断,很安全!
typescript
interface IPerson {
name: string;
age: number;
friend?: {
name: string;
};
}
const info: IPerson = {
name: "why",
age: 18,
};
console.log(info.friend?.name);
info.friend?.name = "zhangsan"; // ❌ 赋值表达式的左边不能是可选链访问
info.friend!.name = "zhangsan"; // ✅ 有点危险...
if (info.friend) {
info.friend!.name = "zhangsan"; // ✅ 安全!
}
- 必须谨慎使用,确认有属性才能使用,否则可能造成运行时错误
typescript
function printMsg(msg?: string) {
// ✅ 编译期不报错
console.log(msg!.toUpperCase());
}
printMsg(); // ❌ 运行时报错,因为不能访问undefined的属性
四. 字面量类型和类型缩小
字面量类型
- 之前我们已经提过字面量类型了
- 在 const 声明字符串、数字、boolean 类型的时候会被推导为字面量类型,其他情况一般要手动指定字面量类型

- 那么这样做有什么意义呢?
- 一般情况下,只有一个值的字面量类型没有什么意义,但是我们可以将多个字面量类型联合在一起;
- 通常用在想指定只能输入几个确定的值的情况下

字面量推理
- 我们来看下面的代码:

- 这是因为我们的对象在进行字面量推理的时候,info 其实是一个 {url: string, method: string},并不会推导为字面量类型,所以我们没办法将一个 string 赋值给一个 字面量 类型。
typescript
// 解决方法1:类型断言
request(info.url, info.method as "GET");
// 解决方法2:让info类型为字面量类型
const info = {
url: "http://codercba.com",
method: "GET",
} as const;
- 注意变成了 readonly 属性,后面还会提,而字面量属性也属于 string 类型,当然可以传入

类型缩小
什么是类型缩小呢?
- 类型缩小的英文是 Type Narrowing(也有人翻译成类型收窄);
- 我们可以通过类似于
typeof padding === "number"的判断语句,来改变 TypeScript 的执行路径; - 在给定的执行路径中,我们可以缩小比声明时更小的类型,这个过程称之为 缩小( Narrowing );
- 而我们编写的
typeof padding === "number"可以称之为 类型保护(type guards);
常见的类型保护有如下几种:
- typeof
- 平等缩小(比如===、!==)
- instanceof
- in
- 等等...
typeof
- 在 TypeScript 中,利用 typeof 检查类型是一种类型保护:
- typeof 判断的范围内,类型会被缩小到对应类型,使相应操作能安全进行
typescript
type ID = number | string;
function printId(id: ID) {
if (typeof id === "string") {
console.log(id.toUpperCase());
} else {
console.log(id);
}
}
- typeof 的运算结果和 js 的一样,但是在 类型上下文(type context) 时会有特别的效果,后面会学到

平等缩小
- 我们可以使用 switch 或者相等的一些运算符来表达相等性(比如===, !==, ==, and != ):
typescript
type Direction = "left" | "right" | "center";
function turnDirection(direction: Direction) {
switch (direction) {
case "left":
console.log("调用left方法");
break;
case "right":
console.log("调用right方法");
break;
case "center":
console.log("调用center方法");
break;
default:
console.log("调用默认方法");
}
}
instanceof
- JavaScript 有一个运算符来检查一个值是否是另一个值的 “实例”:
typescript
function printVal(date: Date | string) {
if (date instanceof Date) {
console.log(date.toLocaleString());
} else {
console.log(date);
}
}
in 操作符
- Javascript 有一个运算符,用于确定对象是否具有带指定
名称的属性:in 运算符- 如果指定的属性在指定的对象或其原型链中,则in 运算符返回 true;
typescript
type Fish = {
swim: "yes";
run: "no";
};
type Dog = {
swim: "yes";
run: "yes";
};
function checkSkill(animal: Fish | Dog) {
if ("swim" in animal) {
animal.swim();
} else {
animal.run();
}
}
五. TypeScript 函数类型
函数类型表达式
- 在 JavaScript 开发中,函数是重要的组成部分,并且函数可以作为一等公民(可以作为参数,也可以作为返回值进行传递)。
- 那么在使用函数的过程中,函数是否也可以有自己的类型呢?
- 我们可以编写函数类型的表达式(Function Type Expressions),来表示函数类型;
- 形式为
(参数1: 参数1类型, 参数2: 参数2类型, ...) => 返回值类型,没有返回值就是 void
typescript
type CalcFunc = (num1: number, num2: number) => number;
function calc(fn: CalcFunc) {
console.log(fn(20, 30));
}
function sum(num1: number, num2: number) {
return num1 + num2;
}
function mul(num1: number, num2: number) {
return num1 * num2;
}
calc(sum);
calc(mul);
在上面的语法中
(num1: number, num2: number) => number,代表的就是一个函数类型:- 接收两个参数的函数:num1 和 num2,并且都是 number 类型;
- 并且这个函数的返回值是 number 类型,如果没有返回值就是 void;
注意:在某些语言中,可能参数名称 num1 和 num2 是可以省略,但是 TypeScript 是不可以的
Note that the parameter name is required. The function type
(string) => voidmeans “a function with a parameter namedstringof typeany“!
调用签名(Call Signatures)
- 在 JavaScript 中,函数除了可以被调用,自己也是可以有属性值的。
- 然而前面讲到的函数类型表达式并不能支持声明属性;
- 如果我们想描述一个带有属性的函数,我们可以在一个对象类型中写一个调用签名(call signature);
typescript
interface ICalcFn() {
name: string
(num1: number, num2: number): void
}
function calc(calcFn: ICalcFn) {
console.log(calcFn.name)
calcFn(10, 20)
}
- 注意这个语法跟函数类型表达式稍有不同,在参数列表和返回的类型之间用的是
:而不是=> - 如何选择调用签名和函数类型表达式?
- 如果只是想描述函数类型本身,使用函数类型表达式
- 如果想描述函数可以被调用,同时作为对象也有其他属性时候,使用函数调用签名
构造签名 (Construct Signatures)
- JavaScript 函数也可以使用
new操作符调用,当被调用的时候,TypeScript 会认为这是一个构造函数(constructors),因为 他们会产生一个新对象。- 如果不使用构造签名,无法推断出类 new 出来的结果类型
- 你可以写一个构造签名( Construct Signatures ),方法是在调用签名前面加一个
new关键词;
typescript
interface IPerson {
new (name: string): Person;
}
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
}
function factory(ctor: IPerson) {
return new ctor("why");
}
factory(Person);
参数的可选类型
- 我们可以指定某个参数是可选的:
typescript
function foo(x: number, y?: number) {
console.log(x, y);
}
- 这个时候这个参数 y 依然是有类型的,它是什么类型呢? number | undefined
Although the parameter is specified as type
number, thexparameter will actually have the typenumber | undefinedbecause unspecified parameters in JavaScript get the valueundefined.
- 另外可选类型需要在必传参数的后面:

参数个数不检测?
TypeScript 对传入的函数类型的参数个数不进行检测(校验)
还记得之前说的吗,基于上下文的类型推导(Contextual Typing)推导出函数返回类型为 void 的时候,并不会强制函数一定不返回内容,那实际上现在再补充一点,也不强制函数一定传入所有的参数
typescript
type CalcType = (num1: number, num2: number) => void;
function calc(calcFn: CalcType) {}
function foo(): number {
return 1;
}
calc(foo);
之前是用 forEach 举例子,实际上我们还可以拿它举例子,我们知道 forEach 的函数签名里有
item和index和arr,然而我们很多时候传入的函数只用到了item,如果每次必须传入所有参数,必然很不方便,这就是 TS 不限制参数个数的原因有些人可能觉得不传,那不应该是可选参数吗?答案是否定的,官方也给出了解答
❌ Don’t use optional parameters in callbacks unless you really mean it:
typescript/* WRONG */ interface Fetcher { getObject(done: (data: unknown, elapsedTime?: number) => void): void; }This has a very specific meaning: the
donecallback might be invoked with 1 argument or might be invoked with 2 arguments. The author probably intended to say that the callback might not care about theelapsedTimeparameter, but there’s no need to make the parameter optional to accomplish this — it’s always legal to provide a callback that accepts fewer arguments.✅ Do write callback parameters as non-optional:
typescript/* OK */ interface Fetcher { getObject(done: (data: unknown, elapsedTime: number) => void): void; }
- 但是请注意,只是不校验传入的函数类型,函数执行的时候还是不能省略的! 也就是官方所说的,不要使用可选参数,除非你真的是那个意思(指函数执行的时候也想少传)
typescript
type CalcType = (num1: number, num2: number) => void;
function calc(calcFn: CalcType) {
calcFn(); // ❌ 至少有两个参数,但是获得0个
}
function foo(): number {
return 1;
}
calc(foo);
- 这个时候就要选择可选参数
typescript
type CalcType = (num1?: number, num2?: number) => void;
function calc(calcFn: CalcType) {
calcFn(); // ✅ 因为使用了可选参数
}
function foo(): number {
return 1;
}
calc(foo);
默认参数
- 从 ES6 开始,JavaScript 是支持默认参数的,TypeScript 也是支持默认参数的:
typescript
function foo(x: number, y: number = 6) {
console.log(x, y);
}
foo(10, 1);
foo(10);
foo(10, undefined);
- 这个时候 y 的类型其实可以简单的看作 "undefined 和 number 类型的联合"
- 因为无论传不传这个默认参数,或者传入 undefined,都不会报错
- 但是实际上 y 是 number 类型,不要搞晕了,只是可以这么理解它的表现
剩余参数
- 从 ES6 开始,JavaScript 也支持剩余参数,剩余参数语法允许我们将一个不定数量的参数放到一个数组中。
typescript
function sum(...nums: number[]) {
let total = 0;
for (const num of nums) {
total += num;
}
return total;
}
const result1 = sum(10, 20, 30);
console.log(result1);
const result2 = sum(10, 20, 30, 40);
console.log(result2);
函数的重载(了解)
- 在 TypeScript 中,如果我们编写了一个 add 函数,希望可以对字符串和数字类型进行相加,应该如何编写呢?
- 我们可能会这样来编写,但是其实是错误的:

那么这个代码应该如何去编写呢?
- 在 TypeScript 中,我们可以去编写不同的重载签名(overload signatures)来表示函数可以以不同的方式进行调用;
- 一般是编写两个或者以上的重载签名,再去编写一个通用的函数以及实现;
比如我们对 sum 函数进行重构:
- 在我们调用 sum 的时候,它会根据我们传入的参数类型来决定执行函数体,到底执行哪一个函数的重载签名;
typescript
function sum(a1: string, a2: string): string;
function sum(a1: number, a2: number): number;
function sum(a1: any, a2: any) {
return a1 + a2;
}
sum(20, 30);
sum("aaa", "bbb");
- 但是注意,有实现体的函数,是不能直接被调用的:
typescript
sum({}, {});
// ❌ 没有与此调用匹配的重载
联合类型和重载
- 我们现在有一个需求: 定义一个函数,可以传入字符串或者数组,获取它们的长度。
- 这里有两种实现方案:
- 方案一:使用联合类型来实现;
- 方案二:实现函数重载来实现;
typescript
function getLength(a: string | any[]) {
return a.length;
}
typescript
function getLength(a: string): number;
function getLength(a: any[]): number;
function getLength(a: any) {
return a.length;
}
- 如果还要求可以传入对象时获取它的 length 属性,还有下面的实现方法
typescript
function getLength({ length: number }): number;
getLength({ length: 3 });
- 在开发中,联合类型和函数重载,我们选择使用哪一种呢?
- 建议:在可能的情况下,尽量选择使用联合类型来实现;
六. this 类型
可推导的 this 类型
- this 是 JavaScript 中一个比较难以理解和把握的知识点:
- coderwhy 老师在公众号也有一篇文章专门讲解 this:https://mp.weixin.qq.com/s/hYm0JgBI25grNG_2sCRlTA;
- 当然在目前的 Vue3 和 React 开发中你不一定会使用到 this:
- Vue3 的 Composition API 中很少见到 this,React 的 Hooks 开发中也很少见到 this 了;
- 但是我们还是简单掌握一些TypeScript 中的 this,TypeScript 是如何处理 this 呢?我们先来看两个例子:
typescript
const obj = {
name: "obj",
foo: function () {
console.log(this.name);
},
};
obj.foo(); // "obj"
function foo1() {
console.log(this);
}
foo1(); // window or undefined or global
- 上面的代码默认情况下是可以正常运行的,也就是TypeScript 在编译时,认为我们的 this 是可以正确去使用的:
- 这是因为在没有指定 this 的情况,this 默认情况下是
any类型的;
- 这是因为在没有指定 this 的情况,this 默认情况下是
this 的编译选项
- VSCode 在检测我们的 TypeScript 代码时,默认情况下运行不确定的 this 按照 any 类型去使用。
- 但是我们可以创建一个
tsconfig.json文件,并且在其中告知 VSCode,this 必须明确指定(不能是隐式的);
- 但是我们可以创建一个

- 在设置了
noImplicitThis为true时, TypeScript 会根据上下文推导 this,但是在不能正确推导时,就会报错,需要我们明确的指定 this。

指定 this 的类型
- 在开启
noImplicitThis的情况下,我们必须指定 this 的类型。 - 如何指定呢?函数的第一个参数类型:
- 函数的第一个参数我们可以根据该函数之后被调用的情况,用于声明 this 的类型(名字必须叫 this);
- 在后续调用函数传入参数时,从第二个参数开始传递的,this 参数会在编译后被抹除;
typescript
function foo(this: string) {
console.log(this);
}
foo.call("1");
this 相关的内置工具
- Typescript 提供了一些工具类型来辅助进行常见的类型转换,这些类型全局可用。
ThisParameterType:- 用于提取一个函数类型 Type 的 this (opens new window)参数类型;
- 如果这个函数类型没有 this 参数返回 unknown;
- 类型上下文这里可以理解为,用来计算出一个新类型的位置(我自己感觉的)
typescript
function foo(this: string) {
console.log(this);
}
// 获取一个函数的this类型,在类型上下文中可用
// 类型上下文
type ThisType = ThisParameterType<typeof foo>;
OmitThisParameter:- 用于移除一个函数类型 Type 的 this 参数类型, 并且返回当前的函数抹除 this 类型后的函数类型
typescript
// 移除一个函数类型Type的this参数类型,在类型上下文中可用
type FnType = OmitThisParameter<typeof foo>;
this 相关的内置工具 - ThisType
- 这个类型不返回一个转换过的类型,它被用作标记一个上下文的 this 类型。(官方文档)
- 事实上官方文档的不管是解释,还是案例都没有说明出来 ThisType 类型的作用;
- 我这里用另外一个例子进行说明:
typescript
interface IState {
name: string;
age: number;
}
interface IData {
state: IState;
running: () => void;
eating: () => void;
}
const info: IData = {
state: {
name: "why",
age: 18,
},
eating() {
console.log(this.name); // ❌ IState上没有name类型
},
running() {
console.log(this.name); // ❌ IState上没有name类型
},
};
- 我们可以选择用前面学到的 this 显式指定
typescript
const info: IData = {
state: {
name: "why",
age: 18,
},
eating(this: IState) {
console.log(this.name); // ✅ this被正确指定为IState类型
},
running(this: IState) {
console.log(this.name); // ✅ this被正确指定为IState类型
},
};
info.eating.call(info.state);
- 但是如果函数一多,手动标注很麻烦,有没有一种方式指定上下文里所有的 this 类型呢?
- 使用
ThisType,和联合类型一起使用
- 使用
typescript
const info: IData & ThisType<IState> = {
state: {
name: "why",
age: 18,
},
eating() {
console.log(this.name); // ✅ this被正确指定为IState类型
},
running() {
console.log(this.name); // ✅ this被正确指定为IState类型
},
};
info.eating.call(info.state);