ts

288 阅读18分钟

1.常用类型

1.1.原始数据类型

原始数据类型包括:布尔值、数值、字符串、nullundefined 以及 ES6 中的新类型 [Symbol] 和 ES10 中的新类型 [BigInt]

let str: string = 'hello typescript'
let num: number = 100
let bool: boolean = true
let u: undefined = undefined;
let n: null = null;

1.2.数组

数组是指定形如 [1, 2, 3] 数据,可以使用语法 number[] 来定义; 此语法适用于任何类型(例如 string[] ,字符串数组等)。可以写成 Array。

let arr: number[] = [1, 2, 3]
let arr2: Array<number> = [1, 2, 3]

1.3.any

TypeScript 还有一个特殊类型 any ,当你不希望某个特定值导致类型检查错误时,可以使用它。 当一个值的类型是 any 时,可以访问它的任何属性,将它分配给任何类型的值,或者几乎任何其他语法 上的东西都合法的:

let obj: any = { x: 0 };
// 使用'any'将禁用所有进一步的类型检查
obj.foo();
obj.bar = 100;
obj = "hello";
const n: number = obj;

1.4.函数

声明函数时,可以在每个参数后添加类型注解,以声明函数接受的参数类型。参数类型注释位于参数名 称之后:还可以定义返回类型

function getFavoriteNumber(n:number): number {
  return 26;
}

1.5.对象类型

对象类型还可以指定其部分或全部属性是可选的。为此,请在属性名称后添加一个 ? :

function printName(obj: { first: string; last?: string }) {
// ...
}
// 两种传递参数都可以
printName({ first: "Felix" });
printName({ first: "Felix", last: "Lu" })

1.6.联合类型

联合类型是由两个或多个其他类型组成的类型,表示可能是这些类型中的任何一种的值。我们将这些类型中的每一种称为联合类型的成员。

let n:number|string|string[]=100

1.7.类型别名

应用一个名称来定义一个类型,类型别名的语法是:

type m = number|string
type Point = {
 x: number;
 y: number;
};
function printCoord(pt: Point) {
  console.log("坐标x的值是: " + pt.x);
  console.log("坐标y的值是: " + pt.y);
}
printCoord({ x: 100, y: 100 });

1.8.接口

一个接口声明是另一种方式来命名对象类型:

interface Point {
  x: number;
  y: number;
}
function printCoord(pt: Point) {
  console.log("坐标x的值是: " + pt.x);
  console.log("坐标y的值是: " + pt.y);
}
printCoord({ x: 100, y: 100 });

1.9.类型断言

有时,你会获得有关 TypeScript 不知道的值类型的信息。如你正在使用 document.getElementById,TypeScript只知道这将返回某种类型的HTMLElement但你可能知道你的页面将始终具有 HTMLCanvasElement 给定 ID 的值,在这种情况下,你可以使用类型断言来指定更具体的类型:

const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;

1.10.枚举

它允许描述一个值,该值可能是一组可能的命名常量之一。

// ts源码
enum Direction {
  Up = 1,
  Down,
  Left,
  Right,
}
console.log(Direction.Up) // 1

1.11.其他类型

1.void 表示没有返回值的函数的返回值。当一个函数没有任何返回语句,或者没有从这些返回语句中返 回任何明确的值时,它都是推断出来的类型。

// 推断出的返回类型是void
function noop() {
   return;
}

2.object 指的是任何不是基元的值( string 、 number 、 bigint 、 boolean 、 symbol 、null 或 undefined )。这与空对象类型 { } 不同,也与全局类型 Object 不同。 object 不是 Object 。始终使用 object !请注意,在JavaScript中,函数值是对象。它们有属性,在它们的原型链中有 Object.prototype ,是Object 的实例,你可以对它们调用 Object.key 。由于这个原因,函数类型在TypeScript中被认为是 object 。

3.unknown 类型代表任何值。这与 any 类型类似,但更安全,因为对未知 unknown 值做任何事情都是不合法的。

function f1(a: any) {
  a.b(); // 正确
}
function f2(a: unknown) {
  a.b(); //报错
}

4.never:有些函数永远不会返回一个值,never 类型表示永远不会被观察到的值。在一个返回类型中,这意味着函数抛出一个异常或终止程序的执行。never 也出现在TypeScript确定一个 union 中没有任何东西的时

function fail(msg: string): never {
   throw new Error(msg);
}

function fn(x: string | number) {
 if (typeof x === "string") {
  // 做一些事
 } else if (typeof x === "number") {
 // 再做一些事
 } else {
  x; // 'never'!
 }
}

5.Function:全局性的 Function 类型描述了诸如 bind 、 call 、 apply 和其他存在于JavaScript中所有函数值的属性。它还有一个特殊的属性,即 Function 类型的值总是可以被调用;这些调用返回 any 。 这是一个无类型的函数调用,一般来说最好避免,因为 any 返回类型都不安全。 如果你需要接受一个任意的函数,但不打算调用它,一般来说, () => void 的类型比较安全。

function doSomething(f: Function) {
  return f(1, 2, 3);
}

2.类型缩小

2.1.typeof,in,instanceof

支持typeof 运算符,返回类型

function printAll(strs: string | string[] | null) {
  if (typeof strs === "object") {
    for (const s of strs) {
      console.log(s);
     }
  } else if (typeof strs === "string") {
  console.log(strs);
  } else {
  // 做点事
   }
 }

in运算符,用于确定对象是否具有某个名称的属性,以此来缩小潜在类型的范围。

type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ("swim" in animal) {
  return animal.swim();
}
return animal.fly();
}

