TS一-基础

TypeScript演练场,一个用于 TypeScriptJavaScript 的在线编辑器,可以通过演练场以安全且可共享的方式在线编写 TypeScriptJavaScript

安装:npm i typescript -g;使用tsc -v查看版本

1.类型注解和类型推断

  • 为每一个变量提供了一个确定的类型,这种做法就叫做类型注解

  • 当我们没有为其提供一个确定的类型,但提供了一个确定的值,那么TypeScript会根据我们给定的值的类型自动推断出这个变量的类型,这就叫类型推断

2.基础类型

JavaScript的基础类型:numberstringbooleannullundefinedsymbolBigInt

SymbolES6 引入了一种新的原始数据类型,表示独一无二的值。
BigIntES10引入的,表示非常大的数。跳转详情

1.字符串类型

let str: string = 'ABCD'  // 普通声明
let str: string = `dddd${a}` // 字符串模板

2.数字类型

let notANumber: number = NaN;  //Nan
let num: number = 123;    //普通数字
let binary: number = 0b1010;  //二进制
let octal: number = 0o744;   //八进制s
let decimal: number = 6;   //十进制
let hex: number = 0xf00d;   //十六进制
let infinityNumber: number = Infinity;//无穷大
Tips: numberBigInt

虽然numberbigint都表示数字,但是这两个类型不兼容。

let big: bigint =  100n;
let num: number = 6;
big = num;
num = big;

会抛出一个类型不兼容的 ts(2322) 错误。

3.布尔类型

// 正确使用:
let booleand: boolean = true     //可以直接使用布尔值
let booleand2: boolean = Boolean(1)     //也可以通过函数返回布尔值

// 错误使用
let createdBoolean: boolean = new Boolean(1)
//这样会报错 应为事实上 new Boolean() 返回的是一个 Boolean 对象 

4.空值void

JavaScript 没有空值(Void)的概念,在TypeScript中,可以用 void 表示没有任何返回值的函数

function sayHello (): void {
  console.log('Hello, world')
}

声明一个void类型的变量没有什么用,因为只能将其赋值为undefinednull(只在 --strictNullChecks 未指定时)

 let dd: void = undefined // 正确的
 let cc: void = null  // strictNullChecks 未指定时才通过
 // 赋值为null时会报红,只有在 strictNullChecks 未指定;或者tsconfig.json中配置strictNullChecks为false时不会报红

5.nullundefined

let u: undefined = undefined; //定义undefined
let n: null = null;  //定义null
1.与 void 的区别

undefinednull 是所有类型的子类型。也就是说 undefined 类型的变量,可以赋值给 string 类型的变量:

//这样写会报错 void类型不可以分给其他类型
let test: void = undefined
let num: string = "1"
 
num = test  // Type 'void' is not assignable to type 'string'.
//这样是没问题的
let test: null = null
let num2: string = "1"
 
num2 = test  // strictNullChecks 未指定时才通过
 
//或者这样的
let test: undefined = undefined
let num2: string = "1"
 
num2 = test  // strictNullChecks 未指定时才通过
2.空值报红报错原因

TypeScript 2.0 增加了对不可为空类型的支持。有一种新的严格空值检查模式,他提供了**strictNullChecks**来限制对空值的检查。可以通过在命令行上添加--strictNullChecks参数来启功严格空值检查。

