TypeScript的学习笔记

先贴上TS的官网:TypeScript: JavaScript With Syntax For Types. (typescriptlang.org)

对TS的认识:

TypeScript(简称TS)是一种由微软开发的开源编程语言,它是JavaScript的超集,添加了静态类型和其他特性。TS被广泛应用于大型项目和复杂应用程序中,它可以提高开发效率、降低维护成本、减少错误和增强代码的可读性和可维护性。

TS的主要作用包括:

  1. 静态类型检查:TS可以在编译时检查代码中的类型错误,减少运行时错误和调试时间。

  2. 代码提示和自动补全:TS可以根据类型信息提供更准确的代码提示和自动补全,提高开发效率。

  3. 更好的可读性和可维护性:TS可以帮助开发人员更好地理解代码,并提供更好的文档和注释。

  4. 更好的模块化:TS提供了更好的模块化支持,可以帮助开发人员更好地组织代码和管理依赖关系。

  5. 更好的语言特性:TS支持ES6及以上的语言特性,并且可以自定义类型和接口,扩展语言的功能。

因此,学习TS可以帮助开发人员更好地理解和编写JavaScript代码,并提高代码的质量和可维护性。同时,TS已经成为了前端开发的一个趋势,掌握TS可以提高就业竞争力。

TS比JS多了哪些类型?

any表示任何类型,可以赋值给任何类型,也可以让任何类型赋值给它。
unknown表示未知类型,可以赋值给任何类型,但不能让任何类型赋值给它。
void表示没有返回值的函数类型。
never表示永远不会返回结果的函数类型,或者总是会抛出异常的函数类型。
enum表示枚举类型。
tuple表示元组类型。
  • 使用any类型的示例
let n: any = 123456
n = 'hello TS'
n = true

解读:上面定义的n变量可以是任何类型的值,在这个示例中,我们可以看到我们将n分别赋值为数字、字符串和布尔值。如果你的TS项目全是any,那就可以称之为anyScript啦

  • 使用unknown类型
function multiply(a: unknown, b: unknown) {
  if (typeof a === "number" && typeof b === "number") {
    return a * b;
  } else {
    return 0;
  }
}

console.log(multiply(2, 3));     // Output: 6
console.log(multiply("2", "3")); // Output: 0

export {}

解读:上面的代码示例只是进行一个类型缩小判断,但关键的点是在unknown类型的值上做任何事情都是不合法的,比如:我想将字符串中的所有字符转换为大写字母。这是不可取的

function processUnknown(arg: unknown) {
  console.log(arg.toUpperCase()); // Error: Object is of type 'unknown'.
}
  • 上面提及到的类型缩小一般是通过判断一个变量是不是某个类型,就可以将其类型缩小为该类型,将一个类型的范围缩小为更具体的子类型
function foo(input: string | number) {
  if (typeof input === 'string') {
    console.log(input.toUpperCase());
  } else {
    console.log(input.toFixed(2));  // 将数字保留两位小数并转换为字符串
  }
}
  • 使用void类型
function logMessage(message: string): void {
  console.log(`Message: ${message}`);
}

解读:将logMessgae这个函数的类型声明为void,它这里的作用只是打印,并不返回任何值

  • 使用never类型
  1. 解析歌词的工具

  2. 封装框架/工具库的时候可以使用一下never

实际开发中只有在进行类型推导时,可能会自动推导出来是never类型,但是很少使用它

  • 使用enum(枚举)类型
enum Direction {
  Up,
  Down,
  Left,
  Right
}

function move(direction: Direction) {
  switch (direction) {
    case Direction.Up:
      console.log('Moving Up')
      break
    case Direction.Down:
      console.log('Moving Down')
      break
    case Direction.Left:
      console.log('Moving Left')
      break
    case Direction.Right:
      console.log('Moving Right')
      break
    default: 
      console.log('none')
  }
}

move(Direction.Up)        // 打印出Moving Up
move(Direction.Down)      // 打印出Moving Down
move(Direction.Left)      // 打印出Moving Left
move(Direction.Right)     // 打印出Moving Right

