关于我 壹文 项目 三五好友
TypeScript基础 及 深入理解高级用法 2024-05-07
文章摘要

TypeScript 基础 及 深入理解高级用法

一、基础类型

TypeScript 支持与 JavaScript 几乎相同的数据类型。

注:类型注解

作用:相当于强类型语言(GO、C++等)中的类型声明

语法:(变量/函数): type

1.JavaScript 已有的数据类型

String(字符串):使用 string 表示文本数据类型。 和 JavaScript 一样,可以使用双引号 " 或单引号 ' 表示字符串, 反引号 ` 来定义多行文本和内嵌表达式

let str: string = 'abc'

Number(数字):和 JavaScript 一样,TypeScript 里的所有数字都是浮点数(不论是整数还是小数,都被视为浮点数来处理)。这些浮点数的类型是 number

let notANumber: number = NaN
let num: number = 123 //普通数字
let infinityNumber: number = Infinity //无穷大
let decimal: number = 6 //十进制
let hexLiteral: number = 0xf00d //十六进制
let binaryLiteral: number = 0b1010 //二进制
let octalLiteral: number = 0o744 //八进制

Boolean(布尔):我们使用 boolean 表示布尔类型,表示逻辑值 true / false

let bool: boolean = true

Object(Array、Function)(数组部分):TypeScript 有两种定义数组的方式:第一种,可以在元素类型后加上 []。 第二种,可以使用数组泛型 Array<元素类型>。 此外,在元素类型中可以使用联合类型。 符号 | 表示或

let arr1: number[] = [1, 2, 3]
let arr2: Array<number> = [1, 2, 3]
let arr3: Array<number | string> = [1, 2, 3, 'a']

Object(Array、Function)(对象部分):TypeScript 有两种定义对象的方式。 第一种,可以在元素后加上 object。 第二种,可以使用 { key: 元素类型 } 形式。 同样在元素类型中可以使用联合类型。注意第一种形式对象元素为只读。

let obj1: object = { x: 1, y: 2 }
obj1.x = 3 // Error: Property 'x' does not exist on type 'object'.

let obj2: {  x: number, y: number } = { x: 1, y: 2 }
obj2.x = 3

Object(Array、Function)(函数部分):(key1:参数类型 1,key2:参数类型 2):函数返回值类型

let plus: (x: number, y: number) => number

plus = (a, b) => a + b

plus(2, 2) // 2

还可以用接口来定义它

interface Add {
  (x: number, y: number): number
}

let add: Add = (a, b) => a + b

Symbol(独一无二的值)

undefinedundefined 表示未定义的值

nullnull 表示对象值缺失

2.TypeScript 新增的数据类型

void(表示函数不返回任何值)

function logMessage(message: string): void {
  console.log(message);
}

any(任意类型,可以绕过 TypeScript 的类型检查,适用于那些类型不确定或需要兼容多种类型的场景)

let value: any = "hello";
value = 42; // 后续可以赋予任何类型的值

never(表示永远不会出现的类型。它用于函数的返回类型,表明该函数永远不可能正常结束(总是抛出异常或无限循环))never类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是never的子类型或可以赋值给never类型(除了never本身之外)。 即使any也不可以赋值给never

function throwError(message: string): never {
  throw new Error(message);
}

元组(表示一个已知元素数量和类型的数组,每个元素的类型可以不同,对应位置的类型必须相同)

let person: [string, number];
person = ["Alice", 30]; // 正确

枚举(为一组命名的常量提供便捷的语法。枚举可以为一组相关的值提供易于记忆的名字,增加代码的可读性,枚举成员值只读,不可修改。)

enum Color {Red, Green, Blue}
let favoriteColor: Color = Color.Green;
  • 数字枚举:初始值为 0, 逐步递增,也可以自定义初始值,之后根据初始值逐步递增

    enum Role {
      Reporter = 1,
      Developer,
      Maintainer,
      Owner,
      Guest
    }
    
    console.log(Role.Developer) // 2
    console.log(Role[2]) // Developer
    
  • 字符串枚举

    enum Message {
      Success = '成功',
      Fail = '失败'
    }
    

高级类型:

  • 类型别名:为类型创建一个新的名字,提高代码的可读性

    type StringOrNumber = string | number;
    
  • 联合类型:表示一个值可以是几种类型之一

    function combine(a: number | string, b: number | string) {
      // ...
    }
    
  • 交叉类型:表示一个值同时满足多种类型

    interface Developer {
      name: string;
      skills: string[];
    }
    
    interface Person {
      age: number;
    }
    
    type FullStackDeveloper = Developer & Person;
    
  • 映射类型:根据现有类型创建新类型,常用于创建像 Partial、Readonly 这样的实用类型

    type Readonly<T> = {
      readonly [P in keyof T]: T[P];
    };
    

