标签搜索

Vue3 学习笔记(七)

sunshine
2022-12-24 / 0 评论 / 32 阅读
温馨提示:
本文最后更新于2024年08月27日,已超过52天没有更新,若内容或图片失效,请留言反馈。
  • 02-详解接口与类型别名之间区别
  • 03-字面量类型和keyof关键字
  • 04-类型保护与自定义类型保护
  • 05-定义泛型和泛型常见操作
  • 06-类型兼容性详解
  • 07-映射类型与内置工具类型
  • 08-条件类型和infer关键字
  • 09-类中如何使用类型

详解接口与类型别名之间区别

接口

接口是一系列抽象方法的声明,是一些方法特征的集合。简单来说,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。

接口跟类型别名类似都是用来定义类型注解的,接口是用interface关键字来实现的,如下:

interface A {
  username: string;
  age: number;
}
let a: A = {
  username: 'xiaoming',
  age: 20
}

因为接口跟类型别名功能类似,所以接口也具备像索引签名,可调用注解等功能。

interface A {
  [index: number]: number;
}
let a: A = [1, 2, 3];

interface A {
  (): void;
}
let a: A = () => {}

接口与别名的区别

那么接口跟类型别名除了很多功能相似外,他们之间也是具备某些区别的。

  1. 对象类型
  2. 接口合并
  3. 接口继承
  4. 映射类型

第一个区别,类型别名可以操作任意类型,而接口只能操作对象类型。

第二个区别,接口可以进行合并操作。

interface A {
  username: string;
}
interface A {
  age: number;
}
let a: A = {
  username: 'xiaoming',
  age: 20
}

第三个区别,接口具备继承能力。

interface A {
  username: string
}
interface B extends A {
  age: number
}
let b: B = {
  username: 'xiaoming',
  age: 20
}

B这个接口继承了A接口,所以B类型就有了username这个属性。在指定类型的时候,b变量要求同时具备A类型和B类型。

第四个区别,接口不具备定义成接口的映射类型,而别名是可以做成映射类型的,关于映射类型的用法后面小节中会详细的进行讲解,这里先看一下效果。

type A = {   // success
  [P in 'username'|'age']: string;
}
interface A {   // error
  [P in 'username'|'age']: string;
}

字面量类型和keyof关键字

字面量类型

在TS中可以把字面量作为具体的类型来使用,当使用字面量作为具体类型时, 该类型的取值就必须是该字面量的值。

type A = 1;
let a: A = 1;

这里的A对应一个1这样的值,所以A类型就是字面量类型,那么a变量就只能选择1作为可选的值,除了1作为值以外,那么其他值都不能赋值给a变量。

那么字面量类型到底有什么作用呢?实际上字面量类型可以把类型进行缩小,只在指定的范围内生效,这样可以保证值不易写错。

type A = 'linear'|'swing';
let a: A = 'ease'    // error

比如a变量,只有两个选择,要么是linear要么是swing,不能是其他的第三个值作为选项存在。

keyof关键字

在一个定义好的接口中,想把接口中的每一个属性提取出来,形成一个联合的字面量类型,那么就可以利用keyof关键字来实现。

interface A {
  username: string;
  age: number;
}
//keyof A -> 'username'|'age'
let a: keyof A = 'username';

如果我们利用typeof语法去引用一个变量,可以得到这个变量所对应的类型,如下:

let a = 'hello';
type A = typeof a;   // string 

那么利用这样一个特性,可以通过一个对象得到对应的字面量类型,把typeof和keyof两个关键字结合使用。

let obj: {
  username: 'xiaoming',
  age: 20
}
let a: keyof typeof obj = 'username'

类型保护与自定义类型保护

类型保护

类型保护允许你使用更小范围下的对象类型。这样可以缩小类型的范围保证类型的正确性,防止TS报错。这段代码在没有类型保护的情况下就会报错,如下:

function foo(n: string|number){
    n.length   // error
}