也可以在项目的tsconfig.json文件中启用strictNullChecks编译器选项。(目前strictNullChecks默认为true

{
  "compilerOptions": {
    "strictNullChecks": true
    // ...
  }
}

一般strictNullChecks都是未指定的在严格空值检查模式下,nullundefined无法赋值给其他类型的变量

3.Anyunknown

1.Any

用来表示可以接受任何类型的值。声明变量的时候没有指定任意类型默认为any

let anys:any = 123
anys = '123'
anys = true

let anys;
anys = '123'
anys = true

弊端如果使用any 就失去了TS类型检测的作用

2.unknown

any可以认为是全集,unknown是位置集。

//unknown 可以定义任何类型的值
let val:unknown
val = '123'
val = true

// 这样写会报错unknow类型不能作为子类型只能作为父类型 any可以作为父类型和子类型
// unknown类型不能赋值给其他类型
let unknownStr:unknown = '123'
let unknownStr2:string = unknownStr

// 这样就没问题 any类型是可以的
let anyStr:any = '123'
let anyStr2:string = anyStr

// unknown可赋值对象只有unknown 和 any
let b:unknown = '123'
let a:any= '456'
a = b

3.区别

二者都是顶级类型(top type),任何类型的值都可以赋值给顶级类型变量:

但是 unknownany 的类型检查更严格,any 什么检查都不做,unknown 要求先收窄类型:

const val1: unknown = "Hello World"; 
const str1: string = val1; // 报错:Type 'unknown' is not assignable to type 'string'.(2322) 

const val2: unknown = "Hello World"; 
const str2: string = val2 as string; // 不报错 如果改成 any,基本在哪都不报错。

// 如果是any类型在对象没有这个属性的时候还在获取是不会报错的
let obj1: any = { b: 1 }
obj1.a

// 如果是unknow 是不能调用属性和方法
let obj: unknown = { b: 1, ccc: (): number => 213 }
obj.b
obj.ccc()

所以能用 unknown 就优先用 unknown,类型更安全一点。

4.never

never 是底类型(bottom type),表示不应该出现的类型。

interface A { type: 'a' } 
interface B { type: 'b' } 
type All = A | B 
function handleValue(val: All) { 
 switch (val.type) { 
   case 'a': 
   val // 这里 val 被收窄为 A 
   break 
   case 'b': 
   val // val 在这里是 B 
   break 
   default: 
   val // 在编辑器中,鼠标移上去会显示:(parameter) val: never
   const exhaustiveCheck: never = val 
   break
 } 
}

// 或者像下列函数,绝不会返回任何数据,故用 never 修饰其返回值:
function fail(msg: string): never {throw new Error(msg);}

在其他场景中,我们或许很少会用到 never 类型,但 never 实际上却是可以无视泛型参数的 extends 类型约束,传入给任何泛型参数的存在。

5.接口interface和对象

TypeScript中,接口interface是一个比较重要的概念,它是对行为的抽象,而具体的行为需要由类去实现,接口interface中的任何代码都不会被最后编译到JavaScript中。

interface Person {
  name: string,
  age: number
}
let person: Person = {
  name: 'why',
  age: 23
}

变量只能接受接口规定的属性,且属性值的类型也必须和接口中规定的一致,多一个属性或者少一个属性在TypeScript中都不是被允许的。

interface Person {
  name: string,
  age: number    // age属性是可选的
}
// 编译报错
let person: Person = {
  name: 'why'
}
  • 可选属性:使用?操作符

    interface Person {
      name: string,
      age?: number  // 这里真实的类型应该为:number | undefined
    }
    let person: Person = {
      name: 'why'
    }
    
  • 任意属性:[propName: string]

    需要注意的是,一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集

    interface Person {
      name: string,
      age?: number,
      [propName: string]: any // 任意属性
    }
    let person: Person = {
      name: 'why',
      sex: 'Man'
    }
    
  • 只读属性: readonly

    readonly 只读属性是不允许被赋值的只能读取

    interface Person {
      name: string,
      readonly age: number,    // 只读
      [propName: string]: any  // 任意属性
    }
    let person: Person = {
      name: 'why',
      age: 20
      sex: 'Man'
    }
    // 编译报错
    person.age = 23
    
  • 添加函数类型声明

    interface Person {
      name: string,
      age: number,
      fnc: () => void,
      cb: (num: number) => number
    }
    let person: Person = {
      name: 'why',
      age: 20,
      fnc: (): void => {
        console.log('fnc...')
      },
      cb: (num): number => {
        return num + 10
      }
    }
    
    person.fnc()
    console.log(person.cb(20));
    

    这里只是简单的示例,更多更复杂的函数声明看下面内容

6.函数的类型

function sum1(x: number, y: number): number {
  return x + y
}
// 函数表达式
const sum2: (x: number, y: number) => number = function (x: number, y: number): number {
  return x + y
}
// 箭头函数
const sum3: (x: number, y: number) => number = (x: number, y: number): number => {
  return x + y
}

推断类型: 如果你在赋值语句的一边指定了类型但是另一边没有类型的话,TypeScript编译器会自动识别出类型:

let sum = function(x: number, y: number): number { return x + y; };

这叫做“按上下文归类”,是类型推论的一种。 它帮助我们更好地为程序指定类型。

  • 1.函数类型接口约束

    interface countInterface {
      (x: number, y: number): number
    }
    const count: countInterface = function (x: number, y: number): number {
      return x + y
    }
    console.log(count(1, 2))
    
  • 2.可选参数?

    //通过?表示该参数为可选参数
    const fn = (name: string, age?:number): string => {
        return name + age
    }
    fn('Bob')
    

    可选参数必须放在最后一个位置,否则会报错。

  • 3.参数默认值

    TypeScript里,我们也可以为参数提供一个默认值当用户没有传递这个参数或传递的值是undefined时。 它们叫做有默认初始化值的参数。

    // 参数默认值
    const fn = (name: string, age: number | string = '空'): string => {
      return `Name:${name}; Age: ${age}`
    }
    console.log(fn('Kcal'));  // Name:Kcal; Age: 空
    
  • 4.剩余参数

    在ES6中,我们可以使用...符号进行收缩剩余参数,在TypeScript的做法:

    // rest是一个数组,我们可以使用数组的类型来定义它
    function totals(a: number, ...rest: number[]) {
      console.log(a)    // 1
      console.log(rest) // [2, 3, 4]
    }
    
    totals(1, 2, 3, 4,)
    
  • 5.函数重载

    学习函数重载,先要了解什么是函数签名,定义如下

    • 函数签名:函数签名 = 函数名称 + 函数参数 + 函数参数类型 + 返回值类型。在TS函数重载中,包含了实现签名和重载签名,两者都是函数签名。
    • 重载:包含以下规则的函数就是TS函数重载
        1. 由一个实现签名 + 一个或多个重载签名合成;
        1. 外部调用函数重载时,稚嫩调用重载的前面 (函数体前面的叫做签名)不能够调用调用使用的函数签名。(这是TS规定的:实现签名下的函数体是给重载签名编写的,实现签名只是在定义时起到了统领所有重载签名的作用, 在执行调用时就看不到实现签名了)
        1. 调用函数重载时会根据传递的参数来判定调用的那个函数;
        1. 只有一个函数体,只有实现签名配备了函数体,所有的重载签名都只有签名,没有配置函数体。
    // 示例 1
    // function greet(person: string): string {
    //   return `Hello, ${person}!`;
    // }
    // greet('World');   // 'Hello, World!'
    
    /**
     * 把示例1改造为:接收字符串或字符串数组作为参数,并返回字符串或字符串数组
     *  方法1:通过判断参数类型来修改函数签名   --这是最直接最简单的
     *  方法2:函数重载(单独定义调用函数的方式)
     */
    
    // 方法1 改造示例
    function greet(person: string | string[]): string | string[] {
      if (typeof person === 'string') {
        return `Hello, ${person}!`;
      } else if (Array.isArray(person)) {
        return person.map(name => `Hello, ${name}!`);
      }
      throw new Error('error');
    }
    
    greet('World');          // 'Hello, World!'
    greet(['TS', 'JS']);     // ['Hello, TS!', 'Hello, JS!']
    
    
    // 方法2 改造示例
    // 重载签名
    function greet(person: string): string;
    function greet(persons: string[]): string[];
    // 实现签名 根据函数类型和数量作出不同的行为
    function greet(person: unknown): unknown {
      if (typeof person === 'string') {
        return `Hello, ${person}!`;
      } else if (Array.isArray(person)) {
        return person.map(name => `Hello, ${name}!`);
      }
      throw new Error('error');
    }
    
    

    如示例2:使用重载签名进行重载,好处在于可以对传入的参数进行限制,只有当签名存在对应类型或数量的参数时,才不会报错。

  • 6.(重载扩展1)使用接口搭配重载

    使用接口搭配重载签名或非重载签名

    interface fncInter {
      (name: 'keyA', age: number): void;
      (name: 'keyB', age: number): void;
      (name: 'keyC', age: number): void;
    }
    let fncA: fncInter = function (name) {
      if (name === 'keyA') {  /*...dosomething */ }
    }
    
    // 使用接口配合函数的默认参数,可实现特定重载签名的效果:
    let fncB: fncInter = function (name = 'keyA', age: number) {
      if (age >= 25) {  /*...dosomething */ }
    }
    let fncC: fncInter = function (name = 'keyB') {  /*...dosomething */ }
    let fncD: fncInter = function (name = 'keyC') {  /*...dosomething */ }
    

    此处其并非严格意义上的重载,但是却利用了重载的思想,并且灵活搭配了函数的默认参数。

  • 7.(重载扩展2)方法重载

    除了常规函数外,类中的方法也可以过载,比如用重载方法greet()来实现一个类:

    class Greeter {
      message: string;
    
      constructor(message: string) {
        this.message = message;
      }
    
      greet(person: string): string;
      greet(persons: string[]): string[];
    
      greet(person: unknown): unknown {
        if (typeof person === 'string') {
          return `${this.message}, ${person}!`;
        } else if (Array.isArray(person)) {
          return person.map(name => `${this.message}, ${name}!`);
        }
        throw new Error('error');
      }
    }
    
    const hi = new Greeter('Hi');
    
    hi.greet('World');          // 'Hello, World!'
    hi.greet(['TS', 'JS']);     // ['Hello, TS!', 'Hello, JS!']
    

7.联合类型|交叉类型|类型断言

1.联合类型(Union Types)

表示取值可以为多种类型中的一种,多种类型使用|分隔开。

let value: string | number
value = 123
value = '123'

当我们使用联合类型的时候,因为TypeScript不确定到底是哪一个类型,所以我们只能访问此联合类型的所有类型公用的属性和方法

// 会编译报错
function getLength (value: string | number): number {
  return value.length
}

// 以下代码不会编译报错
function valueToStr (value: string | number): string {
  return value.toString()
}

另外一个值得注意的地方就是,当联合类型被赋值后,TypeScript会根据类型推断来确定变量的类型,一旦确定后,则此变量只能使用这种类型的属性和方法。

let tsValue: string | number
tsValue = '123'
console.log(tsValue.length) // 编译正确
tsValue = 123
console.log(tsValue.length) // 编译报错

2.交叉类型(Intersection Types)

交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性,使用&定义交叉类型。

type Useless = string & number;
// 很显然,没有任何类型同时满足多种原始类型,Useless实际是个`never`

交叉类型的用武之地是将多个接口类型合并成一个类型。从而实现等同与接口继承的效果

[注意]

合并的多个接口类型存在同名属性时会不兼容

type a = { id: number; name: string; }
type b = { age: number; name: number; }

type concatType = a & b
const mixedConflict: concatType = {
  id: 1,
  name: 'Jack', // Type 'string' is not assignable to type 'never'.
  age: 2
};

若存在相同成员,且成员类型为非基本数据类型时,可合并成功

interface A {
  x: { d: true },
}
interface B {
  x: { e: string },
}
interface C {
  x: { f: number },
  // x: {e: number, f: number }, // 这时,下面 e: '' 就会报错
}
type ABC = A & B & C
let abc: ABC = {
  x: {
    d: true,
    e: '',
    f: 666
  }
}

3.类型断言(Type Assertion)

  • 语法

    // as 语法   推荐
    let someValue: any = "this is a string";
    let strLength: number = (someValue as string).length;
    
    // 尖括号 语法    不推荐
    let someValue1: any = "this is a string";
    let strLength1: number = (<string>someValue1).length;
    

    尖括号格式会与 reactJSX 产生语法冲突

    interface A {
      run: string
    }
    
    interface B {
      build: string
    }
    
    const fn = (type: A | B): string => {
      // return type.run   // 这样写会警告:应为B的接口上面是没有定义run这个属性的
      
      return (type as A).run  // 可以使用类型断言来推断他传入的是A接口的值
    }
    

    注意类型断言只能够「欺骗」TypeScript 编译器,无法避免运行时的错误,反而滥用类型断言可能会导致运行时错误

  • 非空断言

    在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符 ! 可以用于断言操作对象是非 null 和非 undefined 类型

    let a: null | undefined | string;
    // a = 'aaaa'
    a!.toString();  // ok
    a.toString();   // 'a' is possibly 'null' or 'undefined'.
    
    // 扩展:使用(ES2020特性)链判断运算符(?.)
    a?.toString();  // ok
    
    type NumTy = () => number;
    
    function myFunc(num: NumTy | undefined) {
      const num1 = num();   // Error
      const num2 = num!();  //OK
    }
    
  • 确定赋值断言

    在TS中,如果一个变量在声明但未赋值的情况下被使用,则会编译报错。确定赋值断言即是允许在实例属性和变量声明后面放置一个 ! 号,从而告诉 TypeScript 该属性会被明确地赋值

    // 声明变量 但不赋值 使用 确定赋值断言
    let x!: number;
    // 在这个方法里赋值
    initialize();
    // 但编译器不知道会赋值  如果不加 确定赋值断言 ,则会报错
    console.log(2 * x); // Ok
    
    function initialize() { 
      x = 10;
    }
    

8.类型别名type

类型别名用type关键字来给一个类型起一个新的名字,类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
    if (typeof n === 'string') {
        return n;
    }
    else {
        return n();
    }
}