instanceof 检查一个值是否是另一个值的“实例”,instanceof 也是一个类型保护,TypeScript 在由 instanceof 保护的分支中实现缩小。

function logValue(x: Date | string) {
if (x instanceof Date) {
  console.log(x.toUTCString());
} else {
  console.log(x.toUpperCase());
}
}
logValue(new Date()) // Mon, 15 Nov 2021 22:34:37 GMT
logValue('hello ts') // HELLO TS

2.2.真值缩小,等值缩小,使用类型谓词

我们可以在条件、 && 、 || 、 if语句、布尔否定 ( ! ) 等中使用任何表达式。例如, if 语句不希望它们的条件总是具有类型 boolean 。

function printAll(strs: string | string[] | null) {
if (strs && typeof strs === "object") {
  for (const s of strs) {
  console.log(s);
  }
 } else if (typeof strs === "string") {
  console.log(strs);
 }
}

typescript 也使用分支语句做 === , !== , == ,和 != 等值检查,来实现类型缩小。例如:

function example(x: string | number, y: string | boolean) {
  if (x === y) {
  // 现在可以在x,y上调用字符串类型的方法了
    x.toUpperCase();
    y.toLowerCase();
   } else {
    console.log(x);
    console.log(y);
  }
}

为了定义一个用户定义的类型保护,我们只需要定义一个函数,其返回类型是一个类型谓词。在这个例子中, pet is Fish 是我们的类型谓词。谓词的形式是 parameterName is Type ,其中parameterName 必须是当前函数签名中的参数名称。

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
}

3.函数

3.1.函数类型表达式

语法 (a: string) => void意味着"有一个参数的函数,名为a,类型为字符串,没有返回值"。就像 函数声明一样,如果没有指定参数类型,它就隐含为 any 类型。

type GreetFunction = (a: string) => void;
function greeter(fn: GreetFunction) {
  // ...
}

3.1.1.调用签名:

除了可调用之外,函数还可以有属性。然而,函数类型表达式的语法不允许声明属性。 如果我们想用属性来描述可调用的东西,我们可以在一个对象类型中写一个调用签名。 注意,与函数类型表达式相比,语法略有不同:在参数列表和返回类型之间使用 : 而不是 => 。

type DescribableFunction = {
  description: string;
  (someArg: number): boolean;
}
function doSomething(fn: DescribableFunction) {
  console.log(fn.description + " returned " + fn(6));
}
function fn1(n:number) {
  return true
}
fn1.description = 'balabala...'
doSomething(fn1)

3.1.2.构造签名:

JavaScript函数也可以用 new 操作符来调用。TypeScript将这些称为构造函数,因为它们通常会创建一个新的对象。你可以通过在调用签名前面添加 new 关键字来写一个构造签名。

class Ctor {
  s: string
  constructor(s: string) {
    this.s = s
  }
}
type SomeConstructor = {
  new (s: string): Ctor
}
function fn(ctor: SomeConstructor) {
  return new ctor("hello")
}
const f = fn(Ctor)
console.log(f.s)

3.2.泛型函数

泛型函数:高度抽象化的类型。在声明函数时将类型抽象化( 可以是多个类型 ):在函数名后面加上尖括号,里面为抽象化的类型名 (例如: <T, K, U, ... >,其中 T, K, U 是类型参数,各代表一种类型,至于具体是什么类型,在调用函数时由传入的类型决定。 ),在调用函数时再具体化,传入实际的类型,一旦传入类型,所有出现该泛型的地方,都会替换为这个传入的类型。如果没有传入明确的类型,则TS会进行类型推论,自动判断Type的类型

function firstElement<Type>(arr: Type[]): Type | undefined {
  return arr[0];
}

function map<Input, Output>(arr: Input[], func: (arg: Input) => Output):Output[] {
  return arr.map(func);
}
// 参数'n'是'字符串'类型。
// 'parsed'是'number[]'类型。
const parsed = map(["1", "2", "3"], (n) => parseInt(n));

TypeScript可以推断出输入类型参数的类型(从给定的字符串数组),以及基于函数表达式的返回值(数字)的输出类型参数。

3.2.1.限制条件

。很多时候,我们会希望给泛型做一定的约束,让它只能是某些类型之中的一种。这时候,可以使用extends关键字,来实现泛型约束。

function longest<Type extends { length: number }>(a: Type, b: Type) {
  if (a.length >= b.length) {
     return a;
   } else {
     return b;
   }
}
// longerArray 的类型是 'number[]'
const longerArray = longest([1, 2], [1, 2, 3]);
// longerString 是 'alice'|'bob' 的类型。
const longerString = longest("alice", "bob");
// 错误! 数字没有'长度'属性
const notOK = longest(10, 100);

3.2.2.指定类型参数

在前面的例子中,我们都没有手动传入类型,来指定泛型的实际类型,而是由TS自动进行类型推论得出的。不过有些时候,由于泛型太抽象,仅仅靠TS的类型推论,可能无法得出正确的结果。这时候,我们可以在调用函数时手动传入类型,来指定类型参数。

function combine<Type>(arr1: Type[], arr2: Type[]): Type[] { 
  return arr1.concat(arr2); 
 }
 // 会报错,TS根据第一个参数数组[1,2,3],将Type推论为number, 
 // 于是第二个字符串数组就无法通过类型校验, 因为Type[]此时应为number[] 
 combine([1,2,3],['a','b','c'])
 
 //这种情况下需要制定参数
 const arr = combine<string | number>([1, 2, 3], ["hello"]);

3.3.可选参数

函数经常需要一个可变数量的参数。例如, number 的 toFixed 方法需要一个可选的数字计数。通过将参数用 ? 标记

