TypeScript接口与泛型全面精讲

JavaScript/前端
305
0
0
2023-06-04
标签   TypeScript
目录
  • 一、接口
  • 1. Interface 接口类型
  • (1) 接口类型的基本使用
  • (2) 可缺省属性
  • (3) 只读属性
  • (4) 定义函数类型
  • (5) 索引签名
  • 2. Type 类型别名
  • 3. one question
  • 二、泛型
  • 1. 泛型类型参数
  • 2. 泛型类
  • 3. 泛型类型
  • 4. 泛型约束

一、接口

1. Interface 接口类型

(1) 接口类型的基本使用

如下定义一个接口类型:

/ ** 关键字 接口名称 */
interface ProgramLanguage {
 /** 语言名称 */
 name: string;
 /** 使用年限 */
 age: () => number;
}

现在我们就可以直接使用 ProgramLanguage 接口来定义参数的类型了:

function NewStudy(language: ProgramLanguage) {
  console.log(`ProgramLanguage ${language.name} created ${language.age()} years ago.`);
}

我们还可以通过复用接口类型定义来约束其他逻辑。比如,我们通过如下所示代码定义了一个类型为 ProgramLanguage 的变量 TypeScript :

let TypeScript: ProgramLanguage;

(2) 可缺省属性

/** 关键字 接口名称 */
interface OptionalProgramLanguage {
 /** 语言名称 */
 name: string;
 /** 使用年限 */
 age?: () => number;
}
let OptionalTypeScript: OptionalProgramLanguage = {
 name: 'TypeScript'
}; // ok

当属性被标注为可缺省后,它的类型就变成了显式指定的类型与 undefined 类型组成的联合类型,比如示例中 OptionalTypeScript 的 age 属性类型就变成了如下所示内容:

(() => number) | undefined;

(3) 只读属性

interface ReadOnlyProgramLanguage {
  /** 语言名称 */
  readonly name: string;
  /** 使用年限 */
  readonly age: (() => number) | undefined;
}
let ReadOnlyTypeScript: ReadOnlyProgramLanguage = {
name: 'TypeScript',
age: undefined
}
/** ts(2540)错误,name 只读 */
ReadOnlyTypeScript.name = 'JavaScript';

(4) 定义函数类型

在以上示例中,你可能会觉得接口类型仅能用来定义对象的类型,但是接口类型还可以用来定义函数的类型(仅仅是定义函数的类型,而不包含函数的实现),具体示例如下:

interface person {
    name: 'zyj',
    age: 20
}
interface func {
    (persona: person): void
}
let printmessage: func = persona => { console.log(`我是${persona.name},我的年龄是${persona.age}岁`)}

我们定义了一个接口类型 func,它有一个函数类型的匿名成员,函数参数类型 person,返回值的类型是 void,通过这样的格式定义的接口类型又被称之为可执行类型,也就是一个函数类型。

然后我们声明了一个 func 类型的变量,并赋给它一个箭头函数作为值。根据上下文类型推断,赋值操作左侧的 func 类型是可以约束箭头函数的类型,所以即便我们没有显式指定函数参数 persona 的类型,TypeScript 也能推断出它的类型就是 person。

实际上,我们很少使用接口类型来定义函数的类型,更多使用内联类型或类型别名配合箭头函数语法来定义函数类型,具体示例如下:

type personType = (persona: person) => void

我们给箭头函数类型指定了一个别名 personType,在其他地方就可以直接复用 personType,而不用重新声明新的箭头函数类型定义。

(5) 索引签名

索引名称的类型分为 string 和 number 两种,通过如下定义的 LanguageRankInterface 和 LanguageYearInterface 两个接口,我们可以用来描述索引是任意数字或任意字符串的对象:

interface LanguageRankInterface {
  [rank: number]: string;
}
interface LanguageYearInterface {
  [name: string]: number;
}
{
  let LanguageRankMap: LanguageRankInterface = {
    1: 'TypeScript', // ok
    2: 'JavaScript', // ok
    'WrongINdex': '2012' // ts(2322) 不存在的属性名
  };
  let LanguageMap: LanguageYearInterface = {
        TypeScript: 2012, // ok
        JavaScript: 1995, // ok
        1: 1970 // ok
  };
}

注意:在上述示例中,数字作为对象索引时,它的类型既可以与数字兼容,也可以与字符串兼容,这与 JavaScript 的行为一致。因此,使用 0 或 '0' 索引对象时,这两者等价。