因为n有可能是number,所以TS会进行错误提示,可以利用类型断言来解决,但是这种方式只是欺骗TS,如果在运行阶段还是可能报错的,所以并不是最好的方式。利用类型保护可以更好的解决这个问题。

类型保护的方式有很多种,主要是四种方式:

  1. typeof关键字
  2. instanceof关键字
  3. in关键字
  4. 字面量类型

typeof关键字实现类型保护:

function foo(n: string|number){
  if(typeof n === 'string'){
    n.length   // success
  }
}

instanceof关键字实现类型保护,主要是针对类进行保护的:

class Foo {
  username = 'xiaoming'
}
class Bar {
  age = 20
}
function baz(n: Foo|Bar){
  if( n instanceof Foo ){
    n.username
  }
}

in关键字实现类型保护,主要是针对对象的属性保护的:

function foo(n: { username: string } | { age: number }){
  if( 'username' in n ){
    n.username
  }
}

字面量类型保护,如下:

function foo(n: 'username'|123){
  if( n === 'username' ){
    n.length
  }
}

自定义类型保护

除了以上四种方式可以做类型保护外,如果我们想自己去实现类型保护可行吗?答案是可以的,只需要利用is关键字即可, is为类型谓词,它可以做到类型保护。

function isString(n: any): n is string{
  return typeof n === 'string';
}
function foo(n: string|number){
  if( isString(n) ){
    n.length
  }
}

定义泛型和泛型常见操作

定义泛型

泛型是指在定义函数、接口或者类时,未指定其参数类型,只有在运行时传入才能确定。泛型简单来说就是对类型进行传参处理。

type A<T = string> = T //泛型默认值
let a: A = 'hello'
let b: A<number> = 123
let c: A<boolean> = true

这里可以看到通过<T>来定义泛型,还可以给泛型添加默认值<T=string>,这样当我们不传递类型的时候,就会已string作为默认的类型进行使用。

泛型还可以传递多个,实现多泛型的写法。

type A<T, U> = T|U;  //多泛型

在前面我们学习数组的时候,讲过数组有两种定义方式,除了基本定义外,还有一种泛型的写法,如下:

let arr: Array<number> = [1, 2, 3];
//自定义MyArray实现
type MyArray<T> = T[];
let arr2: MyArray<number> = [1, 2, 3];

泛型在函数中的使用:

function foo<T>(n: T){
}
foo<string>('hello');
foo(123);   // 泛型会自动类型推断

泛型跟接口结合的用法:

interface A<T> {
  (n?: T): void
  default?: T
}
let foo: A<string> = (n) => {}
let foo2: A<number> = (n) => {}
foo('hello')
foo.default = 'hi'
foo2(123)
foo2.default = 123

泛型与类结合的用法:

class Foo<T> {
  username!: T;
}
let f = new Foo<string>();
f.username = 'hello';

class Foo<T> {
  username!: T
}
class Baz extends Foo<string> {}
let f = new Baz()
f.username = 'hello'

有时候也会对泛型进行约束,可以指定哪些类型才能进行传递:

type A = {
  length: number
}
function foo<T extends A>(n: T) {}
foo(123)   // error
foo('hello')

通过extends关键字可以完成泛型约束处理。

类型兼容性详解

类型兼容性

类型兼容性用于确定一个类型是否能赋值给其他类型。如果是相同的类型是可以进行赋值的,如果是不同的类型就不能进行赋值操作。

let a: number = 123;
let b: number = 456;
b = a;   // success

let a: number = 123;
let b: string = 'hello';
b = a;   // error

当有类型包含的情况下,又是如何处理的呢?

let a: number = 123;
let b: string | number = 'hello';
//b = a;  // success
a = b;  // error

变量a是可以赋值给变量b的,但是变量b是不能赋值给变量a的,因为b的类型包含a的类型,所以a赋值给b是可以的。

在对象类型中也是一样的处理方式,代码如下:

let a: {username: string} = { username: 'xiaoming' };
let b: {username: string; age: number} = { username: 'xiaoming', age: 20 };
a = b; // success
b = a;  // error

b的类型满足a的类型,所以b是可以赋值给a的,但是a的类型不能满足b的类型,所以a不能赋值给b。所以看下面的例子就明白为什么这样操作是可以的。

function foo(n: { username: string }) {}
foo({ username: 'xiaoming' }) // success
foo({ username: 'xiaoming', age: 20 }) // error
let a = { username: 'xiaoming', age: 20 }
foo(a) // success

这里把值存成一个变量a,再去进行传参就是利用了类型兼容性做到的。

映射类型与内置工具类型

映射类型

可以将已知类型的每个属性都变为可选的或者只读的。简单来说就是可以从一种类型映射出另一种类型。这里我们先要明确一点,映射类型只能用类型别名去实现,不能使用接口的方式来实现。

先看一下在TS中是如何定义一个映射类型的。

type A = {
  username: string
  age: number
}
type B<T> = {
  [P in keyof T]: T[P]
}
type C = B<A>

这段代码中类型C与类型A是完全一样的,其中in关键字就类似于一个for in循环,可以处理A类型中的所有属性记做p,然后就可以得到对应的类型T[p]

那么我们就可以通过添加一些其他语法来实现不同的类型出来,例如让每一个属性都是只读的,可以给每一项前面添加readonly关键字。

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

内置工具类型

每次我们去实现这种映射类型的功能是非常麻烦的,所以TS中给我们提供了很多常见的映射类型,这些内置的映射类型被叫做,内置工具类型。

Readonly就是跟我们上边实现的映射类型是一样的功能,给每一个属性做成只读的。

type A = {
  username: string
  age: number
}
/* type B = {
    readonly username: string;
    readonly age: number;
} */
type B = Readonly<A>

Partial可以把每一个属性变成可选的。

type A = {
  username: string
  age: number
}
/* type B = {
    username?: string|undefined;
    age?: number|undefined;
} */
type B = Partial<A>

Pick可以把某些指定的属性给筛选出来。

type A = {
  username: string
  age: number
  gender: string
}
/* type D = {
    username: string;
    age: number;
} */
type D = Pick<A, 'username'|'age'>

Record可以把字面量类型指定为统一的类型。

/* type E = {
    username: string;
    age: string;
} */
type E = Record<'username'|'age', string>

Required可以把对象的每一个属性变成必选项。

type A = {
  username?: string
  age?: number
}
/* type B = {
    username: string;
    age: number;
} */
type B = Required<A>

Omit是跟Pick工具类相反的操作,把指定的属性进行排除。

type A = {
  username: string
  age: number
  gender: string
}
/* type D = {
    gender: string
} */
type D = Omit<A, 'username'|'age'>

Exclude可以排除某些类型,得到剩余的类型。

// type A = number 
type A = Exclude<string | number | boolean, string | boolean>

我们的内置工具类型还有一些,如:Extract、NonNullable、Parameters、ReturnType等,下一个小节中将继续学习剩余的工具类型。

条件类型和infer关键字

在上一个小节中,学习了Exclude这个工具类型,那么它的底层实现原理是怎样的呢?

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

这里可以看到Exclude利用了 ? : 的写法来实现的,这种写法在TS类型中表示条件类型,让我们一起来了解下吧。

条件类型

条件类型就是在初始状态并不直接确定具体类型,而是通过一定的类型运算得到最终的变量类型。

type A = string
type B = number | string
type C = A extends B ? {} : []

条件类型需要使用extends关键字,如果A类型继承B类型,那么C类型得到问号后面的类型,如果A类型没有继承B类型,那么C类型得到冒号后面的类型,当无法确定A是否继承B的时候,则返回两个类型的联合类型。