解读:上面的代码示例像不像在开发游戏呢哈哈哈,小游戏的上下左右就可以利用枚举去实现;先定义了一个枚举类型 Direction,它包含了四个成员:Up、Down、Left 和 Right。我们还定义了一个函数 move,它接受一个 Direction 类型的参数 direction,并根据 direction 的值输出不同的结果。在调用 move 函数时,我们可以传入 Direction 中的任意一个成员作为参数,例如 Direction.Up、Direction.Down 等。

  • 使用tuple(元组)类型
let myTuple: [string, number] = ['hello', 123];
console.log(myTuple[0]); // 输出 'hello'
console.log(myTuple[1]); // 输出 123

解读:在上面的示例中,定义了一个名为myTuple的tuple类型变量,它包含一个字符串和一个数字。可以使用索引访问它们,如myTuple[0]和myTuple[1]

注意:必须按照t定义的顺序添加元素到uple中,否则会导致类型错误。

一个常见的使用tuple类型的案例是将多个不同类型的值作为一个元组返回,然后根据元组的结构进行解构和使用。

例如,将一个字符串解析为数值和字符串两个值,并将它们作为元组返回。代码示例如下:

function parseValue(str: string): [number, string] {
  const num = parseInt(str, 10);
  const strVal = str.slice(num.toString().length);
  return [num, strVal];
}

const [numberValue, stringValue] = parseValue('42foo');
console.log(numberValue); // 42
console.log(stringValue); // 'foo'

解读:先定义了一个parseValue函数,它接受一个字符串作为输入,并将其解析为一个数值和一个字符串。使用parseInt函数将字符串中的数字部分解析为一个数值,并使用slice方法获取剩余的字符串部分。然后,将数值和字符串组成一个元组,并将其作为函数的返回值。在调用parseValue函数时,将其返回值解构为numberValuestringValue两个变量,并分别输出它们的值。这个例子展示了如何使用tuple类型来组合多个不同类型的值,并在需要时进行解构和使用。

parseInt(str, 10) 是将字符串转换成整数的函数,其中 str 表示要转换的字符串,10 表示使用十进制数进行转换。

  • 联合类型是什么?

其实就是用符号  |  将不同的类型组合到一起,表示某一变量可以是其中的任意一种类型

let value: string | number
value = "hello"
value = 123
  • 类型别名 type

type 是 TypeScript 中用来定义类型别名的关键字。类型别名可以用来给一个类型取一个新的名字,方便后续使用。

type User = {
  name: string
  age: number
}

function getUserInfo(user: User) {
  console.log(user.name + ' is ' + user.age + ' years old.')
}
  • 接口 interface

在TypeScript中,接口是用来定义对象的形状或结构的。接口可以定义对象的属性、方法、函数等,但它本身不实现任何功能,只是用来描述对象的类型。

interface Person {
  name: string
  age: number
  gender?: string     // ?符号表示可选
  foo: () => void     // 这是函数定义类型的写法  () =>
}

let person: Person = {
  name: 'John',
  age: 34,
  gender: 'male',
  foo() {
    console.log('hi , i am John')
  }
}

person.foo()

注意:上面的代码中,并没有实现Person接口的定义,只是用来描述对象的类型。如果定义的对象不符合接口的形状,编译器会给出错误提示。

你是否发现 type interface 都可以用来定义对象类型或函数类型,但其实他们是有区别的:

  1. Type 可以用来定义任何类型,包括基本类型、联合类型、元组和对象类型,而 Interface 只能用来定义对象类型。

  2. Interface 可以被扩展或继承,而 Type 不行。

  3. Interface 可以用来描述类的实例和类的构造函数,而 Type 不行。

总之,一般情况下,如果是非对象类型的定义使用type,如果是对象类型的定义使用interface

  • 交叉类型

交叉类型是 TypeScript 中的一种类型,它表示同时拥有多个类型的对象。使用交叉类型可以将多个类型合并为一个新的类型,这个新的类型包含了所有原始类型的成员。交叉类型使用符号“&”进行连接。

type Person = {
  name: string
  age: number
}

type Teacher= {
  company: string
  salary: number
}

type PersonAndTeacher = Person & Teacher

const john: PersonAndTeacher = {
  name: 'John',
  age: 30,
  company: 'ABC',
  salary: 5000,
}

使用交叉类型可以方便地组合多个类型的成员,使得代码更加简洁易懂。

  • 类型断言as和尖括号<>
  • 类型断言 as 用于将一个类型强制转换成另一个类型。
