TypeScript 语法总结

数据类型

number

TSJS一样不区分整型和浮点型,统一称为number类型。

let age: number = 18;
const height: number = 1.88;

默认情况下,会将赋值的值的类型,作为前面标识符的类型,这个过程称之为类型推导。

let a = 100; //a就是number类型了
a = "hello"; //会报错

boolean

两个值 truefalse。

let flag: boolean = true;
flag = 20 > 30;

string

字符串类型,支持单双或引号表示,也支持模板字符串。

let message1: string = "hello world";
let message2: string = "Hello World";

// 个人习惯: 默认情况下, 如果可以推导出对应的标识符的类型时, 一般情况下是不加
const name = "why";
const age = 18;
const height = 1.88;

let message3 = `name:${name} age:${age} height:${height}`;
console.log(message3);

array

在 JS 中我们数组中可以存放任意的类型,但这是不太好的一种表现,因为不同类型的数据就不好统一的做处理,所以TS可以创建存放相同类型数据的数组。

//创建方式1
const arr1: Array<string> = [];
arr1.push(123); //报错

上述方式可以创建只能存放string类型的数组,但是上述方式的写法在jsx语法中是有冲突的,和标签语法冲突,所以不推荐。

//创建方式2
const arr2: string[] = [];

object,Object 和{}

object 类型用于表示非原始类型

const info = {
  name: "why",
  age: 18,
};
//通过下面的这种方式创建的对象不能取值和设置值
const obj: object = {
  name: "zhangsan",
};

let objectCase: object;
objectCase = 1; // error
objectCase = "a"; // error
objectCase = true; // error
objectCase = null; // error
objectCase = undefined; // error
objectCase = {}; // ok

console.log(info.name);
console.log(obj.name); //报错

Object

这里注意大写的Object代表拥有一些方法类型如toString()hasOwnProperty。所以所有原始类型、非原始类型都可以赋给 Object(严格模式下 nullundefined 不可以)。

let ObjectCase: Object;
ObjectCase = 1; // ok
ObjectCase = "a"; // ok
ObjectCase = true; // ok
ObjectCase = null; // error
ObjectCase = undefined; // error
ObjectCase = {}; // ok

{}Object一样。

symbol

这里介绍基本使用方式,在对象中不能设置两个属性相同的属性。我们在使用 Symbol 的时候,必须添加 es6 的编译辅助库 需要在 tsconfig.json 的 libs 字段加上ES2015 Symbol 的值是唯一不变的。

const info1 = {
  title: "zhangsan",
  title: "lisi", //上面的就会被覆盖掉
};
//这样就可以了
const info2 = {
  title1: "zhangsan",
  title2: "lisi",
};
//使用symbol类型
const title1: symbol = Symbol("title");
const title2 = Symbol("title");
const info3 = {
  [title1]: "zhangsan",
  [title2]: "lisi",
};

null 和 undefined

null类型只有一个值nullundefined也只有一个值undefined

let n1: null = null;
let n2 = null; //这里n2为 any 类型

let n3: undefined = undefined;
let n4 = undefined; //这里n4为 any 类型

any

有时我们无法确定一个变量的类型,并且它有可能变化,我们就可以使用any类型。我们可以对any类型进行任意的操作,对于某些情况的处理过于繁琐不希望添加规定的类型注解,或者在引入一些第三方库时,缺失了类型注解,这个时候我们可以使用any。当然也不要过渡依赖这个类型,不然你使用ts就毫无意义了。

let a: any = "why";
a = 123;
a = true;
const arr: any[] = [12, "zhang"];

unknow

用于描述不确定的类型,其他的类型都可以赋值给unknow类型,但是unkonw只能赋值给anyunkonw类型。

function foo() {
  return "abc";
}
function bar() {
  return 123;
}
// unknown类型只能赋值给any和unknown类型
// any类型可以赋值给任意类型
let flag = true;
let result: unknown; // 最好不要使用any
if (flag) {
  result = foo();
} else {
  result = bar();
}

let message: string = result; //报错
let num: number = result; //报错

console.log(result);

void

void一般表示一个函数是没有返回值的,那么它的返回值就是void类型,JS中函数默认的返回值是undefinedTS中默认是void类型。

可以将nullundefined类型赋值给void类型。

function sum(num1: number, num2: number) {
  console.log(num1 + num2);
}
function sum1(num1: number, num2: number): void {
  console.log(num1 + num2);
}

sum(20, 30);

never

never类型表示永远也不会发生的类型。
一个函数中有死循环或者抛出异常,该函数是不会返回任何东西的,所以void类型是不适合的,我们可以使用never类型。

function foo(): never {
  // 死循环
  while (true) {}
}
function bar(): never {
  throw new Error();
}

另外还有一个场景

function handleMessage(message: string | number | boolean) {
  switch (typeof message) {
    case "string":
      console.log("string处理方式处理message");
      break;
    case "number":
      console.log("number处理方式处理message");
      break;
    default:
      const check: never = message;
  }
}

如果我们封装一个函数,我们对参数类型的判断处理不全面,可以设置一个默认处理方式,当我们漏掉对某种数据类型判断处理的时候就可以将其赋值给never类型,当然这绝对会报错的,这时我们就能发现我们对该数据类型漏处理,从而补全逻辑。

tuple

元组类型

通常数组中会存放数据类型相同的元素,要存放不同类型的元素建议使用对象或者元组数组。