二、接口

在 TypeScript 中,我们可以使用接口 interface 来定义对象类型

interface Person {
  name: string
  age: number
}

let man: Person = {
  name: 'James',
  age: 30
}

我们定义了一个接口 Person 和变量 man,变量的类型是 Person。 这样我们就约束了该变量的值中对象的 keyvalue 要和接口一致。

需要注意的是:

  1. 接口规范首字母大写
  2. 被赋值的变量必须和接口的定义保持一致,参数不能多也不能少
  3. 类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型正确即可

1.可选属性

interface Person {
  name: string
  age?: number
}

let man: Person = {
  name: 'James'
}

2.索引签名

[propName: string]: any

它用于定义对象类型,允许对象的属性名是字符串类型,且对应的值可以是任意类型(any)。这种写法常用于表示一个对象可以接受任意数量的、键为字符串、值为任意类型的属性

interface Person {
  name: string
  age: number
  [x: string]: any
}

let man: Person = {
  name: 'James',
  age: 30,
  height: '180cm'
}

除了 nameage 必须一致以外,其他属性可以随意定义数量不限

3.只读属性

属性名前使用 readonly 关键字制定为只读属性,初始化后不可更改

interface Person {
  readonly name: string
  age: number
}

let man: Person = {
  name: 'James',
  age: 30
}

man.name = 'Tom' // Error: Cannot assign to 'name' because it is a read-only property.

三、函数

TypeScript 函数可以创建有名字的函数或匿名函数,四种定义函数的写法:

function add (x: number, y: number) {
  return x + y
}

const add: (x: number, y: number) => number

type add = (x: number, y: number) => number

interface add {
  (x: number, y: number) => number
}

TypeScript 里的每个函数参数都是必要的。这里不是指不能把 nullundefined 当做参数

1.可选参数

function add (x: number, y: number, z?: number) {
  return x + y
}

add(1, 2)

注意:可选参数必须在必选参数之后

2.默认参数

与 JavaScript 相同,在 TypeScript 里函数参数同样可以设置默认值,用法一致

function add (x: number, y = 2) {
  return x + y
}

3.剩余参数

与 JavaScript 相同。TypeScript 可以把所有参数收集到一个变量里

function add (x: number, ...rest: number[]) {
  return x + rest.reduce((prev, curr) => prev + curr)
}

add(1, 2, 3) // 6

注意:剩余参数必须在必选参数之后,可选参数不允许和剩余参数共同出现在一个函数内

4.函数兼容性

// 固定参数
let a = (p1: number, p2: number) => {}
// 可选参数
let b = (p1?: number, p2?: number) => {}
// 剩余参数
let c = (...args: number[]) => {}

a = b
a = c
b = a // Error
b = c // Error
c = a
c = b

固定参数兼容可选参数和剩余参数。可选参数不兼容固定参数和剩余参数

四、类

public、private、protected、readonly

TypeScript 可以使用三种访问修饰符(Access Modifiers),分别是 publicprivateprotected

  • public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public

    class Animal {
      public name: string
      public constructor (name: string) {
        this.name = name
      }
    }
    
    let a = new Animal('Jack')
    console.log(a.name) // Jack
    a.name = 'Tom'
    console.log(a.name) // Tom
    
  • private 修饰的属性或方法是私有的,不能在声明它的类的外部访问,包括继承它的类也不可以访问

    class Animal {
      private name: string
      public constructor (name: string) {
        this.name = name
      }
    }
    
    let a = new Animal('Jack')
    console.log(a.name) // Error: Property 'name' is private and only accessible within class 'Animal'.
    
    class Cat extends Animal {
      constructor (name: string) {
        super(name)
        console.log(this.name) // Error: // Property 'name' is private and only accessible within class 'Animal'.
      }
    }
    
  • protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问

    class Animal {
      protected name: string
      public constructor (name: string) {
        this.name = name
      }
    }
    
    class Cat extends Animal {
      constructor (name: string) {
        super(name)
        console.log(this.name)
      }
    }
    
  • 以上三种可以修饰构造函数,默认为 public,当构造函数为 private 时,该类不允许被继承或实例化;当构造函数为 protected 时,该类只允许被继承。

  • readonly 修饰的属性为只读属性,只允许出现在属性声明或索引签名中。

注意如果 readonly 和其他访问修饰符同时存在的话,需要写在其后面