interface Person {
  name: string
  age: number
}

const person: Person = {
  name: '张三',
  age: 30
};

// 将 person 转换成 any 类型
const anyValue: any = person as any

// 将 anyValue 转换回 Person 类型
const personAgain: Person = anyValue as Person
  • 类型断言还有其他作用: 
const obj: any = { name: '张三' };

// as
const name1 = (obj as { name: string }).name;

// 尖括号
const name2 = (<{ name: string }>obj).name;

console.log(name1); // 张三
console.log(name2); // 张三

 解读:在上面的例子中,有一个 any 类型的对象 obj,它有一个属性 name。需求是想要取出这个属性的值并把它赋给一个变量。

使用as的语法,在取出属性值的时候显示地指定这个对象的类型,这样就可以直接访问name属性

使用尖括号的语法,将类型断言写在变量名前面,这样也能直接访问name属性。

  • 非空类型断言(慎重使用)

介绍:非空类型断言是一种在TS中使用的特殊语法,它用于告诉编译器某个值一定不会为null或undefined,从而避免编译器在编译时出现类型错误。在TS中,如果我们声明一个变量但没有初始化或赋值,则默认该变量的类型为any。此时如果我们直接使用该变量,编译器会提示变量可能为null或undefined,因为它的类型是any。这时候我们可以使用非空类型断言来告诉编译器该变量一定不会为null或undefined,从而消除编译器的警告。

非空类型断言有两种写法:

1.在变量名后面加上!号

let str: string | null = null;
let length: number = str!.length; // 使用!号告诉编译器str一定不为null

2.使用as关键字

let str: string | null = null;
let length: number = (str as string).length; // 使用as关键字告诉编译器str一定是string类型

需要注意的是,非空类型断言虽然可以避免编译器在编译时出现类型错误,但是如果实际运行时该变量的值为null或undefined,则会导致运行时错误。因此,使用非空类型断言时需要确保该变量的值不会为null或undefined。

  • 操作符 in

在 TypeScript 中,in 是一种操作符用于检查对象中是否存在某个属性。它可以用于 if 语句、循环和类型保护。例如:

interface Person {
  name: string
  age: number
}

const person: Person = {
  name: '张三',
  age: 18
}

if ('name' in person) {
  console.log(person.name)       // '张三'
}

for (const key in person) {
  console.log(key, person[key])  // 'name', '张三'   'age', 18 
}
  • 使用函数类型
type MathFunction = (a: number, b: number) => number

const add: MathFunction = (a, b) => a + b

const multiply: MathFunction = (a, b) => a * b

console.log(add(1, 2))

console.log(multiply(3, 4))

通过使用函数类型,我们可以在 TypeScript 中更加精确地定义函数的类型,从而提高代码的可读性和可维护性。

  • 调用签名和构造签名

概念:

  1. 调用签名是指在调用函数时,指定函数参数的类型和返回值的类型的过程。在 TypeScript 中,调用签名可以通过函数类型(Function Type)来定义。
  2. 构造签名是指在创建一个类实例时,指定构造函数参数的类型和返回值的类型的过程。在 TypeScript 中,构造签名可以通过类类型(Class Type)来定义。
// 1. 调用签名
type Calculate = (a: number, b: number) => number

const add: Calculate = (a, b) => a + b

const result = add(1, 2)

// 2.构造签名

// 定义一个函数类型
type AddFunc = (a: number, b: number) => number

// 构造一个函数签名
function addFunc(a: number, b: number): number {
  return a + b
}

// 使用签名
const myAddFunc: AddFunc = addFunc

// 此时,myAddFunc只能接受两个数字参数,并且返回值也只能是数字类型
console.log(myAddFunc(1, 2))
// console.log(myAddFunc('1', '2'))  // 报错
  • 再讲一下TS的内置工具有哪些

 常见的:

  • ReturnType<T>:获取函数类型的返回值类型。
function foo(): number {
  return 1;
}

type FooReturnType = ReturnType<typeof foo>; // number
  • Parameters<T>:获取函数类型的参数类型。
function bar(x: string, y: number): boolean {
  return true;
}