注意:类型别名,诚如其名,即我们仅仅是给类型取了一个新的名字,并不是创建了一个新的类型

typeinterface 的 一些区别

  • 1.interface 可以继承, type 只能通过 & 交叉类型合并

  • 2.type 可以定义 联合类型 和 可以使用一些操作符, interface不行

  • 3.interface 遇到重名的会合并, type 不行

9.字面量类型

TypeScript 中,字面量不仅可以表示值,还可以表示类型,即字面量类型。也就是在声明变量的时候给变量赋值,将值作为一个类型来设置变量的类型。目前支持的字面量类型支持三种:字符串类型、数字类型、布尔类型:

// 字符串字面量类型
let specifiedStr: 'this is string' = 'this is string';
// 数字字面量类型
let specifiedNum: 123 = 123; 
// 布尔字面量类型
let specifiedBoolean: true = true;

字符串字面量类型

实际上单个的字符串字面量类型并不是很有用,但是可以使用联合类型将多个字符串字面量组合成一个类型(形成类似于字符串枚举的作用)。

type CardinalDirection = 'North' | 'East' | 'South' | 'West';
function move(distance: number, direction: CardinalDirection) {
    // ....
}
move(1, 'North')

enum States {
North = 'north',
East = 'east',
South = 'south',
West = 'west'
}
function move1(distance: number, direction: States) {
    // ....
}
move1(1, States.North)