function f(x?: number) {
  // ...
}
f();
f(10); 

3.4.函数重载

我们可以通过编写重载签名来指定一个可以以不同方式调用的函数。要做到这一点,要写一些数量的函数签名(通常是两个或更多),然后是函数的主体:

// 定义两套重载签名 
// 允许调用函数时只传入name参数
function setUserInfo(name: string): boolean;
// 允许调用函数时传入name, age, gender三个参数
function setUserInfo(name:string, age:number, gender: 1 | 2):string;
// 真值校验,由于两套重载签名规定,调用函数时要么传入三个参数 
// 因此,传入了age,则必定也传入了gender
 if(age){ 
     return `我叫 ${name}, 今年 ${age} 岁啦!` 
   }else{
      return false 
   } 
 }
 
 // 传入一个参数,正确 
 setUserInfo('cc') 
 // 传入三个参数,正确 
 setUserInfo('cc', 18, 2) 
 // 传入两个参数,报错,因为没有定义两个参数的重载签名 
 setUserInfo('cc', 18)

3.5.在函数中声明this

一般而言,TS会如同JS一样,自动推断this的指向。JS中不允许this作为参数,不过TS允许我们在函数中声明this的类型,这种情况尤其在函数的回调参数callback中较为常见

// filterUser个方法,其后是其调用签名 
interface Data { 
  filterUsers(filter: (this: User) => boolean): User[];
}

它的filterUsers就是一个函数的调用签名,这里声明了this是User类型,如果在该方法执行时,callback中的this不是User类型,TS就会提示我们代码写的有误。在函数中声明this时,需要注意一点是,虽然在构造签名中,callback使用箭头形式,但是在我们实际调用该方法时,callback不能使用箭头函数,只能用function关键字。毕竟众所周知,箭头函数没有自己作用域的this,它使用的的this同定义箭头函数时的上下文的this。

3.6.函数展开运算符

3.6.1.形参展开

除了使用可选参数或重载来制作可以接受各种固定参数数量的函数之外,我们还可以使用休止参数来定 义接受无限制数量的参数的函数。rest 参数出现在所有其他参数之后,并使用 ... 的语法:

function multiply(n: number, ...m: number[]) {
  return m.map((x) => n * x);
}
// 'a' 获得的值 [10, 20, 30, 40]
const a = multiply(10, 1, 2, 3, 4);

3.6.2.实参展开

我们可以使用 spread 语法从数组中提供可变数量的参数。例如,数组的 push 方法需要任意数 量的参数

const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
arr1.push(...arr2);

3.6.3. 参数解构

你可以使用参数重构来方便地将作为参数提供的对象,解压到函数主体的一个或多个局部变量中。它看起来像这样:

function sum({ a, b, c }) {
  console.log(a + b + c);
}
sum({ a: 10, b: 3, c: 9 });
//对象的类型注解
function sum({ a, b, c }: { a: number; b: number; c: number }) {
  console.log(a + b + c);
}
type ABC = { a: number; b: number; c: number };
function sum({ a, b, c }: ABC) {
  console.log(a + b + c);
}

4.对象类型

我们通过对象类型来表示这些对象。正如我们所见,它们可以是匿名的:

function greet(person: { name: string; age: number }) {
  return "Hello " + person.name;
}

//接口命名
interface Person {
  name: string;
  age: number;
}
//类型别名
type Person = {
 name: string;
 age: number;
};
function greet(person: Person) {
  return "Hello " + person.name;
}

4.1.属性修改器

对象类型中的每个属性都可以指定几件事:类型、属性是否是可选的,以及属性是否可以被写入。

4.1.1.可选属性

在这些属性的名字后面加上一个问号(?),把它们标记为可选的。

type Shape = {}
interface PaintOptions {
  shape: Shape;
  xPos?: number;
  yPos?: number;
}
function paintShape(opts: PaintOptions) {
 // ...
}
const shape:Shape = {}
paintShape({ shape });
paintShape({ shape, xPos: 100 });
paintShape({ shape, yPos: 100 });
paintShape({ shape, xPos: 100, yPos: 100 });

4.1.2.只读属性

属性也可以被标记为只读。虽然它不会在运行时改变任何行为,但在类型检查期间,一个标记为只读的属性不能被写入

interface SomeType {
  readonly prop: string;
}
function doSomething(obj: SomeType) {
// 可以读取 'obj.prop'.
  console.log(`prop has the value '${obj.prop}'.`);
// 但不能重新设置值
  obj.prop = "hello";
}

4.1.3.索引签名

我们在使用interface或者type alias定义某个对象类型时,以往的做法是列举出所有的属性名并规定它们的值的类型。但是有时候,我们不知道对象里会有哪些属性,无法逐个列出。此时,我们可以使用索引签名,来规定属性名的类型和对应属性值的类型。

//通过索引签名定义一个伪数组,规定了属性名必须是number
interface StringArray {
  [index: number]: string;
}
const myArray: StringArray = ['a', 'b'];
//报错因为属性名是string,不符合约束
const arr1 = {
 grades:'a'
}

当我们使用number类型作为属性名时,JS在把他放进对象之前会先将其转化为string类型。这意味着用 100 (一个 数字 )进行索引和用 "100" (一个 字符串 )进行索引是一样的,所以两者需要一致。

interface Animal {
  name: string;
}
interface Dog extends Animal {
  breed: string;
}
interface NotOkay {
  [x: number]: Animal;
  [x: string]: Dog;
}

然而,如果索引签名是属性类型的联合,不同类型的属性是可以接受的:

interface NumberOrStringDictionary {
  [index: string]: number | string;
  length: number; // 正确, length 是 number 类型
  name: string; // 正确, name 是 string 类型
}

4.2.扩展类型

我们可以从已有的对象类型的配置中,生成一个全新的类型,使其不仅含有原来类型的所有属性,还能拥有自己独有的属性。这样可以方便地实现类型复用,避免过多地重复敲代码,提高我们的工作效率。在定义新类型时,使用 interface 和 type 关键字,实现类型拓展的方式会有差别

4.2.1.类型继承

使用interface关键字声明的对象类型,可以通过extends关键字来继承其它的对象类型,从而直接获得父类型的所有属性配置、属性签名,而不必重复列举一遍可以同时继承多个对象类型

interface A {
  name: string;
}
interface B{
  age:number
}
//C类型继承A类型,包含name属性
interface C extends A {
  unit: string;
}
//D类型继承多个,同时拥有name age属性
interface D extends A,B{
  grade:string
}

4.2.2.交叉类型

通过type关键字给一个对象类型起别名时,使用 & 符号来连接多个类型,从而产生一个新类型,新的类型包含所有其它对象类型的属性,即类型交叉。

type A = {
    name: string
}
interface B {
    age: number
}
type C = A & B

如果 & 连接的是简单联合类型,则产生的新类型是 & 符号两边类型的公有类型。

type A = string|number
type B = string[]|number
//C是AB的共有类型 number
type C = A & B

4.2.3.接口与交叉类型

接口可以定义多次,多次的声明会自动合并:

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

但是类型别名如果定义多次,会报错:不能重复定义

type A = {
  name: string;
}
type A = {
  age: number;
}  //报错

4.3.泛型对象

泛型:使用尖括号<>来声明类型参数 (可以有多个)来表示暂时未知的类型,在实际声明变量时传入相应的类型 (或者由TS自动推论) 来替换相应出现该类型参数的地方,从而将抽象的、未知的类型替换为具体的、已知的类型。一个类型参数指代一种类型,例如<T,K,U,…>分别指代一种暂时未知的类型。将泛型用于定义对象类型,便得到了泛型对象

interface Box<Type> {
  contents: Type;
}
let boxA: Box<string> = { contents: "hello" };
boxA.contents;

泛型同样可在类型别名中使用。而类型别名除了定义对象类型之外,还能用泛型来定义各种其它类型。因此,我们可以使用泛型嵌套来定义更为复杂的类型结构 

type OrNull<Type> = Type | null;
type OneOrMany<Type> = Type | Type[];
type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>;
type OneOrManyOrNullStrings = OneOrManyOrNull<string>;

4.4. 数组类型

数组类型。每当我们写出 number[] 或 string[] 这样的类型时,这实际上只是

Array<number>,Array<string>

function doSomething(value: Array<string>) {
}
let myArray: string[] = ["hello", "world"];
// 这两样都能用
doSomething(myArray);
doSomething(new Array("hello", "world"));

的缩写

4.4.1.只读数组类型

ReadonlyArray 是一个特殊的类型,描述了不应该被改变的数组。

  • 无法进行新增、删除、替换等操作,push、pop等修改自身的方法都无法使用。
  • 可以使用不修改自身的数组方法。例如使用slice方法,返回一个普通的数组
  • 代表一种类型,无法当作构造函数使用,不能使用new操作符。
  • 在声明一个只读数组类型的变量时需指定类型参数,只读数组里只能存放该类型的值。
  • 只读数组类型的变量本身可以接收普通数组的赋值。
  • 普通数组不能接收只读数组的赋值
//指定只读数组
let A:ReadonlyArray<number> = [1,2,3];
//报错
a.pop();
//可以使用slice方法
let C =A.slice(0);
//该变量可以普通重新赋值
A = [1,2,3,4];
let v = new ReadonlyArray();
let b:Array<number> = [4,5,6];
//错误
b = A

4.5.元组类型

元组类型是另一种特殊的数组类型,一般来说它可以存储不同类型的成员,限定了数组的长度及每个成员的类型。(普通数组也可以通过指定联合类型的类型参数,从而存放不同类型的成员) 。元组的成员是可以被修改,可以调用一系列的数组方法。

这是由三个成员的元组类型

type A = [string,number,'g'|'q']
  • 元组可以被解构
function B(a:A){
  const [name,age,gr] = A;
  console.log(name)
}
  • 元组可以指定可选成员(只有最后一个成员可选)
type a = [string, number, Array<string>?];
  • 元组中可使用剩余参数,来指定某个或某些索引位置的成员的类型和其它成员的类型。此时,元组没有长度限制。
type StringNumberBooleans = [string, number, ...boolean[]];

4.5.1.只读元组类型

我们声明一个只读元组:readonly [string, number] ,使元组的成员变为只读成员,不可修改。readonly关键字只能用于字面量数组或字面量元组类型之前,不可用于类型别名之前

type UserInfo3 = [ string, number,...FamilyMember[]]
// ok
let cc: readonly [string, number] = ['cc', 18]
// 报错
let yy: readonly UserInfo3 = [['dd'], '1', 1]
// 用于字面量元组之前,ok
let yy: readonly [...FamilyMember[], string, number] = [['dd'], '1', 1]
// 报错,只读属性不可修改
cc[0] = 'yy'

5.类型操纵