type BarParamsType = Parameters<typeof bar>; // [string, number]
  • ThisParameterType<T>:获取函数类型中的 this 参数类型。
function baz(this: string, x: number): boolean {
  return true;
}

type BazThisType = ThisParameterType<typeof baz>; // string

 还有其他工具类型,如 PartialRequiredReadonlyRecordPickOmit 等,可以根据具体的使用场景灵活运用。

  • getter/setter对属性的访问进行拦截操作
class Person {
  private _name: string;

  get name() {
    console.log('获取了name属性');
    return this._name;
  }

  set name(value: string) {
    console.log(`设置了name属性为${value}`);
    this._name = value;
  }
}

const john = new Person();
john.name = 'John'; // 设置了name属性为John
john.name; // 获取了name属性,返回John

解读:这块属于类的知识点(私有属性起名前加下划线),在上面的示例中, Person 类定义了一个 _name 私有属性,同时也定义了 name 的 getter 和 setter 方法。设置 john.name 属性值时,它会通过 set name 方法拦截,并且在控制台输出一条相关信息;读取 john.name 属性值时,它会通过 get name 方法拦截,并且在控制台输出一条相关信息,然后返回 _name 私有属性的值。这样就可以方便地进行属性访问的拦截操作。

  • TS抽象类和方法使用
abstract class Animal {
  abstract makeSound(): void;
}

class Dog extends Animal {
  makeSound() {
    console.log('汪汪!');
  }
}

const dog = new Dog();
dog.makeSound(); // 输出 "汪汪!"

解读: 抽象类是不能被直接实例化的类,它只能作为其他类的基类使用。在 TypeScript 中,我们可以用 abstract 关键字来定义抽象类和抽象方法。抽象类通常用于定义一些通用的特性或行为,但不关注具体的实现方式。同时,抽象类中的抽象方法必须在子类中进行实现,以使得子类对象具有这些通用特性或行为。上面的Animal就是一个抽象类,makeSound() 是一个抽象方法。

  • 鸭子类型

概念:

鸭子类型是一种在动态语言中常见的类型检查方式,它是指只要对象拥有特定的方法或属性,就可以认为该对象具有某种类型或者说属于某个类。在TS中,鸭子类型可以像对象接口一样使用,而且是不需要去显示地实现某个接口或继承某个类。

interface Quackable {
  quack(): void;
}

function letDuckQuack(duck: Quackable) {
  duck.quack();
}

const duck = {
  quack() {
    console.log('Quack quack!');
  },
};

letDuckQuack(duck); // 输出 "Quack quack!"
  • 索引签名 

概念:索引签名是 TypeScript 中一种特殊的类型定义,用于对包含动态属性的对象进行类型推断或类型检查。它使得我们可以使用字符串或数字作为属性名来访问对象的属性,而不需要提前声明这些属性。

在 TypeScript 中,我们可以使用以下语法来定义索引签名:

type MyType = {
  [propertyName: string]: number;
  // 或者
  [index: number]: string;
}

解读: 其中,propertyName 和 index 都表示索引类型,可以是字符串或数字类型。上述示例中的第一个索引签名定义了一个额外属性,它的键值必须是字符串类型,而键对应的值必须是数字类型;第二个索引签名定义了一个额外属性,它的键值必须是数字类型,而键对应的值必须是字符串类型。

比如:城市对应人口

interface CityPopulators {
  [cityName: string]: number
}

const populations: CityPopulators = {
  Beijing: 21540000,
  Shanghai: 24240000,
  Guangzhou: 14950000,
}

console.log(populations['Beijing']) // 输出 21540000
console.log(populations.Shanghai)   // 输出 24240000
  • 如何理解接口的类实现过程,接口被类实现implements  

概念:在 TypeScript 中,接口与类之间的关系可以通过实现(implements)来建立。类实现了一个接口,就意味着这个类需要遵守接口定义中的规范,并提供一组符合接口要求的成员实现。一个类可以同时实现多个接口。

interface Person {
  name: string
  age: number

  sayHello(): void
}

class Student implements Person {
  constructor(public name: string, public age: number) {}

  sayHello() {
    console.log(`Hello, my name is ${this.name}, I am ${this.age} years old.`)
  }
}

const zhang= new Student('张三', 21)
zhang.sayHello(); // 输出 "Hello, my name is 张三, I am 21 years old."