元组中每个元素都有自己的特性,根据索引值获取到的值可以确定对应的类型。

const info: [string, number, number] = ["why", 18, 1.88];
const name = info[0]; //一定为string类型
console.log(name.length);

应用场景:

function useState<T>(state: T) {
  let currentState = state;
  const changeState = (newState: T) => {
    currentState = newState;
  };
  const info: [string, number] = ["abc", 18];
  const tuple: [T, (newState: T) => void] = [currentState, changeState];
  return tuple;
}

const [counter, setCounter] = useState(10);
setCounter(1000);
const [title, setTitle] = useState("abc"); //每个返回的数据类型的都是确定的
const [flag, setFlag] = useState(true);

枚举类型

枚举其实就是将一组可能出现的值,一个个列举出来,定义在一个类型中,这个类型就是枚举类型。枚举允许开发者定义一组命名常量,常量可以是数字、字符串类型。

enum Direction {
  LEFT,
  RIGHT,
  TOP,
  BOTTON,
}
function turnDirection(direction: Direction) {
  switch (direction) {
    case Direction.LEFT:
      console.log("向左");
      break;
    case Direction.RIGHT:
      console.log("向右");
      break;
    case Direction.TOP:
      console.log("向上");
      break;
    case Direction.BOTTON:
      console.log("向下");
      break;
    default:
      console.log("没有");
  }
}

turnDirection(Direction.LEFT);
turnDirection(Direction.RIGHT);
turnDirection(Direction.TOP);
turnDirection(Direction.BOTTON);

枚举类型的值

枚举类型默认值是从 0 开始依次递增的,也可以给枚举设置初始值。

enum Direction {
  LEFT = 100,
  RIGHT,
  TOP,
  BOTTON,
}
//输出的值就是从100开始 +1 递增

函数

函数定义

函数声明

function hello(name: string) {
  console.log(name);
}
hello("world");

函数表达式

type SumFunc = (x: number, y: number) => number;
const sumFunc: SumFunc = (a, b) => {
  return a + b;
};

函数参数

我们定义函数的时候可以给函数参数设置类型注释,当然也可以不设置(自动推导)

function sum(num1: number, num2: number) {
  return num1 + num2;
}
sum(123, 321);

匿名函数的参数类型

有时候我们的函数参数是一个函数,那么这个函数的参数值就是根据上下文环境推断出来的。

const names = ["abc", "cba", "nba"];
// item根据上下文的环境推导出来的(这里就是string类型), 这个时候可以不添加的类型注解
// 上下文中的函数: 可以不添加类型注解
names.forEach(function (item) {
  console.log(item.split(""));
});

函数参数的对象类型

参数为对象的函数,我们还可以详细的设置对象中的属性的数据类型

function printPoint(point: { x: number; y: number }) {
  console.log(point.x);
  console.log(point.y);
}
printPoint({ x: 123, y: 321 });

函数默认参数

function sum100(x: number, y: number = 100) {
  return x + y;
}

console.log(sum100(12)); //112
剩余参数

函数剩余参数:剩余参数语法允许我们将一个不定数量的参数放到一个数组中。

function sumarg(...arg: number[]) {
  return arg;
}

console.log(sumarg(1, 2, 3, 4, 5));

函数类型

函数作为参数的时候也要确定类型的 。

function foo() {}
type FooFnType = () => void;
function bar(fn: FooFnType) {
  fn();
}
bar(foo);

上面的语法 () => void 就是一个函数类型 没有参数,且没有返回值。

type AddFnType = (num1: number, num2: number) => number;
//使用函数表达式方式创建函数
const add: AddFnType = (a1: number, a2: number) => {
  return a1 + a2;
};

函数中的 this

this也是一个对象,当然它也有类型,TS可以默认被推导出来。

const info = {
  name: "why",
  eating() {
    //这里推导出来就是info对象
    console.log(this.name + " eating");
  },
};
info.eating();

有些情况 TS 就推导不出来,导致报错。

function eating() {
  console.log(this.name + " eating", message);
}
const info = {
  name: "why",
  eating: eating,
};
// 隐式绑定
info.eating(); //编译报错

上述代码就会编译报错,解决方式如下:

type ThisType = { name: string }; //自定义一个this类型

function eating(this: ThisType) {
  console.log(this.name + " eating");
}

const info = {
  name: "why",
  eating: eating,
};

// 隐式绑定
info.eating();

函数的重载

有时候我们通过联合类型会进行很多的逻辑判断,导致代码太冗长,而且返回值有可能也不确定。

function add(a1: number | string, a2: number | string) {
  if (typeof a1 === "number" && typeof a2 === "number") {
    return a1 + a2;
  } else if (typeof a1 === "string" && typeof a2 === "string") {
    return a1 + a2;
  }
}

add(10, 20);

对于上述案例我们可以采用函数的重载来简化代码书写:

函数的名称相同, 但是参数不同的几个函数, 就是函数的重载。

定义函数重载的函数是没有函数体,另外还需要一个参数数量相同但是能接受any类型有函数体的函数。有点抽象,基本语法如下:

function add(num1: number, num2: number): number;
function add(num1: string, num2: string): string;
function add(num1: any, num2: any): any {
  if (typeof num1 === "string" && typeof num2 === "string") {
    return num1.length + num2.length;
  }
  return num1 + num2;
}
const result = add(20, 30); //50
const result2 = add("abc", "cba"); //6
console.log(result);
console.log(result2);