通过结合各种类型操作符,用一种简洁、可维护的方式来表达复杂的操作和值;用现有的类型或值来表达一个新类型的方法。

  • 泛型 —— 带参数的类型
  • Keyof 类型操作符 —— keyof 操作符创建新类型
  • Typeof 类型操作符 —— 使用 typeof 操作符来创建新的类型
  • 索引访问类型 —— 使用 Type['a'] 语法来访问一个类型的子集
  • 条件类型 —— 在类型系统中像if语句一样行事的类型
  • 映射类型 —— 通过映射现有类型中的每个属性来创建类型
  • 模板字面量类型 —— 通过模板字面字符串改变属性的映射类型

5.1.泛型

5.1.1.泛型类型、泛型接口及泛型类

// 定义一个函数,这个函数将返回传入的任何参数(捕获参数的类型,以便返回内容)
function returnType<Type>(data: Type): Type{
    return Type
}
 
// 调用该函数的方式
// 方式一:显示指定函数接收 与 返回值 的类型
let data = returnType<String>("Hello")
 
// 方式一:不显式指定,使用隐式的类型推断
let data = returnType("Hello")
 
// ********************************泛型 类 与 接口************************************
// 泛型接口
interface GenericId<Type> {
    (arg: Type): Type;  //调用签名
}
function identity<Type>(arg: Type): Type {
    return arg;
}
let myIdentity: GenericId = identity
let myIdentity: <Type>(arg: Type) => Type = identity;
//可以把泛型写成一个对象类型的调用签名
let myIdentity: { <Type>(arg: Type): Type } = identity;
 
 
// 泛型类
class GenterId<Type>{
    id: Type;
    addId: (x: Type , y: Type) => Type
}
let newGenterId = new GenterId<Number>();
newGenter.id = 1123432;
newGenter.addId = function(x,y){
    return (x+y)*10;
}

5.1.2.泛型约束

extends关键字

function identity<Type>(arg: Type): Type {
    console.log(arg.length) 
    // 会报语法错误,因为typescript会自动推断且不能保证arg是否包含length属性
}
// 给上述代码添加 类型限制
interface hasLength {
    length: number
}
function identity<Type extends hasLength>(arg: Type): Type {
    console.log(arg.length) 
    // 做了类型限制(不管传递什么值,
    //都要有length这个属性,否则在函数调用的时候就会报错),就不会报错了
}

5.1.3.在泛型中使用类型参数(keyof)

声明一个受另一个类型参数约束的类型参数。

// 这里的 Key extends keyof Type 表示Key的类型值,只能是Type类型中的key值
function getProp<Type,Key extends keyof Type>(obj: Type,key: Key){
    return obj[key]
}
// 这个key值,只能是'a','b','c'中的一个,其他的值都会报错
console.log(getProp({'a':1111,'b':2222, 'c':3333},'a'));

5.1.4.在泛型中使用类类型

在TypeScript中使用泛型创建工厂时,有必要通过其构造函数来引用类的类型。

//使用原型属性来判断和约束类类型的构造函数与实例方法之间的关系
class BeeKeeper {
  hasMask: boolean = true;
}
class ZooKeeper {
  nametag: string = "Mikle";
}
class Animal {
  numLegs: number = 4;
}
class Bee extends Animal {
  keeper: BeeKeeper = new BeeKeeper();
}
class Lion extends Animal {
  keeper: ZooKeeper = new ZooKeeper();
}
function createInstance<A extends Animal>(c: new () => A): A {
  return new c();
}
createInstance(Lion).keeper.nametag;

createInstance(Bee).keeper.hasMask;

5.2.keyof操作符

  • keyof 运算符接收一个对象类型,并产生其的字符串或数字字面联合( "x"|"y ")。
  • 如果该类型有一个字符串或数字索引签名, keyof 将返回这些索引的类型。
type Point = { x: number; y: number };
type P = keyof Point;    // 这里的P的类型为 'x'|'y'
const p1:P = 'x'
const p2:P = 'y'
 
// 当一个对象有索引时,keyof 的值就是其索引对应的值
type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish;
const a:A = 0
 
// M 是 string|number 是因为JavaScript对象的键总是被强制为字符串,所以 obj[0] 总是与 obj["0"] 相同
type Mapish = { [k: string]: boolean };
type M = keyof Mapish;
const m:M = 'a'
const m2:M = 1

5.3.typeof类型操作符

添加了一个 typeof 操作符,可以在类型上下文中使用它来引用一个变量或属性的类型

let s = "hello";
let n: typeof s;
n = 'world'
n= 100    // 报错,n的类型是string

5.4.索引访问类型

type Person = { age: number; name: string; alive: boolean };
type Age = Person["age"];    // 这个时候Age的类型就是索引age对应的类number
let A: Age = 100;    // 正确
let B: Age = '100';    // 错误

索引类型本身就是一个类型,所以可以使用 unions、 keyof 或者其他类型。

interface Person {
    name: string,
    age: number,
    alive: boolean
}
// 索引 => 联合类型
// I 的类型为 number | boolean
type I = Person['age' | 'alive']
// 索引 => keyof操作符
// I 的类型为 string | number | boolean
type I = Person[keyof Person];

5.5.条件类型

interface Animal {
    live: void()
}
interface Dog extends Animal {
    wfun: void
}
// 通过条件表达式动态决定类型
// type expl1 = number  
type expl1 = Dog extends Animal ? number : string
 
// type expl1 = string
type expl2 = Date extends Animal ? number : string

5.5.1.条件类型约束

条件类型中的检查会给一些提示信息。就像用类型守卫缩小范围那样给一个更具体的类型一样,条件类型的真正分支将通过检查的类型进一步约束泛型。

// 会报语法错误: T没有索引message
type MessageOf<T> = T["message"]
// 给T做一个类型限制就可以了
type MessageOf<T extends {message: unknown}> = T["message"]
 