解读:在 Student 类中,使用了 implements 关键字来实现 Person 接口,这意味着 Student 类必须具有接口中定义的所有成员,包括 name 和 age 字段,以及 sayHello() 方法。 


关于TypeScript 中的类型检查和类型推断

  • 每个对象字面量最初都被认为是fresh新鲜的

  • 当一个新的对象字面量分配给一个变量或传递给一个非空目标类型的参数时,对象字面量指定目标类型中不存在的属性是错误的

  • 当类型断言或对象字面量的类型扩大时,新鲜度会消失

首先,每个对象字面量最初被认为是 fresh 新鲜的,意思是它们是全新的且没有与其他任何对象共享类型信息。例如,下面的代码中的两个对象字面量都是 fresh 的:

const obj1 = { x: 1 }
const obj2 = { y: 2 }

这两个对象虽然具有相同的结构,但由于它们是不同的对象实例,因此它们也是不同的类型。

其次,在 TypeScript 中,当一个新的对象字面量分配给一个变量或传递给一个非空目标类型的参数时,对象字面量指定目标类型中不存在的属性会被认为是错误的。例如:

interface Point {
  x: number
  y: number
}

function printPoint(point: Point) {
  console.log(`(${point.x}, ${point.y})`)
}

printPoint({ x: 1, y: 2, z: 3 }) // 报错:Type '{ x: number; y: number; z: number; }' has no properties in common with type 'Point'.

上述示例中,我们定义了一个 Point 接口,表示点的坐标。接着,定义了一个函数 printPoint(),用于打印点的坐标信息。在调用该函数时,我们尝试传递一个额外的属性 z 给它,这会触发一个编译错误,因为 Point 接口中不存在属性 z

最后,当类型断言或对象字面量的类型扩大时,它们的新鲜度会消失。例如:

const obj1 = { x: 1 }
const obj2 = { ...obj1, y: 2 } as { x: number; y: number; z?: number }

console.log(obj2.z) // 输出 undefined

 解读:先创建了一个 fresh 的对象字面量 obj1,它具有一个属性 x: 1。然后使用对象展开语法将其扩展为另一个对象,同时使用类型断言来指定这个对象的类型,并添加一个额外的属性 y: 2 和可选属性 z?: number。这个操作使得原先 fresh 的对象 obj1 不再 fresh,因为它与其他类型信息共享了。你应该有疑惑,为什么会是undefined,其实是因为 z 是可选属性且没有被赋值,所以访问 obj2.z 的结果是 undefined


 泛型编程

概念:泛型是指将类型作为参数进行编程,并在运行时确定该类型的具体使用。在 TypeScript 代码中,泛型可以应用于函数、类和接口中,以提高其组件的灵活性和可复用性。

  • 泛型类型的参数化:使用泛型来定义可接受任意类型参数的函数或类。
function identity<T>(value: T): T {
  return value
}

const output = identity<string>('hello')
  • 泛型接口的使用:通过接口定义带有一个或多个类型参数的类型。 
interface MyPair<T, U> {
  first: T
  second: U
}

const pair: MyPair<number, string> = { first: 1, second: 'two' }
  • 映射类型 

概念:映射类型是 TypeScript 中的一种高级类型,它可以用来从一个旧类型中创建一个新类型。通过映射类型,可以快速地创建一些新类型,而不需要手动地定义每一个新类型。 

  • 映射类型,就是使用了PropertyKeys联合类型的泛型

  • 其实,PropertyKeys是通过keyof创建,然后循环遍历键名创建一个类型

interface Person {
  name: string
  age: number
}

type PersonReadonly = { readonly [P in keyof Person]: Person[P] }

const person: Person = { name: '张三', age: 18 }
const personReadonly: PersonReadonly = { name: '李四', age: 20 }

person.age = 20 // OK
personReadonly.age = 30; // Error: Cannot assign to 'age' because it is a read-only property.

 解读:定义了一个 Person 接口,包含了 name 和 age 两个属性,接着,我们使用了映射类型来创建了一个新类型 PersonReadonly,注意这里有个readonly 只读属性,所以不能去更改personReadonly的age值。

篇幅较长,编辑不易,对你有帮助的话记得三连哦~

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值