那么大多数情况下,条件类型还是在内置工具类型中用的比较多,就像上面的Exclude方法,下面就让我们一起看一下其他内置工具类型该如何去用吧。

Extract跟Exclude正好相反,得到需要筛选的类型。

// type Extract<T, U> = T extends U ? T : never  -> 实现原理
// type A = string 
type A = Extract<string | number | boolean, string>

NonNullable用于排除null和undefined这些类型。

//type NonNullable<T> = T extends null | undefined ? never : T;  -> 实现原理
//type A = string
type A = NonNullable<string|null|undefined>

Parameters可以把函数的参数转成对应的元组类型。

type Foo = (n: number, m: string) => string
//type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;   -> 实现原理
// type A = [n: number, m: string]
type A = Parameters<Foo> 

在Parameters方法的实现原理中,出现了一个infer关键字,它主要是用于在程序中对类型进行定义,通过得到定义的p类型来决定最终要的结果。

ReturnType可以把函数的返回值提取出类型。

type Foo = (n: number, m: string) => string
//type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;   -> 实现原理
//type A = string
type A = ReturnType<Foo>

这里也通过infer关键字定义了一个R类型,对应的就是函数返回值的类型。通过infer关键字可以在泛型之外也可以定义类型出来。

下面再利用infer来实现一个小功能,定义一个类型方法,当传递一个数组的时候返回子项的类型,当传递一个基本类型的时候就返回这个基本类型。

type A<T> = T extends Array<infer U> ? U : T
// type B = number
type B = A<Array<number>>
// type C = string
type C = A<string>

这里的U就是自动推断出的数组里的子元素类型,那么就可以完成我们的需求。

类中如何使用类型

本小节主要讲解在类中如何使用TS的类型,对于类的一些功能使用方式,例如:类的修饰符、混入、装饰器、抽象类等等并不做过多的介绍。

类中定义类型

属性必须给初始值,如果不给初始值可通过非空断言来解决。

class Foo {
  username!: string;
}

给初始值的写法如下:

class Foo {
  //第一种写法
  //username: string = 'xiaoming';
  //第二种写法
  // username: string;
  // constructor(){
  //   this.username = 'xiaoming';
  // }
  //第三种写法
  username: string;
  constructor(username: string){
    this.username = username;
  }
}

类中定义方法及添加类型也是非常简单的。

class Foo {
  ...
  showAge = (n: number): number => {
    return n;
  }
}

类使用接口

类中使用接口,是需要使用implements关键字。

interface A {
  username: string
  age: number
  showName(n: string): string
}

class Foo implements A {
  username: string = 'xiaoming'
  age: number = 20
  gender: string = 'male'  
  showName = (n: string): string => {
    return n
  }
}

在类中使用接口的时候,是一种类型兼容性的方式,对于少的字段是不行的,但是对于多出来的字段是没有问题的,比如说gender字段。

类使用泛型

class Foo<T> {
  username: T;
  constructor(username: T){
    this.username = username;
  }
}
new Foo<string>('xiaoming');

继承中用的也比较多。

class Foo<T> {
  username: T;
  constructor(username: T){
    this.username = username;
  }
}
class Bar extends Foo<string> {
}

最后来看一下,类中去结合接口与泛型的方式。

interface A<T> {
  username: T
  age: number
  showName(n: T): T
}
class Foo implements A<string> {
  username: string = 'xiaoming'
  age: number = 20
  gender: string = 'male'
  showName = (n: string): string => {
    return n
  }
}
  • 了解类型中接口与类型别名之间的区别,及各种使用方式
  • 掌握什么是泛型与泛型的应用场景,以及与其他语法的结合使用
  • 了解了类型的一些高级用法,如:类型保护、类型兼容性
  • 了解了类型的一些高级语法,如:映射类型、条件类型
  • 全面学习TS中内置的工具类型,如: Partial、Readonly …等
感觉很棒,欢迎点赞 OR 打赏~
0
分享到:

评论 (0)

取消