class Animal {
  // public readonly name: string
  public constructor (public readonly name: string) {
    this.name = name
  }
}

五、接口

接口继承接口

interface Animal {
  name: string
  eat (): void
}

interface Predators extends Animal {
  run (): void
}

class Cat implements Predators {
  constructor (name: string) {
    this.name = name
  }
  name: string
  eat () {}
  run () {}
}

继承多个接口用 , 分割,同理实现多个接口方式相同

interface Animal {
  name: string
  eat (): void
}

interface Lovely {
  cute: number
}

interface Predators extends Animal, Lovely {
  run (): void
}

class Cat implements Predators {
  constructor (name: string, cute: number) {
    this.name = name
    this.cute = cute
  }
  name: string
  cute: number
  eat () {}
  run () {}
}

混合类型

interface SearchFunc {
  (source: string, subString: string): boolean
}

let mySearch: SearchFunc
mySearch = function(source: string, subString: string) {
  return source.search(subString) !== -1
}

一个函数还可以有自己的属性和方法

//接口定义
interface Counter {
  (start: number): string // 定义了作为函数的行为,接受一个number类型的参数,返回string
  interval: number // 定义了一个名为interval的属性,类型为number
  reset(): void // 定义了一个名为reset的方法,无参数,返回void
}

//定义了一个Counter接口,它不仅描述了一个函数类型(接受一个数字参数并返回字符串),还包含了额外的属性(interval)和方法(reset)


//类型断言
let counter = <Counter>function (start: number) {}

//使用<Counter>进行类型断言,告诉TypeScript编译器我们打算将这个匿名函数赋予Counter接口的形状。这意味着该函数除了作为普通函数使用外,还会拥有interval属性和reset方法


//扩展函数功能
counter.interval = 123; // 给函数添加interval属性
counter.reset = function () {}; // 给函数添加reset方法

//通过直接赋值的方式,给counter函数对象添加了interval属性和reset方法,使其符合Counter接口的定义


//使用实例
let c = getCounter();
c(10); // 调用作为函数的功能
c.reset(); // 调用reset方法
c.interval = 5.0; // 修改interval属性

六、泛型

泛型是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。可以把泛型理解为代表类型的参数

缺陷展示:

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

createArray(3, 'x') // ['x', 'x', 'x']

这段代码编译不会报错,但是一个显而易见的缺陷是,它并没有准确的定义返回值的类型,可以传入任意类型:

Array<any> 允许数组的每一项都为任意类型。如果我们想将类型固定住,泛型就派上用场了

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

createArray<string>(3, 'x') // ['x', 'x', 'x']

上例中,我们在函数名后添加了 <T>,其中 T 用来指代任意输入的类型,在后面的输入 value: T 和输出 Array<T> 中即可使用了

valuestring类型的时候,函数返回值也会变成Array<string>类型;而当value为 number 类型的时候,自然函数返回值也会变成Array<number>类型

多个类型参数

定义泛型的时候,可以一次定义多个类型参数

function swap<T, U>(tuple: [T, U]): [U, T] {
  return [tuple[1], tuple[0]]
}

swap([7, 'seven']) // ['seven', 7]

泛型约束

在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型(可能是 string、number 等任意类型),所以不能随意的操作它的属性或方法

function loggingIdentity<T>(arg: T): T {
  console.log(arg.length) // Error: Property 'length' does not exist on type 'T'.
  return arg
}

这时,我们可以对泛型进行约束,只允许这个函数传入那些包含 length 属性的变量

interface Lengthwise {
  length: number
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length)
  return arg
}

上例中,我们使用了 extends 约束了泛型 T 必须符合接口 Lengthwise 的形状,也就是必须包含 length 属性

泛型函数

可以用泛型来约束函数的参数和返回值类型

type Log = <T>(value: T) => T

let log: Log = (value) => {
  console.log(value)
  return value
}

log<number>(2) // 2
log('2') // '2'
log(true) // <boolean>true

泛型接口

之前学习过,可以使用接口的方式来定义一个函数需要符合的形状

interface SearchFunc {
  (source: string, subString: string): boolean
}

let mySearch: SearchFunc
mySearch = function (source: string, subString: string) {
  return source.search(subString) !== -1
}

同样也可以使用含有泛型的接口来定义函数的形状

//定义泛型接口
interface CreateArrayFunc<T> {
  (length: number, value: T): Array<T>
}

//实现泛型接口
let createArray: CreateArrayFunc<string> = function(length: number, value: string): Array<string> {
  let result: string[] = [];
  for (let i = 0; i < length; i++) {
    result[i] = value;
  }
  return result;
}