数字/布尔字面量类型

用法与字符串字面量类型相似,都是为了更明确的限制变量的值域,让使用者必须用特定值的数据

interface Config {   
  size: 'small' | 'big';  
  isEnable:  true | false;   
  margin: 0 | 2 | 4;
}

数组和元组

数组

  • 对数组的定义两种:类型[ ]Array<类型>

    let arr: string[] = ["1", "2"];
    let arr1: number[] = [1, 2, 3];
    
    let arr2: Array<string> = ["1", "2"];
    let arr3: Array<number> = [1, 2, 3];
    
    // 多维数组
    let ary:number[][] = [[1,2], [3,4]];
    
  • 定义联合类型

    let arr:(number | string)[] = [1, 'b', 2, 'c']
    
  • 定义指定对象成员的数组

    interface Arrobj {
      name: string,
      age: number
    }
    let arr3: Arrobj[] = [{ name: 'jimmy', age: 22 }]
    

元组(Tuple)

元组类型允许表示一个已知元素的数量和类型的数组,各元素的类型不必相同

理解:一个数组如果知道它确定的长度,且每个位置的值的类型也是确定的,那么就可以把这样的数组称为元组。

// tuple数组只有2个元素,并且第一个元素类型为string,第二个元素类型为number
let tuple: [string, number] = ['AAA', 123]