注意:函数重载真正执行的是同名函数最后定义的函数体 在最后一个函数体定义之前全都属于函数类型定义 不能写具体的函数实现方法 只能定义类型

可选类型

对象类型也可以指定哪些属性是可选的,可以在属性的后面添加一个?

function printPoint(point: { x: number; y: number; z?: number }) {
  console.log(point.x);
  console.log(point.y);
  console.log(point.z);
}
//可以传2个或者3个参数
printPoint({ x: 123, y: 321 });
printPoint({ x: 123, y: 321, z: 111 });

联合类型

我们可以指定一个参数为多种类型

function printID(id: number | string | boolean) {
  // 使用联合类型的值时, 需要特别的小心
  if (typeof id === "string") {
    //这里就可以判断出id是否为string类型了
    // TypeScript帮助确定id一定是string类型
    console.log(id.toUpperCase());
  } else {
    console.log(id);
  }
}

printID(123);
printID("abc");
printID(true);

当一个函数的参数只有一个,而且设置成可选,那么也相当于该参数为联合类型,,另外函数的可选参数只能放在最后。

function foo(message?: string) {
  console.log(message);
}
//等同于
function foo(message: string | undefined) {
  console.log(message);
}
foo();

类型别名

参数的类型注释过于冗长的时候可以设置一个别名来代替这种注释

type IDType = string | number | boolean;
type PointType = {
  x: number;
  y: number;
  z?: number;
};
function printId(id: IDType) {}
function printPoint(point: PointType);

类型断言

有时候我们获取到到的类型信息不是具体的,比如我们通过document.getElementById获取到类型是一个HTMLElement,但是并不知道具体的类型:

const el = document.getElementById("why") as HTMLImageElement;
el.src = "zxxx";

另外我们也可以利用类型断言为更具体或者不太具体的类型版本

const name = "code" as number; //报错
const name2 = "code" as unknow as number; //可以

非空类型断言

有时候我们会执行一个对象的方法,但是该对象有可能为undefined,那么我们执行就会报错。

function printMessage(message?: string) {
  console.log(message.length); //有可能message为undefined,从而报错
  console.log(message!.length); //保证不为空。不会报错
}

使用 !. 表示可以确定某个标识符是有值,跳过ts在编译阶段对它的检测。

类型保护

typeof 类型保护

function double(input: string | number | boolean) {
  if (typeof input === "string") {
    return input + input;
  } else {
    if (typeof input === "number") {
      return input * 2;
    } else {
      return !input;
    }
  }
}

in 关键字

in 关键字是判断对象上是否有该属性。

interface Bird {
  fly: number;
}

interface Dog {
  leg: number;
}

function getNumber(value: Dog | Bird) {
  if ("fly" in value) {
    return value.fly;
  }
  return value.leg;
}

instanceof 类型保护

instanceof 是判断属性是否在对象的原型链上。

class Animal {
  name!: string;
}
class Bird extends Animal {
  fly!: number;
}
function getName(animal: Animal) {
  if (animal instanceof Bird) {
    console.log(animal.fly);
  } else {
    console.log(animal.name);
  }
}

自定义类型保护

下面的例子如果函数返回true那么定义的test就是string类型。

function isString(test: any): test is string {
  return typeof test === "string";
}

function example(foo: any) {
  if (isString(foo)) {
    //此时就缩小确定了foo的类型为string
    console.log("it is a string" + foo);
    console.log(foo.length); // string function
    // 如下代码编译时会出错,运行时也会出错,因为 foo 是 string 不存在toExponential方法
    //toExponential()为number上的方法,表示以指数表示法返回该数值字符串表示形式
    console.log(foo.toExponential(2));
  }
  // 编译不会出错,但是运行时出错
  console.log(foo.toExponential(2));
}
example("hello world");

可选链

可选链的操作符为 ?. 当前面的标识符值不存在的时候就会短路返回undefined,从而不会报错

type Person = {
  name: string;
  friend?: {
    name: string;
    age?: number;
    girlFriend?: {
      name: string;
    };
  };
};
const info: Person = {
  name: "why",
};

console.log(info.name); //why
console.log(info.friend?.name); //undefined
console.log(info.friend?.age); //undeined
console.log(info.friend?.girlFriend?.name); //undeined

这里再说一个操作符?? 该操作符为空值操作符,当操作符的左侧为null或者undefined时,返回右侧的操作数,否则返回左侧操作数。这个与 ||操作符类似,但是 ||操作符是当左侧布尔值为false时返回右侧的操作数。

let message: string | null = "Hello World";
const content = message ?? "你好啊, 李银河";
console.log(content); // Hello World

字面量类型

我们可以如下定义:

const message: "Hello World" = "Hello World";

这里显示的类型为 Hello World ,这就是字面量类型。当然默认这样做是没有意义的。
我们可以将多个字面量类型联合起来使用:

type Alignment = "left" | "right" | "center";
let align: Alignment = "left";
align = "right";
align = "center";
align = "other"; //报错,只能赋值 'left' | 'right' | 'center'中的一个

字面量推理

有如下代码:

type Method = "GET" | "POST";
type Request = {
  url: string;
  method: Method;
};

const options = {
  url: "https://www.coderwhy.org/abc",
  method: "POST",
};
request(options.url, options.method); //这里会报第二个参数的错

因为这样传递参数会默认推理为string类型,但是我们第二个参数类型为 Method字面量。
两种解决方式:

//第一种
request(options.url, options.method as Method); //将string转为 "范围更小 "的字符串字面量
const options = {
  url: "https://www.coderwhy.org/abc",
  method: "POST",
} as const; //将对象中的属性 断言为为对应值的字面量类型

通过class关键字来声明

我们可以声明一些类的属性,默认时any类型,也可以设置默认值。

类也可以有自己的构造函数,构造函数默认返回当前创造出来的实例。

class Person {
  name!: string; //这里加!表示这个属性在实例化的时候可以不用赋值
  age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  eating() {
    console.log(this.name + " eating");
  }
}
const p = new Person("why", 18);
console.log(p.name);
console.log(p.age);
p.eating();

我们也可以将属性定义直接写在构造函数中的参数中(当然这样就是不太清晰)。

class Person {
  constructor(public name:string) {
    this.name = name
  }
  getName():void {
    console.log(this.name)
  }
}

const p = new Person('zhangsan');
p.getName()

类的继承

面向对象的其中一大特性就是继承,继承不仅仅可以减少我们的代码量,也是多态的使用前提。使用extends关键字来实现继承,子类中使用super来访问父类。

class Person {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
  eating() {
    console.log("eating 100行");
  }
}

class Student extends Person {
  sno: number;
  constructor(name: string, age: number, sno: number) {
    // super调用父类的构造器
    super(name, age);
    this.sno = sno;
  }
  eating() {
    console.log("student eating");
    super.eating(); //调用父类原型上的方法
  }
  studying() {
    console.log("studying");
  }
}

const stu = new Student("why", 18, 111);
console.log(stu.name);
console.log(stu.age);
console.log(stu.sno);

stu.eating();

类的多态

为了写出更加具备通用性的代码。

class Animal {
  action() {
    console.log("animal action");
  }
}

class Dog extends Animal {
  action() {
    console.log("dog running!!!");
  }
}

class Fish extends Animal {
  action() {
    console.log("fish swimming");
  }
}

class Person extends Animal {}

function makeActions(animals: Animal[]) {
  animals.forEach((animal) => {
    animal.action();
  });
}
makeActions([new Dog(), new Fish(), new Person()]);

类修饰符

  • public 修饰的是在任何地方可见、公有的属性或方法,默认编写的属性就是public
  • private 修饰的是仅在同一类中可见、私有的属性或方法,也可以使用#来表示该属性为一个私有属性。 比如,#name:string
  • protected修饰的是仅在类自身及子类中可见、受保护的属性或方法
  • readonly有一个属性我们不希望外界可以任意的修改,只希望确定值后直接使用,那么可以使用。
  • static 静态成员,只有类本身可以访问

getter 和 setter

一些私有属性我们是不能直接访问的,或者某些属性我们想要监听它的获取(getter)和设置(setter)的过程。

class Person {
  private _name: string;
  constructor(name: string) {
    this._name = name;
  }
  // 访问器setter/getter
  // setter
  set name(newName) {
    this._name = newName;
  }
  // getter
  get name() {
    return this._name;
  }
}

const p = new Person("why");
p.name = "coderwhy";
console.log(p.name);

上述代码转换为 es5 就就是使用了Object.defineProperty方法修改了对象的属性的getset方法吧。

抽象类

在定义很多通用的调用接口时, 我们通常会让调用者传入父类,通过多态来实现更加灵活的调用方式。但是,父类本身可能并不需要对某些方法进行具体的实现,所以父类中定义的方法,,我们可以定义为抽象方法。

  • 抽象方法,必须存在于抽象类中。
  • 抽象类是使用abstract声明的类。
  • 抽象类是不能被实例的话(也就是不能通过new创建)。
  • 抽象方法必须被子类实现,否则该子类必须是一个抽象类。
function makeArea(shape: Shape) {
  return shape.getArea();
}

abstract class Shape {
  abstract getArea(): number;
}

class Rectangle extends Shape {
  private width: number;
  private height: number;

  constructor(width: number, height: number) {
    super();
    this.width = width;
    this.height = height;
  }

  getArea() {
    return this.width * this.height;
  }
}

class Circle extends Shape {
  private r: number;

  constructor(r: number) {
    super();
    this.r = r;
  }

  getArea() {
    return this.r * this.r * 3.14;
  }
}

const rectangle = new Rectangle(20, 30);
const circle = new Circle(10);

console.log(makeArea(rectangle));
console.log(makeArea(circle));

这里思考重写重载区别:重写是指的是子类继承父类并重新定义父类同名方法,重载则是一个函数多个类型定义。

类的类型

类本身也可以作为一种数据类型。

//声明一个类
class Person {
  name: string = "123";
  eating() {}
}
//创建一个对象,设置为Person类型,必须有相同的属性
const p1: Person = {
  name: "why",
  eating() {},
};

接口的使用

接口的声明

前面我们可通过type来声明一个对象类型。

type InfoType = { name: string; age: number };

我们现在可以通过接口来实现:

interface IInfoType {
  readonly name: string;
  age: number;
  friend?: {
    name: string;
  };
}
const info: IInfoType = {
  name: "why",
  age: 18,
  friend: {
    name: "kobe",
  },
};

索引类型

有时候我们需要定义很多属性,但实际其属性名都为相同的数据类型。

interface IndexLanguage {
  [index: number]: string;
}

const frontLanguage: IndexLanguage = {
  0: "HTML",
  1: "CSS",
  2: "JavaScript",
  3: "Vue",
};

