Interface 接口类型

  1. function Study(language: { name: string; age: () => number }) {
  2. console.log(`ProgramLanguage ${language.name} created ${language.age()} years ago.`);
  3. }
  4. Study({
  5. name: 'TypeScript',
  6. age: () => new Date().getFullYear() - 2012
  7. });
  8. Study({
  9. name: 2, // 不能将类型“number”分配给类型“string”。ts(2322)
  10. age: () => new Date().getFullYear() - 2012
  11. });
  12. Study({
  13. name: 'TypeScript'
  14. // 类型“{ name: string; }”的参数不能赋给类型“{ name: string; age: () => number; }”的参数。
  15. // 类型 "{ name: string; }" 中缺少属性 "age",但类型 "{ name: string; age: () => number; }" 中需要该属性。ts(2345)
  16. });
  17. Study({
  18. id: 2, // 对象文字可以只指定已知属性,并且“id”不在类型“{ name: string; age: () => number; }”中。ts(2345)
  19. name: 'TypeScript',
  20. age: () => new Date().getFullYear() - 2012
  21. });

在上边的示例中,如果我们先把这个对象字面量赋值给一个变量,然后再把变量传递给函数进行调用,那么 TypeScript 静态类型检测就会仅仅检测形参类型中定义过的属性类型,而包容地忽略任何多余的属性,此时也不会抛出一个 ts(2345) 类型错误。

  1. function Study(language: { name: string; age: () => number }) {
  2. console.log(`ProgramLanguage ${language.name} created ${language.age()} years ago.`);
  3. }
  4. let ts = {
  5. id: 2,
  6. name: 'TypeScript',
  7. age: () => new Date().getFullYear() - 2012
  8. };
  9. Study(ts); // ok

这并非一个疏忽或 bug,而是有意为之地将对象字面量和变量进行区别对待,我们把这种情况称之为对象字面量的 freshness。

  1. /** 纯 JavaScript 解构语法 */
  2. function StudyJavaScript({name, age}) {
  3. console.log(name, age);
  4. }
  5. /** TypeScript 里解构与内联类型混用 */
  6. function StudyTypeScript({name, age}: {name: string, age: () => number}) {
  7. console.log(name, age);
  8. }
  9. /** 纯 JavaScript 解构语法,定义别名 */
  10. function StudyJavaScript({name: aliasName}) { // 定义name的别名
  11. console.log(aliasName);
  12. }
  13. /** TypeScript */
  14. function StudyTypeScript(language: {name: string}) {
  15. // console.log(name); // 不能直接打印name
  16. console.log(language.name);
  17. }

从上述代码中我们可以看到,在函数中,对象解构和定义接口类型的语法很类似(如第 12 行和 17 行所示),注意不要混淆。实际上,定义内联的接口类型是不可复用的,所以我们应该更多地使用interface关键字来抽离可复用的接口类型。

  1. / ** 关键字 接口名称 */
  2. interface ProgramLanguage {
  3. /** 语言名称 */
  4. name: string;
  5. /** 使用年限 */
  6. age: () => number;
  7. }
  8. function NewStudy(language: ProgramLanguage) {
  9. console.log(`ProgramLanguage ${language.name} created ${language.age()} years ago.`);
  10. }

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

  1. let TypeScript: ProgramLanguage;
  2. TypeScript = {
  3. name: 'TypeScript',
  4. age: () => new Date().getFullYear() - 2012
  5. }
  6. TypeScript = {
  7. name: 'TypeScript'
  8. }
  9. // 类型 "{ name: string; }" 中缺少属性 "age",但类型 "ProgramLanguage" 中需要该属性。ts(2741)
  10. TypeScript = {
  11. name: 'TypeScript',
  12. age: () => new Date().getFullYear() - 2012,
  13. id: 1
  14. }
  15. // 不能将类型“{ name: string; age: () => number; id: number; }”分配给类型“ProgramLanguage”。
  16. // 对象文字可以只指定已知属性,并且“id”不在类型“ProgramLanguage”中。ts(2322)

可缺省属性

  1. /** 关键字 接口名称 */
  2. interface OptionalProgramLanguage {
  3. /** 语言名称 */
  4. name: string;
  5. /** 使用年限 */
  6. age?: () => number;
  7. }
  8. let OptionalTypeScript: OptionalProgramLanguage = {
  9. name: 'TypeScript'
  10. }; // ok

上面 age 的类型是 (() => number) | nudefined 但和直接定义为 (() => number) | nudefined 不一样,前者是可缺省的,后者不可缺省。

只读属性

  1. interface ReadOnlyProgramLanguage {
  2. /** 语言名称 */
  3. readonly name: string;
  4. /** 使用年限 */
  5. readonly age: (() => number) | undefined;
  6. }
  7. let ReadOnlyTypeScript: ReadOnlyProgramLanguage = {
  8. name: 'TypeScript',
  9. age: undefined
  10. }
  11. /** ts(2540)错误,name 只读 */
  12. ReadOnlyTypeScript.name = 'JavaScript';