当赋值或访问一个已知索引的元素时,会得到正确的类型:

let tuple: [string, number] = ['AAA', 123]
console.log(tuple[1]) // 123
console.log(tuple[2]) // 报错
  • 元祖类型的解构赋值

    元组可以看成是数组的变种,一样支持数组的解构赋值

    let tuple: [number, string, number] = [12, 'AAA', 123]
    
    let [a, b, c] = tuple
    
    console.log(a); // 12
    console.log(b); // AAA
    
    // 如果解构的个数超出元组的个数,会报错
    
  • 可选元素

    let tuple: [number, string, number?, boolean?] = [12, 'AAA', , true]
    
    let [a, b, c, d] = tuple
    
    console.log(a, b, c, d);  // 12 'AAA' undefined true
    
  • 剩余元素

    let tuple: [number, ...string[]] = [12, 'AAA', 'BBB', 'CCC']
    let [a, b, c, d] = tuple
    console.log(a, b, c, d);  // 12 'AAA' 'BBB' 'CCC'
    
  • 只读(readonly)

    const nums:readonly [number, number] = [20, 30]
    nums[0] = 10  // 无法修改
    
    num = ['a', 'b']  // 无法修改
    
    /**
     * const ↑
     *
     * let ↓
     */
    let nums1:readonly [number, number] = [20, 30]
    nums1[0] = 10  // 无法修改
    
    nums1 = [100, 200]  // 可以修改
    

枚举类型enum

枚举 Enum 类型用来表示取值限定在指定的范围,例如一周只能有七天,一年只有十二个月,颜色只能有红、绿、蓝等。

enum colors  {
  red,
  green,
  blue
}
console.log(colors.red)   // 0
console.log(colors.green) // 1
console.log(colors.blue)  // 2

如果不指定值,默认是从 0 开始的。而且不仅可以正向的获取值,还可以通过值反向获取枚举:

enum colors  {
  red = 10,
  green,
  blue
}
console.log(colors.red)   // 10
console.log(colors.green) // 11
console.log(colors.blue)  // 12

// 反向获取枚举
console.log(colors[10])   // red
console.log(colors[11])   // green
console.log(colors[12])   // blue

类(class)

ES6引入了 Class(类),作为对象的模板。通过 class 关键字,可以定义类。在 TS 中使用:

class Person {
  name: string
  // age: number   // 在TypeScript是不允许直接在constructor 定义变量的 需要在constructor上面先声明

  constructor(name: string) {
    this.name = name
  }
  sayHello() {
    console.log(`hello, ${this.name}`)
  }
}

TS类的访问修饰符

  • public:公有的,在任何地方都可以访问到。
  • private:受保护的,只能在类的内部及其类的子类内部使用。
  • protected:私有的,只能在类的内部进行使用。
class Person {
  public name: string
  private age: number
  protected address: string

  constructor(name: string, age: number, address: string) {
    this.name = name
    this.age = age
    this.address = address
  }
  sayHello() {
    console.log(`hello, ${this.name}-${this.age}-${this.address}`)
  }
}

class Teacher extends Person {
  sayTeacher() {
    // 调用父类的方法
    super.sayHello()
    console.log(this.name);
    console.log(this.age);  // 编译报错
    console.log(this.address);
  }
}

let t1 = new Teacher('Jack', 20, '翻斗大街翻斗花园二号楼1001室')
t1.sayTeacher()
console.log(t1.name)    // why
console.log(t1.age)     // 编译报错
console.log(t1.address) // 编译报错

static 静态属性和静态方法

static 定义的属性和方法,不可以通过 this 去访问,只能通过类名去调用

class Person {
  public name: string
  static sex:string = 'Man'

  constructor(name:string){
    this.name = name
    //  this.sex // static 定义的属性 不可以通过this 去访问 只能通过类名去调用
  }

  public static fncA(){
    return 'fncAAAAAAA...'
  }

  static fncB(){
    console.log(this.fncA(), 'fncA');
    console.log(this.sex, 'sex');
  }
}
// let person = new Person('jack')
// person.sex    // 编译报错:属性‘sex’在类型‘Person’上不存在,你的意思是改为访问静态成员“Person.sex”?
// person.fncA()    // 编译报错
console.log(Person.sex);  // Man
console.log(Person.fncA());  // fncAAAAAAA...
Person.fncB()

注意:如果两个函数都是 static 静态的是可以通过 this 互相调用。