interface ILanguageYear {
  [name: string]: number;
}

const languageYear: ILanguageYear = {
  C: 1972,
  Java: 1995,
  JavaScript: 1996,
  TypeScript: 2014,
};

函数类型

接口也可以用来定义函数类型

interface CalcFn {
  (n1: number, n2: number): number;
}

function calc(num1: number, num2: number, calcFn: CalcFn) {
  return calcFn(num1, num2);
}

const add: CalcFn = (num1, num2) => {
  return num1 + num2;
};

calc(20, 30, add);

当然一般还是推荐用类型别名来定义函数:

type CalcFn = (n1: number, n2: number) => number;

接口继承

接口也可以继承,而且支持多继承

interface ISwim {
  swimming: () => void;
}

interface IFly {
  flying: () => void;
}

interface IAction extends ISwim, IFly {}

const action: IAction = {
  swimming() {},
  flying() {},
};

交叉类型

联合类型表示多个类型中一个即可,还有另外一种类型合并,就是交叉类型(Intersection Types)。交叉类似表示需要满足多个类型的条件,交叉类型使用 & 符号。

type WhyType = number & string; //WhyType就是never 当然是无意义的

我们一般对对象使用交叉类型

interface ISwim {
  swimming: () => void;
}

interface IFly {
  flying: () => void;
}

type MyType1 = ISwim | IFly;
type MyType2 = ISwim & IFly;

const obj1: MyType1 = {
  flying() {},

  swimming() {},
};

const obj2: MyType2 = {
  swimming() {},
  flying() {},
};

构造函数的类型接口

这种情况是将一个构造函数作为参数传入函数,加上 new 作为与普通函数定义的区别。

class Animal {
  constructor(public name: string) {}
}
//不加new是修饰函数的,加new是修饰类的
interface WithNameClass {
  new (name: string): Animal;
}
function createAnimal(clazz: WithNameClass, name: string) {
  return new clazz(name);
}
let a = createAnimal(Animal, "hello");
console.log(a.name);

接口定义任何属性

如果我们在定义接口的时候无法预先知道有什么属性,或者我们需要添加额外的一些未知属性。
当然注意这个属性名的类型只能是 number,string,symbol 或者模版文本类型。

interface Prerson {
  id: number;
  name: string;
  [propName: string]: any; //这里的propName是任意取的
}

const pr: Prerson = {
  id: 111,
  name: "zamhsan",
  age: 18,
};

console.log(pr);

接口的实现

通过类来实现接口,继承只能单继承,但是一个类可以实现多个接口。

interface ISwim {
  swimming: () => void;
}

interface IEat {
  eating: () => void;
}

//类实现接口
class Animal {}
//继承只能单继承,一个类可以实现多个接口
class Fish extends Animal implements ISwim, IEat {
  constructor() {
    super();
  }
  swimming() {
    console.log("hahh");
  }
  eating() {
    console.log("hahh");
  }
}

function swimAction(swim: ISwim) {
  swim.swimming();
}

//所有实现了接口的类的实例都是可以传入的
swimAction(new Fish());

interface 和 type 的区别

这两者都可以用来定义对象类型。如果我们定义非对象类型,通常会推荐使用type,如果是定义对象类型,两者就有区别了。

  • interface可以重复的对某个接口来定义属性,最终会合并。
  • type只是定义的别名,但是别名不能重复。
interface IFoo {
  name: string;
}

interface IFoo {
  age: number;
}
//同时定义两个相同名称的接口后一个会合并前一个接口中的属性
const foo: IFoo = {
  name: "zhangsan",
  age: 12,
};
//在window中定义一个属性
interface Window {
  age: number;
}
//并不会报错了
window.age = 19;
console.log(window.age);
//注意不要加上  export {} 否者上面还是会报错,毕竟这个window在浏览器中才有的

//会报错
type Bar = {
  name: string;
};
type Bar = {
  age: 18;
};

接口和类型别名的区别

在大多数情况下,接口和类型别名没有差别,只有特殊场景下有一些区别

  1. 基础数据类型与接口不同,接口往往表示的是一个对象的形状,类型别名还可以用于其它类型,基本类型,联合类型,元祖等。
// primitive
type Name = string;

// union
type PartialPoint = PartialPointX | PartialPointY;

// tuple
type Data = [number, string];

// dom
let div = document.createElement("div");
type B = typeof div;
  1. 接口重复定义会被合并为单个接口,而类型不允许重复定义。
interface Name1 {
  first: string;
}
interface Name1 {
  last: string;
}
type name2 = string;
  1. 扩展方式不一样

接口的扩展方式是通过继承实现,而类型别名是通过交叉类型实现。

interface Name3 {
  last: string;
}

interface Name1 extends Name3 {
  first: string;
}

type name2 = string & Name1;

字面量赋值

上面的代码是因为不符合接口定义的类型而报错,但是我们将该对象先赋值给一个变量,然后再将变量赋值给这个接口定义那就不会报错。

那是因为我们将一个变量标识符赋值给其他变量的时候会进行擦除操作,上述代码就是擦除了 address 这个属性,发现剩下的属性是刚好满足的那就编译通过,但是擦除后剩下的属性类型还是不满足的话编译还是会报错的。

泛型

我们可以通过函数来封装一些API,通过传入不同的函数参数,让函数帮助我们完成不同的操作。
比如我们定义一个函数,传入一个参数就会返回对应类型的值,如果使用any的话返回的都是any类型,我们要实现传入一个number类型值,返回的也是number