定义函数类型

  1. interface StudyLanguage {
  2. (language: ProgramLanguage): void
  3. }
  4. /** 单独的函数实践 */
  5. let StudyInterface: StudyLanguage
  6. = language => console.log(`${language.name} ${language.age()}`);

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

  1. type StudyLanguageType = (language: ProgramLanguage) => void

索引签名

  1. interface LanguageRankInterface {
  2. [rank: number]: string;
  3. }
  4. interface LanguageYearInterface {
  5. [name: string]: number;
  6. }
  7. {
  8. let LanguageRankMap: LanguageRankInterface = {
  9. 1: 'TypeScript', // ok
  10. 2: 'JavaScript', // ok
  11. 'WrongINdex': '2012' // ts(2322) 不存在的属性名
  12. };
  13. let LanguageMap: LanguageYearInterface = {
  14. TypeScript: 2012, // ok
  15. JavaScript: 1995, // ok
  16. 1: 1970 // ok
  17. };
  18. }

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

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

  1. {
  2. interface StringMap {
  3. [prop: string]: number;
  4. age: number; // ok
  5. name: string; // ts(2411) name 属性的 string 类型不能赋值给字符串索引类型 number
  6. }
  7. interface NumberMap {
  8. [rank: number]: string;
  9. 1: string; // ok
  10. 0: number; // ts(2412) 0 属性的 number 类型不能赋值给数字索引类型 string
  11. }
  12. interface LanguageRankInterface {
  13. name: string; // ok
  14. 0: number; // ok
  15. [rank: number]: string;
  16. [name: string]: number;
  17. }
  18. }

另外,由于上边提到了数字类型索引的特殊性,所以我们不能约束数字索引属性与字符串索引属性拥有截然不同的类型,具体示例如下

  1. {
  2. interface LanguageRankInterface {
  3. [rank: number]: string; // ts(2413) 数字索引类型 string 类型不能赋值给字符串索引类型 number
  4. [prop: string]: number;
  5. }
  6. }

继承与实现

  1. / ** 关键字 接口名称 */
  2. interface ProgramLanguage {
  3. /** 语言名称 */
  4. name: string;
  5. /** 使用年限 */
  6. age: () => number;
  7. }
  8. {
  9. interface DynamicLanguage extends ProgramLanguage {
  10. rank: number; // 定义新属性
  11. }
  12. interface TypeSafeLanguage extends ProgramLanguage {
  13. typeChecker: string; // 定义新的属性
  14. }
  15. /** 继承多个 */
  16. interface TypeScriptLanguage extends DynamicLanguage, TypeSafeLanguage {
  17. name: 'TypeScript'; // 用原属性类型的兼容的类型(比如子集)重新定义属性
  18. }
  19. }

我们既可以使用接口类型来约束类,反过来也可以使用类实现接口

  1. / ** 关键字 接口名称 */
  2. interface ProgramLanguage {
  3. /** 语言名称 */
  4. name: string;
  5. /** 使用年限 */
  6. age: () => number;
  7. }
  8. /** 类实现接口 */
  9. {
  10. // 类“LanguageClass”错误实现接口“ProgramLanguage”。
  11. // 类型 "LanguageClass" 中缺少属性 "age",但类型 "ProgramLanguage" 中需要该属性。ts(2420)
  12. class LanguageClass implements ProgramLanguage {
  13. name: string = '';
  14. // age = () => new Date().getFullYear() - 2012
  15. }
  16. }

Type 类型别名

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

  1. / ** 关键字 接口名称 */
  2. interface ProgramLanguage {
  3. /** 语言名称 */
  4. name: string;
  5. /** 使用年限 */
  6. age: () => number;
  7. }
  8. {
  9. /** 联合 */
  10. type MixedType = string | number;
  11. /** 交叉 */
  12. type IntersectionType = { id: number; name: string; }
  13. & { age: number; name: string };
  14. /** 提取接口属性类型 */
  15. type AgeType = ProgramLanguage['age'];
  16. }

Interface 与 Type 的区别

接口类型和类型别名都可以实现内联类型的复用,但接口类型可重复定义且属性会叠加,而类型别名不可重复定义

  1. {
  2. interface Language {
  3. id: number;
  4. }
  5. interface Language {
  6. name: string;
  7. }
  8. let lang: Language = {
  9. id: 1, // ok
  10. name: 'name' // ok
  11. }
  12. }
  13. {
  14. /** ts(2300) 重复的标志 */
  15. type Language = {
  16. id: number;
  17. }
  18. /** ts(2300) 重复的标志 */
  19. type Language = {
  20. name: string;
  21. }
  22. let lang: Language = {
  23. id: 1,
  24. name: 'name'
  25. }
  26. }