注意:虽然属性可以与索引签名进行混用,但是属性的类型必须是对应的数字索引或字符串索引的类型的子集,否则会出现错误提示。

{
  interface StringMap {
    [prop: string]: number;
    age: number; // ok
    name: string; // ts(2411) name 属性的 string 类型不能赋值给字符串索引类型 number
  }
  interface NumberMap {
    [rank: number]: string;
    1: string; // ok
    0: number; // ts(2412) 0 属性的 number 类型不能赋值给数字索引类型 string
  }
  interface LanguageRankInterface {
    name: string; // ok
    0: number; // ok
    [rank: number]: string;
    [name: string]: number;
  }
}

在上述示例中,因为接口 StringMap 属性 name 的类型 string 不是它所对应的字符串索引(第 3 行定义的 prop: string)类型 number 的子集,所以会提示一个错误。同理,因为接口 NumberMap 属性 0 的类型 number 不是它所对应的数字索引(第 8 行定义的 rank: number)类型 string 的子集,所以也会提示一个错误。

2. Type 类型别名

接口类型的一个作用是将内联类型抽离出来,从而实现类型可复用。其实,我们也可以使用类型别名接收抽离出来的内联类型实现复用。

/** 类型别名 */
{
  type LanguageType = {
    /** 以下是接口属性 */
    /** 语言名称 */
    name: string;
    /** 使用年限 */
    age: () => number;
  }
}

针对接口类型无法覆盖的场景,比如组合类型、交叉类型,我们只能使用类型别名来接收,如下代码所示:

{
  /** 联合 */
  type MixedType = string | number;
  /** 交叉 */
  type IntersectionType = { id: number; name: string; } 
    & { age: number; name: string };
  /** 提取接口属性类型 */
  type AgeType = ProgramLanguage['age'];  
}

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

3. one question

如何定义如下所示 age 属性是数字类型,而其他不确定的属性是字符串类型的数据结构的对象?

{
  age: 1, // 数字类型
  anyProperty: 'str', // 其他不确定的属性都是字符串类型
  ...
}

我们肯定要用到两个接口的联合类型及类型缩减,这个问题的核心在于找到一个既是 number 的子类型,这样 age 类型缩减之后的类型就是 number;同时也是 string 的子类型,这样才能满足属性和 string 索引类型的约束关系。哪个类型满足这个条件呢?那只有特殊类型 never。

never 有一个特性是它是所有类型的子类型,自然也是 number 和 string 的子类型,所以答案如下代码所示:

  type UnionInterce =
  | {
      age: number;
    }
  | ({
      age: never;
      [key: string]: string;
    });
  const O: UnionInterce = {
    age: 2,
    string: 'string'
  };

在上述代码中,我们在第 3 行定义了 number 类型的 age 属性,第 6 行定义了 never 类型的 age 属性,等价于 age 属性的类型是由 number 和 never 类型组成的联合类型,所以我们可以把 number 类型的值(比如说数字字面量 1)赋予 age 属性;但是不能把其他任何类型的值(比如说字符串字面量 'string' )赋予 age。

同时,我们在第 5 行~第 8 行定义的接口类型中,还额外定义了 string 类型的字符串索引签名。因为 never 同时又是 string 类型的子类型,所以 age 属性的类型和字符串索引签名类型不冲突。如第 9 行~第 12 行所示,我们可以把一个 age 属性是 2、string 属性是 'string' 的对象字面量赋值给 UnionInterce 类型的变量 O。

二、泛型

1. 泛型类型参数

function reflect<P>(param: P) {
  return param;
}

这里我们可以看到,尖括号中的 P 表示泛型参数的定义,param 后的 P 表示参数的类型是泛型 P(即类型受 P 约束)。

const reflectStr = reflect<string>('string'); // str 类型是 string
const reflectNum = reflect<number>(1); // num 类型 number

然后在调用函数时,我们也通过 <> 语法指定了如下所示的 string、number 类型入参,相应地,reflectStr 的类型是 string,reflectNum 的类型是 number。

另外,如果调用泛型函数时受泛型约束的参数有传值,泛型参数的入参可以从参数的类型中进行推断,而无须再显式指定类型(可缺省),因此上边的示例可以简写为如下示例:

const reflectStr2 = reflect('string'); // str 类型是 string
const reflectNum2 = reflect(1); // num 类型 number

泛型不仅可以约束函数整个参数的类型,还可以约束参数属性、成员的类型,比如参数的类型可以是数组、对象,如下示例:

function reflectArray<P>(param: P[]) {
  return param;
}
const reflectArr = reflectArray([1, '1']); // reflectArr 是 (string | number)[]

这里我们约束了 param 的类型是数组,数组的元素类型是泛型入参。

2. 泛型类

在类的定义中,我们还可以使用泛型用来约束构造函数、属性、方法的类型,如下代码所示:

class Memory<S> {
  store: S;
  constructor(store: S) {
    this.store = store;
  }
  set(store: S) {
    this.store = store;
  }
  get() {
    return this.store;
  }
}
const numMemory = new Memory<number>(1); // <number> 可缺省
const getNumMemory = numMemory.get(); // 类型是 number
numMemory.set(2); // 只能写入 number 类型
const strMemory = new Memory(''); // 缺省 <string>
const getStrMemory = strMemory.get(); // 类型是 string
strMemory.set('string'); // 只能写入 string 类型

3. 泛型类型

将类型入参的定义移动到类型别名或接口名称后,此时定义的一个接收具体类型入参后返回一个新类型的类型就是泛型类型。

type GenericReflectFunction<P> = (param: P) => P;
interface IGenericReflectFunction<P> {
  (param: P): P;
}
const reflectFn4: GenericReflectFunction<string> = reflect; // 具象化泛型
const reflectFn5: IGenericReflectFunction<number> = reflect; // 具象化泛型
const reflectFn3Return = reflectFn4('string'); // 入参和返回值都必须是 string 类型
const reflectFn4Return = reflectFn5(1); //  入参和返回值都必须是 number 类型

在泛型定义中,我们甚至可以使用一些类型操作符进行运算表达,使得泛型可以根据入参的类型衍生出各异的类型,如下代码所示:

type StringOrNumberArray<E> = E extends string | number ? E[] : E;
type StringArray = StringOrNumberArray<string>; // 类型是 string[]
type NumberArray = StringOrNumberArray<number>; // 类型是 number[]
type NeverGot = StringOrNumberArray<boolean>; // 类型是 boolean

发散一下,如果我们给上面这个泛型传入了一个 string | boolean 联合类型作为入参,将会得到什么类型呢?且看如下所示示例:

type BooleanOrString = string | boolean;
type WhatIsThis = StringOrNumberArray<BooleanOrString>; // 好像应该是 string | boolean ?
type BooleanOrStringGot = BooleanOrString extends string | number ? BooleanOrString[] : BooleanOrString; //  string | boolean

如果你使用 VS Code 尝试了这个示例,并 hover 类型别名 WhatIsThis ,那么你会发现显示的类型将是 boolean | string[]。这个就是所谓的分配条件类型:

在条件类型判断的情况下(比如上边示例中出现的 extends),如果入参是联合类型,则会被拆解成一个个独立的(原子)类型(成员)进行类型运算。

比如上边示例中的 string | boolean 入参,先被拆解成 string 和 boolean 这两个独立类型,再分别判断是否是 string | number 类型的子集。因为 string 是子集而 boolean 不是,所以最终我们得到的 WhatIsThis 的类型是 boolean | string[]。

4. 泛型约束

function reflectSpecified<P extends number | string | boolean>(param: P):P {
  return param;
}
reflectSpecified('string'); // ok
reflectSpecified(1); // ok
reflectSpecified(true); // ok
reflectSpecified(null); // ts(2345) 'null' 不能赋予类型 'number | string | boolean'

同样,我们也可以把接口泛型入参约束在特定的范围内,如下代码所示:

interface ReduxModelSpecified<State extends { id: number; name: string }> {
  state: State
}
type ComputedReduxModel1 = ReduxModelSpecified<{ id: number; name: string; }>; // ok
type ComputedReduxModel2 = ReduxModelSpecified<{ id: number; name: string; age: number; }>; // ok
type ComputedReduxModel3 = ReduxModelSpecified<{ id: string; name: number; }>; // ts(2344)
type ComputedReduxModel4 = ReduxModelSpecified<{ id: number;}>; // ts(2344)

在上述示例中,ReduxModelSpecified 泛型仅接收 { id: number; name: string } 接口类型的子类型作为入参。