function sum<T>(num: T): T {
  return num;
}

//方式一,明确传入类型
sum<number>(12);

//方式二,类型推导
sum(50); //但是会推导为字面量类型

定义接口我们也可以使用泛型

interface IPerson<T1, T2, T3 = string> {
  //可以设置默认类型,与函数参数默认值一样
  name: T1;
  age: T2;
  address: T3;
}
//接口是没有类型推导的,x传入确切的类型
const p: IPerson<string, number> = {
  name: "zhang",
  age: 18,
  address: "zhangsan",
};

泛型类

class Point<T> {
  x: T;
  y: T;
  z: T;

  constructor(x: T, y: T, z: T) {
    this.x = x;
    this.y = y;
    this.z = z;
  }
}
const p = new Point<string>("1.3", "1.6", "1.7");
const p1 = new Point<number>(1, 2, 3);

另外还有泛型函数,泛型类型别名等用法。

type Cart<T> = { list: T[] } | T[];
let c1: Cart<string> = { list: ["1"] };
let c2: Cart<number> = [1];

function firstName<T>(name: T) {
  console.log(name);
}

泛型约束

我们希望传入的数据类型都有某些共同的性质,比如都要有某些属性才允许传入。
这里注意要和 extends 区分开。

interface ILength {
  length: number;
}

function getLength<T extends ILength>(arg: T) {
  return arg.length;
}

//只要有length就可以传进来
getLength(["abc"]);
getLength("abc");
getLength({ length: 100 });

默认泛型类型

当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用。

function createArray<T = string>(length: number, value: T): Array<T> {
  let result: T[] = [];
  for (let i = 0; i < length; i++) {
    result[i] = value;
  }
  return result;
}

实用技巧

typeof

typeof既可以用作类型保护,也可以用来推理类型。

let p = {
  name: "zhangsan",
  age: 19,
};

type pname = typeof p;

keyof

keyof 可以用来取得一个对象接口的所有 key 值。

//检查输入的属性是否为对象的属性
//T[K] 表示 类型T 属性K 的类型
function getProps<T, K extends keyof T>(o: T, name: K[]): T[K][] {
  return name.map((n) => o[n]);
}

console.log(
  getProps({ name: "zhangsan", age: 18, flag: true }, ["name", "age", "flag"])
);

interface Person {
  name: string;
  age: number;
  gender: "male" | "female";
}
//type PersonKey = 'name'|'age'|'gender';
type PersonKey = keyof Person;

function getValueByKey(p: Person, key: PersonKey) {
  return p[key];
}
let val = getValueByKey({ name: "hello", age: 10, gender: "male" }, "name");
console.log(val);

索引访问操作符

使用[] 操作符可以进行索引访问。

interface p {
  name: string;
  age: number;
}

type pp = p["name"];

映射类型 in

可以使用 in 来批量操作类型中的属性

interface Person {
  name: string;
  age: number;
}

type Person_part = {
  [key in keyof Person]?: Person[key];
};

infer

infer 可以在 extends 条件类型的字句中,在真实的分支中引用此推断类型变量,推断待推断的类型。

使用 infer 推断函数返回类型

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

type fn = () => number;
type fnReturnType = ReturnType<fn>; // number

上述 T extends U ? X : Y 的形式为条件类型。

意思就是如果 T 能继承 U 则返回类型就为 X 否者为 Y,在这个例子中 T 为一个函数,可以继承 (..args:any[]) => infer R 这种形式,所以返回值 为 infer R,所以 变量 R 就为 number。

反解 promise

// promise 响应类型
type PromiseResType<T> = T extends Promise<infer R> ? R : T;

//这个函数返回值推导为Promise<string>
async function strPromise() {
  return "string promise";
}

interface Person {
  name: string;
  age: number;
}
async function personPromise() {
  return {
    name: "p",
    age: 12,
  } as Person;
}

type StrPromise = ReturnType<typeof strPromise>; // Promise<string>
// 反解
type StrPromiseRes = PromiseResType<StrPromise>; // str

type PersonPromise = ReturnType<typeof personPromise>; // Promise<Person>
// 反解
type PersonPromiseRes = PromiseResType<PersonPromise>; // Person

反解函数参入参类型

type Fn<T> = (arg: T) => void;

type FnArg<U> = U extends Fn<infer A> ? A : any;

function fn(name: string) {
  console.log(name);
}

type ArgTp = FnArg<typeof fn>; //string

元组类型转联合类型

type ElementOf<T> = T extends Array<infer A> ? A : never;

type TTuple = [string, number];

const arr: TTuple = ["zhangsan", 18];

type Union = ElementOf<typeof arr>;

new 操作符

// 获取参数类型
type ConstructorParameters<T extends new (...args: any[]) => any> =
  T extends new (...args: infer P) => any ? P : never;

// 获取实例类型
type InstanceType<T extends new (...args: any[]) => any> = T extends new (
  ...args: any[]
) => infer R
  ? R
  : any;

class TestClass {
  constructor(public name: string, public string: number) {}
}

type Params = ConstructorParameters<typeof TestClass>; // [string, numbder]

type Instance = InstanceType<typeof TestClass>; // TestClass

这里要注意,当我们定义一个类的时候,会获得两个类型。