抽象类(abstract

TypeScript中,可以使用 abstract 关键字来定义抽象类以及抽象类中的抽象方法,在使用抽象类的过程中,有几点需要注意:

  • 抽象类不能被实例化,只能被继承。
  • 抽象类中的抽象方法必须被子类实现。
abstract class Animal {
  name: string
  constructor (name: string) {
    this.name = name
  }
}
const animal = new Animal() // 编译报错

class Person extends Animal{}
const person = new Person('why')
console.log(person.name)    // why

上面代码报错抽象类无法被实例化。抽象类中的抽象方法必须被子类实现:

abstract class Animal {
  name: string
  constructor(name: string) {
    this.name = name
  }
  abstract getName(): string
}

class Person extends Animal {
  // 子类必须实现抽象类中的抽象方法
  getName(): string {
    return this.name
  }
}
const person = new Person('张三')
console.log(person.getName())

类的继承(extends

通过 extends 关键字来实现子类继承父类,子类也可以通过 super 关键字来访问父类的属性或者方法。

class Person {
  name: string
  age: number

  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }
  sayHello() {
    console.log(`hello, ${this.name}-${this.age}`)
  }
}

class Teacher extends Person {
  constructor(name: string, age: number){
    // 调用父类的构造函数
    super(name, age)
  }
  sayTeacher(){
    // 调用父类的方法
    return super.sayHello()
  }
}

let t1 = new Teacher('Jack', 20)
t1.sayHello()   // hello, Jack-20
t1.sayTeacher() // hello, Jack-20

关于类属性的简写方式,就是在类的构造函数中指明访问修饰符

// 简写形式
class Person {
  constructor (public name: string) {}
}
// 等价于
class Person {
  name: string
  constructor (name: string) {
    this.name = name
  }
}

implements / extends

  • implements:实现,从 父类或者接口 实现所有的属性和方法,同时可以重写属性和方法,包含一些新功能

  • extends:继承,一个新的接口或者类,从父类或者接口继承所有的属性和方法,不可以重写属性,但可以重写方法

接口可以继承接口或类,类只能继承类

interface 定义接口,也可以理解成类定义接口。使用关键词 implements,后面跟 interface 的名字(多个用逗号隔开), 继承还是用 extends

interface P1 {
  get(type: boolean): boolean
}

interface P2 {
  set(): void,
  asd: string
}

class A {
  name: string
  constructor() {
    this.name = "123"
  }
}

class Person extends A implements P1, P2 {
  asd: string

  constructor() {
    super()   // 访问派生类的构造函数中的this前,必须调用super
    this.asd = '123'
  }

  get(type: boolean) {
    return type
  }
  set() {}
}
  • 接口继承接口
interface P3 extends P1{
  handleOn(): string
}
// P3 继承了 P1 的属性和方法
  • 类实现接口 (多个接口时用逗号隔开)

    // P1,P2同上例
    class TestA implements P1, P2 {
      name: string
      asd: string
      constructor(asd: string) {
        this.name = "123"
        this.asd = asd
      }
    
      get(type: boolean) {
        return type
      }
      set() { }
    }
    

    **疑问:**定义了接口,为何在继承这个接口的类中还要写接口的实现方法?不如直接就在这个类中写实现方法岂不是更便捷,还省去了定义接口。

    解答:规范

    • 在代码设计中,接口是一种规范;接口通常用于来定义某种规范, 类似于你必须遵守的协议,
    • 站在程序角度上说接口只规定了类里必须提供的属性和方法,从而分离了规范和实现,增强了系统的可拓展性和可维护性;
  • 接口继承类

    class Point {
        x: number;
        y: number;
        constructor(x: number, y: number) {
            this.x = x;
            this.y = y;
        }
    }
    
    interface Point3d extends Point {
        z: number;
    }
    
    let point3d: Point3d = {x: 1, y: 2, z: 3};
    

泛型(generics

泛型 和 any

当我们定义一个变量不确定类型的时候有两种解决方式:

  • any

    使用 any 定义时存在的问题:虽然 以 知道传入值的类型但是无法获取函数返回值的类型;另外也失去了ts类型保护的优势

  • 泛型

    泛型指的是在定义函数/接口/类型时,不预先指定具体的类型,而是在使用的时候在指定类型限制的一种特性。

在函数中使用泛型

function str<T>(a: T, b: T):string{
  return `${a}----${b}`
}
console.log(str( '20', 'str')); // 正确
console.log(str( 20, 'str'));   // 编译报错,后面的参数类型要与第一个参数的类型保持一致

// let str = <T>(a: T, b: T): string => {
//   return `${a}----${b}`
// }

泛型可以是多个的

function str<T, P>(a: T, b: P):string{
  return `${a}----${b}`
}
console.log(str( '20', 'str'));
console.log(str( 21, 'str'));

在接口中使用泛型

interface IReturnItemFn<T> {
  (para: T): T
}
// 当要传入一个number作为参数的时候,就可以这样声明函数:
const returnItem: IReturnItemFn<number> = para => para

其它示例

// 注意,这里写法是定义的方法
interface Search {
  <T, Y>(name: T, age: Y): T
}

let fn: Search = function <T, Y>(name: T, id: Y): T {
  console.log(name, id)
  return name;
}

fn('li', 11); //编译器会自动识别传入的参数,将传入的参数的类型认为是泛型指定的类型


// 示例二
interface fnInter<T> {
  (arg: T): T
}
function fn<T>(arg: T): T {
  return arg
}
let result: fnInter<number> = fn
result(123)


// 示例三
interface CreateArray {
  <T>(length: number, value: T): T[]
}
let createArrayFunc: CreateArray = function (length, value) {
  let result = []
  for (let index = 0; index < length; index++) {
    result[index] = value
  }
  return result
}
console.log(createArrayFunc(3, 'AAA'))  // ['AAA', 'AAA', 'AAA']
console.log(createArrayFunc(2, true))   // [true, true]

在类中使用泛型

class Animal<T> {
 name:T;
 constructor(name: T){
  this.name = name;
 }
 action<T>(say:T) {
   console.log(say)
 }
}
let cat = new Animal('cat');

cat.action('mimi')  // mimi

泛型约束

比如期望在一个泛型上获取 length 参数,但有的类型没有 length 属性,就会报错

function getLegnth<T>(arg:T) T {
  return arg.length   // 编译报错,类型“T”上不存在属性“length”
}

于是,我们就得对使用的泛型进行约束,我们约束其为具有length属性的类型,这里我们会用到interface,代码如下

interface Len {
  length: number
}

function getLegnth<T extends Len>(arg: T) {
  console.log(arg, arg.length)  // 123 3
  return arg.length
}

getLegnth<string>('123')

常见使用场景:

interface Person {
  name: string;
  age: number;
}
function student<T extends Person>(arg: T): T {
  return arg;
}

student({ name: 'lili' });  // 类型 "{ name: string; }" 中缺少属性 "age",但类型 "Person" 中需要该属性
student({ name: "lili", age: '11' }); // 不能将类型“string”分配给类型“number”
student({ name: "lili", age: 11 });

使用 keyof 约束对象

其中使用了TS泛型和泛型约束。首页T类型继承(extends)对象类型的子类型,然后使用 keyof 操作符获取 T 类型的所有键,它的返回类型时联合类型,最后利用 extends约束 K 类型必须作为 keyof T 联合类型的子类型。

function prop<T, K extends keyof T>(obj: T, key: K) {
  console.log(obj, key, obj[key]);
  return obj[key]
}

let o = { a: 1, b: 2, c: 3 }
prop(o, 'a')
prop(o, 'd') //此时就会报错发现找不到

关键词

in

in 用来遍历枚举类型:

type Keys = "a" | "b" | "c"

type Obj =  {
  [p in Keys]: any
} // -> { a: any, b: any, c: any }

infer

在条件类型语句中,可以用 infer 声明一个类型变量并且对它进行使用。

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

以上代码中 infer R 就是声明一个变量来承载传入函数签名的返回值类型,简单说就是用它取到函数返回值的类型方便之后使用。

typeof

JS 提供了 typeof,用来获取基本数据的类型。

TS提供的typeof 的主要用途是在类型上下文中获取变量或者属性的类型

interface Person {
  name: string;
  age: number;
}
const sem: Person = { name: "semlinker", age: 30 };

// 通过 typeof 操作符获取 sem 变量的类型并赋值给 Sem 类型变量
type Sem = typeof sem; // type Sem = Person

const lolo: Sem = { name: "lolo", age: 5 }

/**-------隔断--------*/
let p = {  name: 'asasa',  age: 10}
function p1(parmas: typeof p) {  // 它会去解析p。 然后变成 parmas : { name:string, age:number}
  console.log(p.age, p.name)
}
p1(p)
  • 处理枚举类型使用

    处理枚举类型时,单独使用没有多大的实际用途,一般还会搭配 keyof 操作符

    enum HttpMethod {
      Get,
      Post
    }
    
    const method: typeof HttpMethod = {
      Get: 0,
      Post: 1,
    }
    
    type Method = keyof typeof HttpMethod; // "Get" | "Post"
    
  • 处理函数时使用

    用来获取函数对象的类型,在获取对应的函数类型之后,你可以继续利用 TypeScript 内置的 ReturnType 、Parameters 等工具类型来分别获取函数的返回值类型和参数类型。

    function add(a: number, b: number) {
      return a + b;
    }
    
    type AddType = typeof add;  // (a: number, b: number) => number
    type AddReturnType = ReturnType<AddType> // number
    type AddParamsType = Parameters<AddType> // [a: number, b: number]
    
  • 处理 Class

    class Point {
      x: number;
      y: number;
      constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
      }
    }
    
    // new (x: number, y: number) => Point
    function createPoint(Constructor: typeof Point, x: number, y: number) {
      return new Constructor(x, y);
    }
    
    • 在上示代码中,createPoint 是一个工厂函数,用于快速创建 Point 类的实例。通过 typeof 操作符,你就可以获取 Point 类对应的构造签名,从而实现相应的类型校验。

    • 在定义 Constructor 参数类型时,如果不使用 typeof 操作符的话,将会出现错误信息:类型“point”没有构造签名

  • const断言

    如果想要获取更精确的类型,需要使用const断言

    let requestMethod = "Get";
    let requestMethod2 = "Get" as const;
    
    type R0 = typeof requestMethod; // string
    type R1 = typeof requestMethod2; // "Get"
    
    let user = {
      id: 666,
      name: "阿宝哥",
    };
    
    let user2 = {
      id: 666,
      name: "阿宝哥",
    } as const;
    
    
    type U0 = typeof user;  // { id: number; name: string; }
    
    type U1 = typeof user2; // type U1 = { readonly id: 666; readonly name: "阿宝哥"; }
    

keyof索引查询

用于获取某种类型的所有键,其返回类型是联合类型。

interface Eg1 {
  name: string,
  readonly age: number,
}

type T1 = keyof Eg1 // T1的类型实则是name | age

// let art1: T1 = 'name'    // OK
// let art2: T1 = 'age2'    // 编译报错, 只能取 name | age


class Eg2 {
  private name: string;
  public readonly age: number;
  protected home: string;
  constructor(name:string, age:number, home: string){
    this.name = name
    this.age = age
    this.home = home
  }
}
// T2实则被约束为 age
// 而 name 和 home 不是公有属性,所以不能被 keyof 获取到
type T2 = keyof Eg2

// let atr1: T2 = 'name'   // 编译报错
// let atr2: T2 = 'age'    // OK
// let atr3: T2 = 'home'   // 编译报错

keyof实际应用

interface Person {
  id: number;
  name: string;
  age: number;
};
  • 获取对象所有属性的类型

    type P2 = Person[keyof Person];  // number | string
    
    • Person['key'] 是查询类型(Lookup Types), 可以获取到对应属性类型的类型;
    • Person[keyof Person]本质上是执行 Person['id' | 'name' | 'age']
    • 由于联合类型具有分布式的特性,Person['id' | 'name' | 'age'] 变成了 Person['id'] | Person['name'] | Person['age']
    • 最后得到的结果就是 number | string
  • 约束泛型参数范围

    type MyPick<T, K extends keyof T> = { [P in K]: T[P] };
    
    type P3 = MyPick<Person, 'id' | 'age'>
    
    • K extends keyof TK 进行了约束,只能是'id','name','age'中的一个类型或者几个类型组成的联合类型;
    • 如果没有这个约束,{ [P in K]: T[P] } 则会报错。
  • 和映射类型组合实现某些功能

    • 给对象类型的所有属性加上readonly修饰符

      type MyReadonly<T> = { readonly [P in keyof T]: T[P] };
      type P4 = MyReadonly<Person>;  // { readonly id: number; readonly name: string; readonly age: number; }
      
      • [P in keyof T]是对所有属性的键值类型进行遍历,案例中得到的 P 分别是'id','name'和'age';
      • T[P] 是查询类型,由上面可知Person['id'] 的结果是 numberPerson['name']stringPerson['age']number
      • 将每个属性类型添加readonly修饰符。
    • 去掉对象类型的某些属性

      type MyOmit<T, K> = { [P in keyof T as P extends K ? never : P]: T[P] };
      type P5 = MyOmit<Person, 'id' | 'name'>   // {age: number;}
      
      • as P extends K ? never : P这部分代码叫做重映射,因为我们不一定需要的是P,有些情况下需要对P进行一些转换;
      • 案例中K 中包含的P键值类型则通过never忽略了,相反则保留。所以最后的结果是{age: number}
    • 给对象类型添加新的属性

      type AppendToObject<T, U extends keyof any, V> = {
        [P in keyof T | U]: P extends keyof T ? T[P] : V
      }
      type P6 = AppendToObject<Person, 'address', string> // { address: string; id: number; name: string; age: number; }
      
  • 和条件类型组合实现功能

    两个对象类型合并成一个新的类型

    type Skill = {
      run: () => void;
    }
    type Merge<F extends Record<string, any>, S extends Record<string, any>> = {
      [P in keyof F | keyof S]: P extends keyof S ? S[P] : P extends keyof F ? F[P] : never;
    };
    
    type P7 = Merge<Person, Skill>; // { id: number; name: string; age: number; run: () => void; }
    
    • 代码中P extends keyof S ? X : Y 的部分叫做 条件类型
    • 代码中的含义就是如果 PF 的属性类型,则取 F[P],如果 PS 的属性类型,则取S[P]

extends

有时候我们定义的泛型不想过于灵活或者说想继承某些类等,可以通过 extends 关键字添加泛型约束

interface LhInter {
  length: number;
}

function argLh<T extends LhInter>(arg: T): T {
  console.log(arg.length);
  return arg;
}

argLh(3);  // Error
argLh({length: 10, value: 3});  // OK

索引类型(Index types)

let person = {
  name: 'musion',
  age: 35
}

function getValues(person: any, keys: string[]) {
  return keys.map(key => person[key])
}

console.log(getValues(person, ['name', 'age'])) // ['musion', 35]
console.log(getValues(person, ['gender'])) // [undefined]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值