// 使用条件类型去判断,也可对T进行类型限制
type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
interface Email {
    message: string;
}
interface Dog {
    bark(): void;
}
// type EmailMessageContents = string
type EmailMessageContents = MessageOf<Email>;
 
// type DogMessageContents = never
type DogMessageContents = MessageOf<Dog>;

5.5.2.在条件类型中进行推理

条件类型提供了一种方法来推断在真实分支中使用 infer 关键字进行对比的类型。 我们可以从函数类型中提取出返回类型:

type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
? Return: never;
// type Num = number
type Num = GetReturnType<() => number>;
// type Str = string
type Str = GetReturnType<(x: string) => string>;
// type Bools = boolean[]
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>;

// 给泛型传入 string 类型,条件类型会返回 never

type Never = GetReturnType<string>

const nev:Never = 'error' as never

5.5.3.分布式条件类型

当给条件类型定义的新类型传入一个联合类型时,就会变成分布式

type toArray<Type> = Type extends any ? Type[] : never
// 给toArray传入一个联合类型,得到的新类型为 type newData = String[] | Number[]
type newData = toArray<String | Number>
// 如果想要避免得到这样 String[] | Number[] 类型,可以对toArray做个调整
type toArray<Type> = [Type] extends [any] ? Type[] : never
//  type newData = (String | Number)[]
type newData = toArray<String | Number>

5.6.映射类型

映射类型是一种通用类型,它使用 PropertyKeys 的联合(经常通过 keyof 创建)迭代键来创建一个类型。

type FeatureFlags = {
  darkMode: () => void;
  newUserProfile: () => void;
}
type FeatureOptions = OptionsFlags<FeatureFlags>;

//OptionsFlags 将从 Type 类型中获取所有属性,并将它们的值改为布尔值。
//type FeatureOptions = {darkMode: boolean;newUserProfile: boolean;}

5.6.1.映射类型修改器

在映射过程中,有两个额外的修饰符可以应用: readonly 和  ?  ,它们分别影响可变性和可选性。 你可以通过用 - 或  +  作为前缀来删除或添加这些修饰语。如果你不加前缀,那么就假定是 + (默认值) 。

type CreateMutable<Type> = {
    // 从一个类型的属性中删除 "readonly"属性
    -readonly [Property in keyof Type]: Type[Property];
};
type LockedAccount = {
    readonly id: string;
    readonly name: string;
};
/*
    type UnlockedAccount = {
        id: string;
        name: string;
    }
*/
 
type UnlockedAccount = CreateMutable<LockedAccount>;
// 从一个类型的属性中删除 "可选" 属性
type Concrete<Type> = {
    [Property in keyof Type]-?: Type[Property];
};
type MaybeUser = {
    id: string;
    name?: string;
    age?: number;
};
/*
    type User = {
        id: string;
        name: string;
        age: number;
    }
*/
type User = Concrete<MaybeUser>;

5.6.2.通过as做key的重映射

可以使用as

type MapNewKeyOfMap<Type> = {
    [Property in keyof Type as newType]: Type[Property]
}
 
// 可以利用其他功能,从先前的属性名称中创建新的属性名称。
type Getters<Type> = {
    [Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]
};
interface Person {
    name: string;
    age: number;
    location: string;
}
/*
    type LazyPerson = {
        getName: () => string;
        getAge: () => number;
        getLocation: () => string;
    }
*/
type LazyPerson = Getters<Person>

6.类

6.1.类的属性与方法

字段也可以有初始化器;这些初始化器将在类被实例化时自动运行。

// 声明并初始化属性的两种方式 (如果只声明,不初始化就会报错)
 
// 方式一
class pointer {
    x: number = 0
    y: number = 0
}
// 方式二,在constructor函数中初始化
class pointer {
    x: number
    y: number
    constructor(){
        this.x = 0
        this.y = 0
    }
}

6.1.1.readonly

字段的前缀可以是 readonly 修饰符。这可以防止在构造函数之外对该字段进行赋值

class Greeter {
  readonly name: string = "world";
  constructor(otherName?: string) {
  if (otherName !== undefined) {
     this.name = otherName;  // name 只读属性的值只能在 constructor 函数中被修改
    }
  }
 err() {
   this.name = "not ok"; // name是只读属性,重新修改他的值会报错
  }
}
const g = new Greeter();
g.name = "also not ok"; // name是只读属性,重新修改他的值会报错

6.1.2.构造器

class Point {
    x: number;
    y: number;
    // 带默认值的正常签名
    constructor(x = 0, y = 0) {
        this.x = x;
        this.y = y;
    }
}
class Point {
    // 重载
    constructor(x: number, y: string);
    constructor(s: string);
    constructor(xs: any, y?: any) {
    // ...
    }
}

super();如果在子类中写了构造函数,在子类构造函数中必须对父类的构造函数进行调用

在类的方法体中,仍然必须通过 this 访问字段和其他方法。

class Animal{
    name: string;
    constructor(name: string){
        this.name = name
    }
    sayHello(){
        this.name = 'hh';
        console.log('Hello World');
    }
}
class Cat extends Animal{
    age: number
    constructor(name:string, age: number){
        super(name); // 这里调用父类的构造函数
        this.age = age; 
    }

    sayHello(): void {
        super.sayHello(); // 在类方法中super表示当前类的父类
    }
}

const cat = new Cat('tom', 12);
console.log(`I'm ${cat.name}, I'm ${cat.age} years old.`); //I'm tom, I'm 12 years old.
cat.sayHello(); // 'Hello World'

6.1.3.Getters和Setters