class Component {
  static myName: string = "静态名称属性";
  myName: string = "实例名称属性";
}
//ts 一个类型 一个叫值
//放在=后面的是值
let com = Component; //这里是代表构造函数

//冒号后面的是类型
let c = new Component(); //这里是代表实例类型
let f: typeof Component = com;

console.log(typeof c, typeof f);

另类练习

// 定义一个函数,第一个参数是一个只有一个参数的函数,要求第二个参数的类型为第一个参数函数的参数类型

type ReducerState<T> = (arg: T) => T;

type ReducerStateInfer<U extends ReducerState<any>> = U extends ReducerState<
  infer I
>
  ? I
  : never;

function reducer<R extends ReducerState<any>>(
  fun: R,
  arg: ReducerStateInfer<R>
) {
  return fun(arg);
}

const num = (count: number) => count + 1;

console.log(reducer(num, 100));

内置工具

这里介绍下 ts 内置的一些工具集

Exclude<T,U>

从 T 可分配的类型中剔除 U。

type Exclude<T, U> = T extends U ? never : T;

type E = Exclude<string | number, string>;
//上述我个人理解的额是等同于以下形式
type E = Exclude<string , string> | Exclude<number, string>;
let e: E = 10;

Extract<T,U>

从 T 可分配给的类型中提取 U

type Extract<T, U> = T extends U ? T : never;

type E = Extract<string | number, string>;
let e: E = "1";

NonNullable

从 T 中排出 null 和 undefined

type NonNullable<T> = T extends null | undefined ? never : T;

type E = NonNullable<string | number | null | undefined>;
let e: E = null;

基于 infer 实现的一些类型

  • ReturnType 返回函数的返回值类型
  • Parameters 获取函数的参数类型

Partial

将传入属性转换为可选属性

type Partial<T> = { [P in keyof T]?: T[P] };
interface A {
  a1: string;
  a2: number;
  a3: boolean;
}
type aPartial = Partial<A>;
const a: aPartial = {}; // 不会报错

Required

将可选属性转为必选属性

type Required<T> = { [P in keyof T]-?: T[P] };
interface Person {
  name: string;
  age: number;
  gender?: "male" | "female";
}

let p: Required<Person> = {
  name: "hello",
  age: 10,
  gender: "male",
};

Readonly

将传入的属性都加上只读前缀

interface Person {
  name: string;
  age: number;
  gender?: "male" | "female";
}
//type Readonly<T> = { readonly [P in keyof T]: T[P] };
let p: Readonly<Person> = {
  name: "hello",
  age: 10,
  gender: "male",
};
p.age = 11; //error

Pick<T,k>

从传入的属性中提取特定的属性出来

interface Todo {
  title: string;
  description: string;
  done: boolean;
}

type Pick<T, K extends keyof T> = { [P in K]: T[P] };

type TodoBase = Pick<Todo, "title" | "done">;

type TodoBase = {
  title: string;
  done: boolean;
};

Record<K,T>

构造一个类型,该类型具有一组属性 K,每个属性的类型为 T。

type Record<K extends keyof any, T> = {
  [P in K]: T;
};
type Point = "x" | "y";
type PointList = Record<Point, { value: number }>;
const cars: PointList = {
  x: { value: 10 },
  y: { value: 20 },
};

Omit<K,T>

用于将传入的属性剔除特定属性并返回新的类型。

type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
type User = {
  id: string;
  name: string;
  email: string;
};

type UserWithoutEmail = Omit<User, "email">;
//没有email的类型声明了

装饰器

暂时不了解

tsconfig.json

该文件是 typescript 项目的配置文件,包含 typescript 相关编译配置,如果一个目录下存在一个 tsconfig.json 文件,那么它意味着这个目录是 TypeScript 项目的根目录。

重要编译选项

  • files - 设置要编译的文件的名称。
  • include - 设置需要进行编译的文件,支持路径模式匹配。
  • exclude - 设置无需进行编译的文件,支持路径模式匹配。
  • compilerOptions - 设置与编译流程相关的选项。

详细的配置介绍:
compilerOptions 选项:

{
  "compilerOptions": {
    /* 基本选项 */
    "target": "es5", // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
    "module": "commonjs", // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
    "lib": [], // 指定要包含在编译中的库文件
    "allowJs": true, // 允许编译 javascript 文件
    "checkJs": true, // 报告 javascript 文件中的错误
    "jsx": "preserve", // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
    "declaration": true, // 生成相应的 '.d.ts' 文件
    "sourceMap": true, // 生成相应的 '.map' 文件
    "outFile": "./", // 将输出文件合并为一个文件
    "outDir": "./", // 指定输出目录
    "rootDir": "./", // 用来控制输出目录结构 --outDir.
    "removeComments": true, // 删除编译后的所有的注释
    "noEmit": true, // 不生成输出文件
    "importHelpers": true, // 从 tslib 导入辅助工具函数
    "isolatedModules": true, // 将每个文件做为单独的模块 (与 'ts.transpileModule' 类似).

    /* 严格的类型检查选项 */
    "strict": true, // 启用所有严格类型检查选项
    "noImplicitAny": true, // 在表达式和声明上有隐含的 any类型时报错
    "strictNullChecks": true, // 启用严格的 null 检查
    "noImplicitThis": true, // 当 this 表达式值为 any 类型的时候,生成一个错误
    "alwaysStrict": true, // 以严格模式检查每个模块,并在每个文件里加入 'use strict'

    /* 额外的检查 */
    "noUnusedLocals": true, // 有未使用的变量时,抛出错误
    "noUnusedParameters": true, // 有未使用的参数时,抛出错误
    "noImplicitReturns": true, // 并不是所有函数里的代码都有返回值时,抛出错误
    "noFallthroughCasesInSwitch": true, // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)

    /* 模块解析选项 */
    "moduleResolution": "node", // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
    "baseUrl": "./", // 用于解析非相对模块名称的基目录
    "paths": {}, // 模块名到基于 baseUrl 的路径映射的列表
    "rootDirs": [], // 根文件夹列表,其组合内容表示项目运行时的结构内容
    "typeRoots": [], // 包含类型声明的文件列表
    "types": [], // 需要包含的类型声明文件名列表
    "allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入。

    /* Source Map Options */
    "sourceRoot": "./", // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
    "mapRoot": "./", // 指定调试器应该找到映射文件而不是生成文件的位置
    "inlineSourceMap": true, // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
    "inlineSources": true, // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性

    /* 其他选项 */
    "experimentalDecorators": true, // 启用装饰器
    "emitDecoratorMetadata": true // 为装饰器提供元数据的支持
  }
}