createArray(3, 'x'); // ['x', 'x', 'x']

七、高级类型

交叉类型联合类型条件类型

1.交叉类型

& 符号,多个类型合并为一个类型,新的类型具有所有类型的特性

interface DogInterface {
  run (): void
}
interface CatInterface {
  jump (): void
}
let pet: DogInterface & CatInterface = {
  run () {},
  jump () {}
}

2.联合类型

取值可以为多种类型中的一种

let a: number | string = 1 // or '1'

字面量联合类型

let a: 'a' | 'b' | 'c'
let b: 1 | 2 | 3

对象联合类型

interface DogInterface {
  run (): void
}
interface CatInterface {
  jump (): void
}
class Dog implements DogInterface {
  run () {}
  eat () {}
}
class Cat implements CatInterface {
  jump () {}
  eat () {}
}
enum Master { Boy, Girl }

//getPet 方法体内的 pet 变量被推断为 Dog 和 Cat 的联合类型。在类型未确定的情况下,只能访问联合类型的公有成员 eat 方法

function getPet (master: Master) {
  let pet = master === Master.Boy ? new Dog() : new Cat()
  pet.eat()
  return pet
}

3.条件类型

形式为 T extends U ? X : Y,如果类型 T 可以赋值为 U 结果就为 X 反之为 Y

type TypeName<T> =
  T extends string ? 'string' :
  T extends number ? 'number' :
  T extends boolean ? 'boolean' :
  T extends undefined ? 'undefined' :
  T extends Function ? 'function' :
  'object'

type T1 = TypeName<string> // type T1 = "string"
type T2 = TypeName<string[]> // type T2 = "object"

深入理解 TypeScript 高级用法篇

一、Typescript 类型系统

1.Typescript 的类型是支持定义 “函数定义” 的

如何在 Typescript 类型系统中定义函数呢?

Typescript 中类型系统中的的函数被称作 泛型操作符,其定义的简单的方式就是使用 type 关键字:

// 这里我们就定义了一个最简单的泛型操作符
type foo<T> = T;

这里的代码如何理解呢,其实这里我把代码转换成大家最熟悉的 Javascript 代码其实就不难理解了

// 把上面的类型代码转换成 `JavaScript` 代码
function foo(T) {
	return T
}

其实类型系统中的函数还支持对入参的约束

// 这里我们就对入参 T 进行了类型约束
type foo<T extends string> = T;

那么把这里的代码转换成我们常见的 Typescript 是什么样子的呢

function foo(T: string) {
	return T
}

当然啦我们也可以给它设置默认值

// 这里我们就对入参 T 增加了默认值
type foo<T extends string = 'hello world'> = T;

那么这里的代码转换成我们常见的 Typescript 就是这样的

function foo(T: string = 'hello world') {
	return T
}

2.Typescript 的类型是支持 “条件判断” 的

这在 Typescript 官方文档被称为 条件类型(Conditional Types),定义的方法也非常简单,就是使用 extends 关键字

T extends U ? X : Y;

这不就是 三元运算符 嘛!

结合上面的 “函数”,我们就可以实现了一个简单的带判断逻辑的函数

type num = 1;
type str = 'hello world';

type IsNumber<N> = N extends number ? 'yes, is a number' : 'no, not a number';

type result1 = IsNumber<num>; // "yes, is a number"
type result2 = IsNumber<str>; // "no, not a number"

3.Typescript 的类型是支持 “作用域” 的

全局作用域

就像常见的编程语言一样,在 Typescript 的类型系统中,也是支持 全局作用域 的。换句话说,你可以在没有 导入 的前提下,在 任意文件任意位置 直接获取到并且使用它。

通常使用 declare 关键字来修饰,例如我们常见的 图片资源 的类型定义:

declare module '*.png';
declare module '*.svg';
declare module '*.jpg';

当然我们也可以在 全局作用域 内声明一个类型

declare type str = string;
declare interface Foo {
  propA: string;
  propB: number;
}

需要注意的是,如何你的模块使用了 export 关键字导出了内容,上述的声明方式可能会失效,如果你依然想要将类型声明到全局,那么你就需要显式地声明到全局:

declare global {
  const ModuleGlobalFoo: string;
}

类型也可以支持闭包

function Foo<T> () {
  return function(param: T) {
    return param;
  }
}

const myFooStr = Foo<string>();
// const myFooStr: (param: string) => string
// 这里触发了闭包,类型依然可以被保留
const myFooNum = Foo<number>();
// const myFooNum: (param: number) => number
// 这里触发了闭包,类型也会保持相互独立,互不干涉
Not-By-AI