Skip to content
On this page

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 ,但并不知道它具体的类型:
    • 断言前 myElHTMLElement || null, 就算不是 null,HTMLElement 上也没有 src 属性,因此无法访问
    • 断言后,编译器直接相信 myEl更具体的 HTMLImageElement,它上面有 src 属性,因此可以访问 src

image-20230311174740351

image-20230311175126677

  • 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 类型的时候会被推导为字面量类型,其他情况一般要手动指定字面量类型

image-20230311191234828

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

image-20230311193640242

字面量推理

  • 我们来看下面的代码:

image-20230311194131490

  • 这是因为我们的对象在进行字面量推理的时候,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 类型,当然可以传入

image-20230315170326486

类型缩小

  • 什么是类型缩小呢?

    • 类型缩小的英文是 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) 时会有特别的效果,后面会学到

image-20230315113651643

平等缩小

  • 我们可以使用 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) => void means “a function with a parameter named string of type any“!

调用签名(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, the x parameter will actually have the type number | undefined because unspecified parameters in JavaScript get the value undefined.

  • 另外可选类型需要在必传参数的后面:

image-20230319154755355

参数个数不检测?

  • 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 的函数签名里有itemindexarr,然而我们很多时候传入的函数只用到了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 done callback 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 the elapsedTime parameter, 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 函数,希望可以对字符串和数字类型进行相加,应该如何编写呢?
  • 我们可能会这样来编写,但是其实是错误的:

image-20230319160502465

  • 那么这个代码应该如何去编写呢?

    • 在 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 中一个比较难以理解和把握的知识点:
  • 当然在目前的 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 的编译选项

  • VSCode 在检测我们的 TypeScript 代码时,默认情况下运行不确定的 this 按照 any 类型去使用。
    • 但是我们可以创建一个 tsconfig.json 文件,并且在其中告知 VSCode,this 必须明确指定(不能是隐式的);

image-20230319171220158

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

image-20230319172727051

指定 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);
Comments
  • Latest
  • Oldest
  • Hottest
Powered by Waline v2.14.9