如果存在get,但没有set属性,则该属性自动是可读的
如果没有指定setter参数的类型,它将从getter返回类型中推断出来
访问器和设置器必须有相同的成员可见性 get和set的函数方法名相同

class A {
    _data = 0
    get data(){
        return this._data
    }
    
    set data(value){
        this._data = value
    }
}
const a = new A()
console.log(a.data) // 0

6.1.4.索引签名

类可以申明索引签名,这些签名的作用其他对象类型的索引签名作用一样

class MyClass {
    [s: string]: boolean | ((s: string) => boolean);
    check(s: string) {
        return this[s] as boolean;
    }
}

6.2.类继承

6.2.1.implements

使用 implements 去继承接口。如果一个类不能正确地去实现接口,就会报错

interface  Pingable {
    ping(): void;
}
class Sonar implements Pingable {
    ping() {
        console.log("ping!");
    }
}
 
// 这个类 Ball 会报错,因为没有实现接口中的方法ping
class Ball implements Pingable {
    pong() {
        console.log("pong!");
    }
}

implements 子句只是检查类是否可以被当作接口类型来对待。它根本不会改变类的类型或其方法

interface Checkable {
    check(name: string): boolean;
}
 
class NameChecker implements Checkable {
    check(s) {
        // 接口不会影响类实现时的参数类型,所以这里的s 有 any类型的隐式转换,所以s不能调用toLowercse 这个方法
        return s.toLowerCase() === "ok";
    }
}
 
 
 
// 假如接口中存在可选属性,那么类在实现这个接口的时候写不写都不影响,但是如果不实现的话,这个类在实例化的时候就不能调用接口那个可选参数
interface A {
    x: number;
    y?: number;
}
class C implements A {
    x = 0;
}
const c = new C();
c.y = 10;    // 会报错,因为类C没有实现可选参数y,故可选参数y不能被其实例化对象调用

6.2.2.extends

类与类之间的继承关系,使用extends关键字,子类拥有父类的属性和方法

class Animal {
    move() {
        console.log("Moving along!");
    }
}
class Dog extends Animal {
    woof(times: number) {
        for (let i = 0; i < times; i++) {
            console.log("woof!");
        }
    }
}
const d = new Dog();
// 父类的类方法
d.move();
// 子类的类方法
d.woof(3);

6.2.3.重写方法

子类可以覆盖父类的一个字段或属性。你可以使用 super. 语法来访问父类方法

class Base {
    greet() {
        console.log("Hello, world!");
    }
}
class Derived extends Base {
    // 重写父类的方法greet 正常greet这个函数是跟父类一致的应该没有参数的,
    //所以name用的是可选参数,如果不是可选参数**greet(name:string)**的话,则不属于重写,而是重新定义的一个新的函数会报错
    greet(name?: string) {    
        if (name === undefined) {
            super.greet();
        } else {
            console.log(`Hello, ${name.toUpperCase()}`);
        }
    }
}
const d = new Derived();
d.greet();
d.greet("reader");

6.2.4.子类父类的初始化顺序

类初始化的顺序是:

    父类的字段被初始化
    父类构造函数运行
    子类的字段被初始化
    子类构造函数运行
class Base {
    name = "base";
    constructor() {
        console.log("My name is " + this.name);
    }
}
class Derived extends Base {
    name = "derived";
    constructor() {
        super()
        console.log("My name is " + this.name);
    }
}
// 打印 "My name is base",然后才是 "My name is derived"
const d = new Derived();

6.2.5.继承内置类型

在继承内置类型的时候,子类在实例化一个新的对象,并使用这个对象调用类中的方法的时候可能会出现“xxx is not a function”这个错误。这个时候需要在子类的构造函数中进行特殊操作

class MsgError extends Error {
    constructor(m: string) {
        super(m);
        // 明确地设置原型。(不明确设置原型,在实例化对象之后调用sayHello方法会有运行时错误)
        Object.setPrototypeOf(this, MsgError.prototype)
    }
    sayHello() {
        return "hello " + this.message;
    }
}

6.3.成员可见性

  • public:

类成员的默认可见性是公共( public )的。一个公共( public ) 成员可以在任何地方被访问。 因为 public 已经是默认的可见性修饰符,所以你永远不需要在类成员上写它,但为了可读性的原 因,可能会选择将其写上。

  • private:

private 和 protected 一样,但不允许从子类中访问该成员。 私有( private )成员对子类是不可见的,所以子类不能增加其可见性。

  • protected: 受保护的( protected )成员只对它们所声明的类的子类可见
class Greeter { 
    public greet() { 
        console.log("Hello, " + this.getName()); 
    } 
    protected getName() {
        return "hi"; 
    } 
}
class SpecialGreeter extends Greeter {
    public howdy() {
    // 在此可以访问受保护的成员
     console.log("Howdy, " + this.getName());
    }
}
const g = new SpecialGreeter();
g.greet(); // 没有问题
g.getName(); // 报错 无权访问

6.4.静态成员

类可以有静态成员。这些成员并不与类的特定实例相关联。它们可以通过类的构造函数对象本身来访问

class MyClass {
    static x = 0;
    static printX() {
        console.log(MyClass.x);
    }
}
// 类中的静态成员,只能类本身访问,不与类的实例对象相关联
console.log(MyClass.x);
MyClass.printX();

// 静态成员也可以使用相同的 public 、 protected 和 private 可见性修饰符。
class MyClass {
    private static x = 0;
    static printX() {
        console.log(MyClass.x);
    }
}
 
 
// 静态成员也会被继承。
class Base {
    static getGreeting() {
        return "Hello world";
    }
}
class Derived extends Base {
    myGreeting = Derived.getGreeting();
}