声明文件

声明文件的作用是代码补全和语法提示。
通常我们会把声明语句单独的放到一个单独的文件,这个文件就是声明文件(xxx.d.ts),声明文件的文件扩展名是:.d.ts
常用的声明语句:
declare var 声明全局变量
declare function 声明全局方法
declare class 声明全局类
declare enum 声明全局枚举类型
declare namespace 声明(含有子属性的)全局对象
interfacetype 声明全局类型
export 导出变量
export namespace 导出(含有子属性的)对象
export default ES6 默认导出
export = commonjs 导出模块
export as namespace UMD 库声明全局变量
declare global 扩展全局变量
declare module 扩展模块
/// 三斜线指令 用法

书写声明文件

自动生成

如果库的源码本身就是由 ts 写的,那么在使用 tsc 脚本将ts 编译为 js 的时候,添加 declaration 选项,就可以同时也生成 .d.ts 声明文件了。

手动书写

当一个第三方库没有提供声明文件时,我们就需要自己书写声明文件了。
在不同的场景下,声明文件的内容和使用方式会有所区别。
详情

命名空间

将一个模块内部在进行划分作用域。

export namespace time {
  export function format(time: number) {
    return "2074-02-22";
  }
}
//将函数包裹到命名的对象中导出使用
import { time } from "./utils/format";
console.log(time.format(1));

类型的查找

在 TS 中所有类型几乎都是我们自己编写的 ,但是还有一些我们并没有编写的类型也可使用,这是因为 TS 内部为我们声明了很多类型。

const img = document.getElementById("img") as HTMLImageElement;

上述的 HTMLImageElement 等类型就是TS内部声明好了的,我们在vscode可以通过ctrl+左键点击进入声明文件。

像这样的.d.ts文件就是用来做类型声明的文件,它仅仅用来做类型检测,告知TS有哪些类型。TS会在三个地方找到我们类型声明:

  • 内置类型声明
  • 外部定义类型声明
  • 自己定义类型声明

内置类型声明就是 TS 自带的。

外部定义类型的声明就是我们使用一些第三方库的时候它有一个.d.ts文件声明了类型。

这些库通常有两种方式声明类型,一种是自带的(比如axios),另一种需要单独下载(比如lodash)。下载模版是@types/模块名称
我们在 TS 引入这些库,如果没有类型声明就会报错,我们当然也可以自己编写声明。

这个变量是没有定义的,当然会报错

定义之后就不会报错了。
另外我们还可以声明函数,类

declare function name(): void;
declare class Person {
  name: string;
  constructor(name: string);
}

声明模块

如果某模块不能使用,我们可以自己声明。在声明模块的内部,我们可以通过 export 导出对应库的类、函数等。

语法: declare module '模块名' {}

declare module "lodash" {
  export function join(args: []): any;
}

声明文件

在开发 vue 的过程中,默认是不识别我们的.vue 文件的,那么我们就需要对其进行文件的声明。在开发 vue 的过程中,默认是不识别我们的.vue 文件的,那么我们就需要对其进行文件的声明。

declare module "*.vue";
declare module "*.jpg";
declare module "*.jpeg";
declare module "*.png";
declare module "*.svg";
declare module "*.gif";

声明命名空间

比如我们引入了 JQuery,就可以声明命令空间

declare namespace $ {
  function ajax(settings: any): void;
}

在 TS 中就可以这样使用

$.ajax({
  //...
});

发布声明文件

如果是我们为第三方库添加声明文件,如果作者不愿合并我们提交的pr 那么我们可以自己将声明文件提交到@types,之后其他人下载就可以通过下载@types/xx来进行文件的下载。
与普通的npm模块不同,@types 是统一由 DefinitelyTyped 管理的。要将声明文件发布到 @types 下,就需要给 DefinitelyTyped 创建一个 pull-request,其中包含了类型声明文件,测试代码,以及 tsconfig.json 等。

pull-request 需要符合它们的规范,并且通过测试,才能被合并,稍后就会被自动发布到 @types 下。
DefinitelyTyped 中创建一个新的类型声明,需要用到一些工具,DefinitelyTyped 的文档中已经有了详细的介绍,这里就不赘述了,以官方文档为准。

Last modification:March 8, 2022
如果觉得我的文章对你有用,请随意赞赏