6.5.类的static区块

静态块允许写一串有自己作用域的语句,可以访问包含类中的私有字段。这意味着我们可以用写语句的所有能力来写初始化代码,不泄露变量,并能完全访问我们类的内部结构。

class Foo {
    static #count = 0;    // 私有字段count,#只能在es2015之后使用
    get count() {
        return Foo.#count;
    }
    // 静态区块
    static {
        try {
            const lastInstances = {
                length: 100
            };
        Foo.#count += lastInstances.length;
        }
        catch {}
    }
}

6.6.泛型类

类可以像接口一样使用通用约束默认值。 静态成员中的类型参数不能引用类的类型参数

class Box<Type> {
  contents: Type;
  constructor(value: Type) {
    this.contents = value;
  }
}
// const b: Box<string>
const b = new Box("hello!");


class Box<Type> {
    // 报错 静态成员不能引用类的类型参数。
    static defaultValue: Type;
}
// Box<string>.defaultValue = 'hello'
// console.log(Box<number>.defaultValue

6.7.类运行时的this

class MyClass {
    name = "MyClass";
    getName() {
        return this.name;
    }
}
const c = new MyClass();
const obj = {
    name: "obj",
    getName: c.getName,
};
// 输出 "obj", 而不是 "MyClass" this指向很奇特
console.log(obj.getName());
 
 
// 想要解决这种this指向问题的方法如:
// 1、在类中定义方法时,使用箭头函数
class MyClass {
    name = "MyClass";
    getName = () => {
        return this.name;
    };
}
const c = new MyClass();
const obj = {
    name: "obj",
    getName: c.getName,
};
// 输出 "MyClass"
console.log(obj.getName());
 
// 2、this参数
// 在方法或函数定义中,一个名为 this 的初始参数在TypeScript中具有特殊的意义。这些参数在编译过程中会被删除
class MyClass {
    name = "MyClass";
    getName(this: MyClass) {
        return this.name;
    };
}
const c = new MyClass();
// 输出 "MyClass"
console.log(c.getName());

6.8.参数属性

将构造函数参数变成具有相同名称和值的类属性。这些被称为参数属性,通过在构造函数参数前加上可见性修饰符 public 、 private 、 protected 或 readonly 中的一 个来创建

class Params {
    // 参数属性
    constructor(public readonly x: number, protected y: number, private z: number)
    {
        // ……
    }
}
const a = new Params(1, 2, 3);
// (property) Params.x: number
console.log(a.x);
console.log(a.z)

6.9.类表达式

类表达式与类声明非常相似。唯一的区别是,可以通过它们最终绑定的标识符来引用它们。

const someClass = class<Type> {
    content: Type;
    constructor(value: Type) {
        this.content = value;
    }
};
// const m: someClass<string>
const m = new someClass("Hello, world")

6.10.抽象类与成员

抽象的方法或抽象的字段是没有提供实现的方法或字段。这些成员必须存在于一个抽象类中, 不能直接实例化。 抽象类的作用是作为子类的父类,实现所有的抽象成员。

abstract class Base {
    abstract getName(): string;
    printName() {
        console.log("Hello, " + this.getName());
    }
}
// const b = new Base(); //这是错误的,抽象类不能直接被实例化
 
class Derived extends Base {
    getName() {
        return "world";
    }
}
const d = new Derived();  // 继承抽象类Base,并将抽象类中的所有抽象方法都给重写了,不然会报错
d.printName()

7.ts中的模块

7.1.ES模块语法

1.默认导出 (default)

// @filename: hello.ts

export default function helloWorld() {
  console.log("Hello, world!");
}

//导入
import hello from "./hello.js";
hello();

2.直接导出

// @filename: maths.ts
export var pi = 3.14;
export let squareTwo = 1.41;
export const phi = 1.61;
export class RandomNumberGenerator {}
export function absolute(num: number) {
  if (num < 0) return num * -1;
  return num;
}

//导入
import { pi, phi, absolute,RandomNumberGenerator } from "./maths.js";

7.2.额外的导入语法

1.重命名的方式{old as new}

import { pi as π } from "./maths.js";

2.混合导入

import phi, { pi as π } from "./maths.js";
    • as name
import * as math from "./maths.js";

console.log(math.pi);

// const positivePhi: number

const positivePhi = math.absolute(math.phi)

7.3.ts特定ES模块

1.直接导出类型

// @filename: animal.ts
export type Cat = { breed: string; yearOfBirth: number };
export interface Dog {
  breeds: string[];
  yearOfBirth: number;
}

// @filename: app.ts
import { Cat, Dog } from "./animal.js";
type Animals = Cat | Dog;

2.import type 用于申明一个类型的导入

// @filename: animal.ts

export type Cat = { breed: string; yearOfBirth: number };
export type Dog = { breeds: string[]; yearOfBirth: number };

export const createCatName = () => "fluffy";

// @filename: valid.ts

import type { Cat, Dog } from "./animal.js";

export type Animals = Cat | Dog;

// @filename: app.ts

import type { createCatName } from "./animal.js";

const name = createCatName();

3.内联类型导入 以type为前缀

// @filename: app.ts

import { createCatName, type Cat, type Dog } from "./animal.js";
export type Animals = Cat | Dog;
const name = createCatName();

7.4.CommonJs导出

function absolute(num: number) {
  if (num < 0) return num * -1;
    return num;
}
module.exports = {
  pi: 3.14,
  squareTwo: 1.41,
  phi: 1.61,
  absolute,
};

const maths = require("maths");
// pi: any
maths.pi;
const { squareTwo } = require("maths");
// const squareTwo: any
squareTwo;