(整理)TypeScript学习记录

146 阅读37分钟

参考链接:

  1. 官方文档:文档简介 · TypeScript中文网 · TypeScript——JavaScript的超集

  2. 官方运行沙箱:TypeScript: 演练场 - 一个用于 TypeScript 和 JavaScript 的在线编辑器

  3. 2022 typescript史上最强学习入门文章(2w字) - 掘金

1、 为什么

因为JavaScript是弱类型, 很多错误只有在运行时才会被发现,而TypeScript提供了一套静态检测机制, 帮助我们在编译时就发现错误

2、 是什么

关于TypeScript,官方自诩为先进的JavaScript,我认为是JS的超集,具备JS的功能,同时还提供静态类型检测,还提供后端语言才有的:枚举、泛型、类型转换、命名空间、声明文件、类、接口等。

3、 基本类型

let str: string = "jimmy";
let num: number = 24;
let bool: boolean = false;
let u: undefined = undefined;
let n: null = null;
let obj: object = {x: 1};
let big: bigint = 100n;
let sym: symbol = Symbol("me");

null 和 undefined 可以赋值给 其他基本类型,反之不行,因为他们两个是其他基本类型的子集。若在 tsconfig.json 指定了 "strictNullChecks":true,则这两个只能赋值给 void 和他们各自的类型。

number 和 bigint 互不兼容,所以不能相互赋值。

4、 其他类型

1. Array

// 普通定义
let arr:string[] = ["1","2"];
let arr2:Array<string> = ["1","2"];

// 联合类型
let arr:(number | string)[];
arr3 = [1, 'b', 2, 'c'];

// 指定成员对象
interface Arrobj{
  name:string,
  age:number
}
let arr3:Arrobj[]=[{name:'jimmy',age:22}]

2. Function

// 函数声明
function sum(x: number, y: number): number {
    return x + y;
}

// 函数表达式
let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
    return x + y;
};

// 用接口定义函数类型
interface SearchFunc{
  (source: string, subString: string): boolean;
}

// 可选参数
// 可选参数后面不允许再出现必需参数
function buildName(firstName: string, lastName?: string) {
    if (lastName) {
        return firstName + ' ' + lastName;
    } else {
        return firstName;
    }
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');

// 参数默认值
function buildName(firstName: string, lastName: string = 'Cat') {
    return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');

// 剩余参数
function push(array: any[], ...items: any[]) {
    items.forEach(function(item) {
        array.push(item);
    });
}
let a = [];
push(a, 1, 2, 3);

// 函数重载
// *********************重点!!!**********************
    JS 是动态语言,我们通常会使用不同类型的参数来调用同一个函数,该函数会根据不同的参数而返回不同的类型的调用结果:
function add(x, y) {
 return x + y;
}
add(1, 2); // 3
add("1", "2"); //"12"

由于 TypeScriptJavaScript 的超集,因此以上的代码可以直接在 TypeScript 中使用,但当 TypeScript 编译器开启 noImplicitAny 的配置项时,以上代码会提示以下错误信息:
// Parameter 'x' implicitly has an 'any' type.
// Parameter 'y' implicitly has an 'any' type.

    该信息告诉我们参数 x 和参数 y 隐式具有 any 类型。为了解决这个问题,我们可以为参数设置一个类型。因为我们希望 add 函数同时支持 stringnumber 类型,因此我们可以定义一个 string | number 联合类型,同时我们为该联合类型取个别名:
type Combinable = string | number;
    因此,代码示例更新如下:
function add(a: Combinable, b: Combinable) {
    if (typeof a === 'string' || typeof b === 'string') {
     return a.toString() + b.toString();
    }
    return a + b;
}
    此时仍然报错:
// Property 'split' does not exist on type 'number'.
    函数重载的定义:函数重载或方法重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力。所以,用TS提供的函数重载特性,代码更新如下:
type Types = number | string

function add(a:number,b:number):number;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
function add(a:Types, b:Types) {
  if (typeof a === 'string' || typeof b === 'string') {
    return a.toString() + b.toString();
  }
  return a + b;
}
const result = add('Semlinker', ' Kakuqo');
result.split(' ');

    这样 ,代码的类型就实现了“自适应”。

3.Tuple

    这是 TypeScript 中特有的类型,其工作方式类似于数组。元组最重要的特性是可以限制数组元素的个数和类型。
    注意,元组类型只能表示一个已知元素数量和类型的数组,长度已指定,越界访问会提示错误。如果一个数组中可能有多种类型,数量和类型都不确定,那就直接any[]
// 使用
let x: [string, number]; 
// 类型必须匹配且个数必须为2

x = ['hello', 10]; // OK 
x = ['hello', 10,10]; // Error 
x = [10, 'hello']; // Error

// 只读
const point: readonly [number, number] = [10, 20];

// 解构赋值
let employee: [number, string] = [1, "Semlinker"];
let [id, username] = employee;

// 可选元素
let optionalTuple: [string, boolean?];
optionalTuple = ["Semlinker", true];
console.log(`optionalTuple : ${optionalTuple}`);
optionalTuple = ["Kakuqo"];
console.log(`optionalTuple : ${optionalTuple}`);
// 输出:
optionalTuple : Semlinker,true
optionalTuple : Kakuqo

// 剩余元素
type RestTupleType = [number, ...string[]];
let restTuple: RestTupleType = [666, "Semlinker", "Kakuqo", "Lolo"];
console.log(restTuple[0]);
console.log(restTuple[1]);

4、void

    定义:表示没有类型,与其他类型同级,不能直接赋值
let a: void; 
let b: number = a; // Error
只能为它赋予nullundefined(在strictNullChecks未指定为true时)

注意:方法没有返回值将得到undefined,但是我们需要定义成void类型,而不是undefined类型。否则将报错:
function fun(): undefined {
  console.log("this is TypeScript");
};
fun(); // Error

5、never

表示的是那些永不存在的值的类型,包含两种情况:异常和死循环
// 异常
function err(msg: string): never { // OK
  throw new Error(msg); 
}

// 死循环
function loopForever(): never { // OK
  while (true) {};
}

    never类型同nullundefined一样,也是任何类型的子类型,也可以赋值给任何类型。但是没有类型是never的子类型或可以赋值给never类型(除了never本身之外),即使any也不可以赋值给never
let ne: never;
let nev: never;
let an: any;

ne = 123; // Error
ne = nev; // OK
ne = an; // Error
ne = (() => { throw new Error("异常"); })(); // OK
ne = (() => { while(true) {} })(); // OK

6、any

任何类型都可以被归为 any 类型。这让 any 类型成为了类型系统的顶级类型

如果是一个普通类型,在赋值过程中改变类型是不被允许的:
let a: string = 'seven';
a = 7;
// TS2322: Type 'number' is not assignable to type 'string'.
但如果是 any 类型,则允许被赋值为任意类型。
let a: any = 666;
a = "Semlinker";
a = false;
a = 66
a = undefined
a = null
a = []
a = {}

在any上访问任何属性都是允许的,也允许调用任何方法.
let anyThing: any = 'hello';
console.log(anyThing.myName);
console.log(anyThing.myName.firstName);
let anyThing: any = 'Tom';
anyThing.setName('Jerry');
anyThing.setName('Jerry').sayHello();
anyThing.myName.setFirstName('Cat');

变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型:
let something;
something = 'seven';
something = 7;
something.setName('Tom');
等价于:
let something: any;
something = 'seven';
something = 7;
something.setName('Tom');

注意:
    这太宽松了,any 简直是无所不能,所以用不用any都一样,我觉得不到万不得已的情况都不用 any,因为这样就失去了使用 TS 的意义。
    为了解决 any 带来的问题,TypeScript 3.0 引入了 unknown 类型。

7、unknow

unknown与any一样,所有类型都可以分配给unknown:
let notSure: unknown = 4;
notSure = "maybe a string instead"; // OK
notSure = false; // OK

注意:unknown 与 any 的最大区别是: 任何类型的值可以赋值给 any,同时 any 类型的值也可以赋值给任何类型。unknown 任何类型的值都可以赋值给它,但它只能赋值给 unknown 和 any。
let notSure: unknown = 4;
let uncertain: any = notSure; // OK

let notSure: any = 4;
let uncertain: unknown = notSure; // OK

let notSure: unknown = 4;
let uncertain: number = notSure; // Error

示例:
如果不缩小类型,就无法对unknown类型执行任何操作:
function getDog() {
 return '123'
}
 
const dog: unknown = {hello: getDog};
dog.hello(); // Error
可以使用typeof、类型断言等方式来缩小未知范围:
function getDogName() {
 let x: unknown;
 return x;
};
const dogName = getDogName();
// 直接使用
const upName = dogName.toLowerCase(); // Error
// typeof
if (typeof dogName === 'string') {
  const upName = dogName.toLowerCase(); // OK
}
// 类型断言 
const upName = (dogName as string).toLowerCase(); // OK

8、Number、String、Boolean、Symbol

这是相应原始类型的包装对象,俗称对象类型。
从类型兼容性上看,原始类型兼容对应的对象类型,反过来对象类型不兼容对应的原始类型。
let num: number;
let Num: Number;
Num = num; // ok
num = Num; // ts(2322)报错

注意:不要使用对象类型来注解值的类型,因为这没有任何意义。

9、object、Object 和 {}

    object 代表的是所有非原始类型,在严格模式下,null 和 undefined 类型也不能赋给 object。
let lowerCaseObject: object;
lowerCaseObject = 1; // ts(2322)
lowerCaseObject = 'a'; // ts(2322)
lowerCaseObject = true; // ts(2322)
lowerCaseObject = null; // ts(2322)
lowerCaseObject = undefined; // ts(2322)
lowerCaseObject = {}; // ok

    Object 代表所有拥有 toString、hasOwnProperty 方法的类型,所以所有原始类型、非原始类型都可以赋给 Object。同样,在严格模式下,null 和 undefined 类型也不能赋给 Object。
let upperCaseObject: Object;
upperCaseObject = 1; // ok
upperCaseObject = 'a'; // ok
upperCaseObject = true; // ok
upperCaseObject = null; // ts(2322)
upperCaseObject = undefined; // ts(2322)
upperCaseObject = {}; // ok

注意:Object 不仅是 object 的父类型,同时也是 object 的子类型。
type isLowerCaseObjectExtendsUpperCaseObject = object extends Object ? true : false; // true
type isUpperCaseObjectExtendsLowerCaseObject = Object extends object ? true : false; // true
upperCaseObject = lowerCaseObject; // ok
lowerCaseObject = upperCaseObject; // ok

{}空对象类型和大 Object 一样,也是表示原始类型和非原始类型的集合,并且在严格模式下,null 和 undefined 也不能赋给 {} ,如下示例:
let ObjectLiteral: {};
ObjectLiteral = 1; // ok
ObjectLiteral = 'a'; // ok
ObjectLiteral = true; // ok
ObjectLiteral = null; // ts(2322)
ObjectLiteral = undefined; // ts(2322)
ObjectLiteral = {}; // ok
type isLiteralCaseObjectExtendsUpperCaseObject = {} extends Object ? true : false; // true
type isUpperCaseObjectExtendsLiteralCaseObject = Object extends {} ? true : false; // true
upperCaseObject = ObjectLiteral;
ObjectLiteral = upperCaseObject;

结论:{}、大 Object 是比小 object 更宽泛的类型(least specific),{} 和大 Object 可以互相代替,用来表示原始类型(null、undefined 除外)和非原始类型;而小 object 则表示非原始类型。

5、 类型推断

如果每定义一个变量都去定义相应的类型,会不会很烦?会!但是 TS 有类型推断的功能:
{
  let str = 'this is string'; // 等价 let str: string = 'this is string'; 下面类似
  let num = 1; // 等价
  let bool = true; // 等价
}
{
  const str = 'this is string'; // 不等价
  const num = 1; // 不等价
  const bool = true; // 不等价
}
在 TypeScript 中,具有初始化值的变量、有默认值的函数参数、函数返回的类型都可以根据上下文推断出来。
如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查:
let myFavoriteNumber;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;

6、 类型断言

    TypeScript 类型检测无法做到绝对智能,毕竟程序不能像人一样思考。有时会碰到我们比 TypeScript 更清楚实际类型的情况,这时候类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。它没有运行时的影响,只是在编译阶段起作用。
const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = arrayNumber.find(num => num > 2); // 提示 ts(2322)

// 使用类型断言
const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = arrayNumber.find(num => num > 2) as number;

// 两种使用语法
// 尖括号 语法
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;

// as 语法
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

    注意:以上两种方式虽然没有任何区别,但是尖括号格式会与react中JSX产生语法冲突,因此我们更推荐使用 as 语法。
    
// 非空断言
    在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符 ! 可以用于断言操作对象是非 null 和非 undefined 类型。具体而言,x! 将从 x 值域中排除 nullundefinedlet mayNullOrUndefinedOrString: null | undefined | string;
mayNullOrUndefinedOrString!.toString(); // ok
mayNullOrUndefinedOrString.toString(); // ts(2531)

type NumGenerator = () => number;

function myFunc(numGenerator: NumGenerator | undefined) {
  // Object is possibly 'undefined'.(2532)
  // Cannot invoke an object which is possibly 'undefined'.(2722)
  const num1 = numGenerator(); // Error
  const num2 = numGenerator!(); //OK
}

// 确定赋值断言
    允许在实例属性和变量声明后面放置一个 ! 号,从而告诉 TypeScript 该属性会被明确地赋值。为了更好地理解它的作用,我们来看个具体的例子:
let x: number;
initialize();

// Variable 'x' is used before being assigned.(2454)
console.log(2 * x); // Error
function initialize() {
  x = 10;
}
    很明显该异常信息是说变量 x 在赋值前被使用了,要解决该问题,我们可以使用确定赋值断言:
let x!: number;
initialize();
console.log(2 * x); // Ok

function initialize() {
  x = 10;
}

7、 字面量类型

在 TypeScript 中,字面量不仅可以表示值,还可以表示类型,即所谓的字面量类型。支持 3 种字面量类型:字符串字面量类型、数字字面量类型、布尔字面量类型,对应的字符串字面量、数字字面量、布尔字面量分别拥有与其值一样的字面量类型。
{
  let specifiedStr: 'this is string' = 'this is string';
  let specifiedNum: 1 = 1;
  let specifiedBoolean: true = true;
}

{
  let specifiedStr: 'this is string' = 'this is string';
  let str: string = 'any string';
  specifiedStr = str; // ts(2322) 类型 '"string"' 不能赋值给类型 'this is string'
  str = specifiedStr; // ok 
}

// 字符串字面量类型
实际上,定义单个的字面量类型并没有太大的用处,它真正的应用场景是可以把多个字面量类型组合成一个联合类型,用来描述拥有明确成员的实用的集合。使用字面量联合类型描述了一个明确、可 'up' 可 'down' 的集合,这样就能清楚地知道需要的数据结构了。
type Direction = 'up' | 'down';

function move(dir: Direction) {
  // ...
}
move('up'); // ok
move('right'); // ts(2345) Argument of type '"right"' is not assignable to parameter of type 'Direction'

// 数字字面量类型及布尔字面量类型
与上面的很相似:
interface Config {
  size: 'small' | 'big';
  isEnable:  true | false;
  margin: 0 | 2 | 4;
}

// let和const分析
将 const 定义为一个不可变更的常量,在缺省类型注解的情况下,TypeScript 推断出它的类型直接由赋值字面量的类型决定
{
  const str = 'this is string'; // str: 'this is string'
  const num = 1; // num: 1
  const bool = true; // bool: true
}

在下面代码中,缺省显式类型注解的可变更的变量的类型转换为了赋值字面量类型的父类型,比如 str 的类型是 'this is string' 类型(这里表示一个字符串字面量类型)的父类型 string,num 的类型是 1 类型的父类型 number。

{
  let str = 'this is string'; // str: string
  let num = 1; // num: number
  let bool = true; // bool: boolean
}

这种设计符合编程预期,意味着我们可以分别赋予 str 和 num 任意值(只要类型是 string 和 number 的子集的变量):
str = 'any string';
num = 2;
bool = false;

将 TypeScript 的字面量子类型转换为父类型的这种设计称之为 "literal widening",也就是字面量类型的拓宽,比如上面示例中提到的字符串字面量类型转换成 string 类型
具体而言:const有不可变性,所以采用字面量类型强制锁定类型是合理的,而let是可变的,所以let的类型锚定赋值字面量类型的父类型,这样就可以让let变量重新赋值为相同父类型下的其他值。

8、 类型拓宽(仍需加强)

定义:所有通过 letvar 定义的变量、函数的形参、对象的非只读属性,如果满足指定了初始值且未显式添加类型注解的条件,那么它们推断出来的类型就是指定的初始值字面量类型拓宽后的类型,这就是字面量类型拓宽。

let str = 'this is string'; // 类型是 string
let strFun = (str = 'this is string') => str; // 类型是 (str?: string) => string;
const specifiedStr = 'this is string'; // 类型是 'this is string'
let str2 = specifiedStr; // 类型是 'string'
let strFun2 = (str = specifiedStr) => str; // 类型是 (str?: string) => string;

代码解析:
    因为第 1~2 行满足了 let、形参且未显式声明类型注解的条件,所以变量、形参的类型拓宽为 string(形参类型确切地讲是 string | undefined)。
因为第 3 行的常量不可变更,类型没有拓宽,所以 specifiedStr 的类型是 'this is string' 字面量类型。
第 4~5 行,因为赋予的值 specifiedStr 的类型是字面量类型,且没有显式类型注解,所以变量、形参的类型也被拓宽了。其实,这样的设计符合实际编程诉求。我们设想一下,如果 str2 的类型被推断为 'this is string',它将不可变更,因为赋予任何其他的字符串类型的值都会提示类型错误。

    对 nullundefined 的类型进行拓宽,通过 letvar 定义的变量如果满足未显式声明类型注解且被赋予了 nullundefined 值,则推断出这些变量的类型是 any:
{
  let x = null; // 类型拓宽成 any
  let y = undefined; // 类型拓宽成 any

  /** -----分界线------- */
  const z = null; // 类型是 null

  /** -----分界线------- */
  let anyFun = (param = null) => param; // 形参类型是 null
  let z2 = z; // 类型是 null
  let x2 = x; // 类型是 null
  let y2 = y; // 类型是 undefined
}
注意:在严格模式下,一些比较老的版本中(2.0nullundefined 并不会被拓宽成“any”。

    TypeScript 提供了一些控制拓宽过程的方法。其中一种方法是使用 const。如果用 const 而不是 let 声明一个变量,那么它的类型会更窄。

覆盖 TS的默认行为:
一种是提供显式类型注释:
// Type is { x: 1 | 3 | 5; }
const obj: { x1 | 3 | 5 } = {
  x1 
};

另一种方法是使用 const 断言:
// Type is { x: number; y: number; }
const obj1 = { 
  x1, 
  y2 
}; 

// Type is { x: 1; y: number; }
const obj2 = {
  x1 as const,
  y2,
}; 

// Type is { readonly x: 1; readonly y: 2; }
const obj3 = {
  x1, 
  y2 
} as const;

9、类型缩小(仍需加强)

1、使用类型守卫,将函数参数的类型从 any 缩小到明确的类型:
{
  let func = (anything: any) => {
    if (typeof anything === 'string') {
      return anything; // 类型是 string 
    } else if (typeof anything === 'number') {
      return anything; // 类型是 number
    }
    return null;
  };
}

2、使用类型守卫将联合类型缩小到明确的子类型:
{
  let func = (anything: string | number) => {
    if (typeof anything === 'string') {
      return anything; // 类型是 string 
    } else {
      return anything; // 类型是 number
    }
  };
}

3、通过字面量类型等值判断(===)或其他控制流语句(包括但不限于 if、三目运算符、switch 分支)将联合类型收敛为更具体的类型:
{
  type Goods = 'pen' | 'pencil' |'ruler';
  const getPenCost = (item: 'pen') => 2;
  const getPencilCost = (item: 'pencil') => 4;
  const getRulerCost = (item: 'ruler') => 6;
  const getCost = (item: Goods) =>  {
    if (item === 'pen') {
      return getPenCost(item); // item => 'pen'
    } else if (item === 'pencil') {
      return getPencilCost(item); // item => 'pencil'
    } else {
      return getRulerCost(item); // item => 'ruler'
    }
  }
}
补充:事实上,如果我们将上面的示例去掉中间的流程分支,编译器也可以推断出收敛后的类型
  const getCost = (item: Goods) =>  {
    if (item === 'pen') {
      item; // item => 'pen'
    } else {
      item; // => 'pencil' | 'ruler'
    }
  }

4、特殊值:
以下从联合类型中排除 null 的方法是错误的:
const el = document.getElementById("foo"); // Type is HTMLElement | null
if (typeof el === "object") {
  el; // Type is HTMLElement | null
}
因为在 JavaScripttypeof null 的结果是 "object" ,所以你实际上并没有通过这种检查排除 null 值

falsy 的原始值也会产生类似的问题:
function foo(x?: number | string | null) {
  if (!x) {
    x; // Type is string | number | null | undefined\
  }
}
因为空字符串和 0 都属于 falsy 值,所以在分支中 x 的类型可能是 stringnumber 类型

5、在它们上放置一个明确的 “标签”:(重点)
interface UploadEvent {
  type"upload";
  filenamestring;
  contentsstring;
}

interface DownloadEvent {
  type"download";
  filenamestring;
}

type AppEvent = UploadEvent | DownloadEvent;

function handleEvent(e: AppEvent) {
  switch (e.type) {
    case "download":
      e; // Type is DownloadEvent 
      break;
    case "upload":
      e; // Type is UploadEvent 
      break;
  }
}
这种模式也被称为 ”标签联合“ 或 ”可辨识联合“,它在 TypeScript 中的应用范围非常广。实际上就是开发者手动的“分割类型”,让编译器更好的判断类型。

10、联合类型

1、      表示取值可以为多种类型中的一种,使用 | 分隔每个类型:let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven'; // OK
myFavoriteNumber = 7; // OK


2、      通常与 null 或 undefined 一起使用:const sayHello = (name: string | undefined) => {
  /* ... */
};
sayHello("semlinker"); 
sayHello(undefined);


3、      类型 A 和类型 B 联合后的类型是同时接受 A 和 B 值的类型:let num: 1 | 2 = 1;
type EventNames = 'click' | 'scroll' | 'mousemove';

11、类型别名

用来给一个类型起个新名字。类型别名常用于联合类型。
type Message = string | string[];
let greet = (message: Message) => {
  // ...
};

12、交叉类型

// 定义:将多个类型合并为一个类型。 
1、让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性,使用&定义交叉类型。
{
  type Useless = string & number;
}
注意:交叉类型真正的用武之地就是将多个接口类型合并成一个类型,从而实现等同接口继承的效果,也就是所谓的合并接口类型,如下代码所示:
交叉类型真正的用武之地就是将多个接口类型合并成一个类型,从而实现等同接口继承的效果,也就是所谓的合并接口类型,如下代码所示:

思考一下:如果合并的多个接口类型存在同名属性会是什么效果呢?
如果同名属性的类型不兼容,比如上面示例中两个接口类型同名的 name 属性类型一个是 number,另一个是 string,合并后,name 属性的类型就是 number 和 string 两个原子类型的交叉类型,即 never,如下代码所示:
  type IntersectionTypeConfict = { id: number; name: string; } 
  & { age: number; name: number; };
  const mixedConflict: IntersectionTypeConfict = {
    id: 1,
    name: 2, // ts(2322) 错误,'number' 类型不能赋给 'never' 类型
    age: 2
  };
如果同名属性的类型兼容,比如一个是 number,另一个是 number 的子类型、数字字面量类型,合并后 name 属性的类型就是两者中的子类型。如下所示示例中 name 属性的类型就是数字字面量类型 2,因此,我们不能把任何非 2 之外的值赋予 name 属性。
  type IntersectionTypeConfict = { id: number; name: 2; } 
  & { age: number; name: number; };

  let mixedConflict: IntersectionTypeConfict = {
    id: 1,
    name: 2, // ok
    age: 2
  };
  mixedConflict = {
    id: 1,
    name: 22, // '22' 类型不能赋给 '2' 类型
    age: 2
  };
如果同名属性是非基本数据类型的话,又会是什么情形:
interface A {
  x:{d:true},
}
interface B {
  x:{e:string},
}
interface C {
  x:{f:number},
}
type ABC = A & B & C
let abc:ABC = {
  x:{
    d:true,
    e:'',
    f:666
  }
}
在混入多个类型时,若存在相同的成员,且成员类型为非基本数据类型,那么是可以成功合并。

13、接口(重点)

1、      是什么    对行为的抽象,而具体如何行动需要由类(classes)去实现(implement),除了可用于[对类的一部分行为进行抽象]以外,也常用于对「对象的形状(Shape)」进行描述。
接口一般首字母大写。定义的变量比接口少了或者多了一些属性是不允许的,所以赋值的时候,变量的形状必须和接口的形状保持一致。可以通过可选属性解决。


2、      可选 | 只读属性interface Person {
  readonly name: string;
  age?: number;
}
TypeScript 还提供了 ReadonlyArray<T> 类型,它与 Array<T> 相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改。


3、      任意属性    我们希望一个接口中除了包含必选和可选属性之外,还允许有其他的任意属性,这时我们可以使用 索引签名 的形式来满足上述要求。
interface Person {
  name: string;
  age?: number;
  [propName: string]: any;
}

let tom: Person = {
  name: 'Tom',
  gender: 'male'
};
注意:一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集。
interface Person {
  name: string;
  age?: number;
  [propName: string]: string;
}

let tom: Person = {
  name: 'Tom',
  age: 25,
  gender: 'male'
};

// index.ts(3,5): error TS2411: Property 'age' of type 'number' is not assignable to string index type 'string'.
// index.ts(7,5): error TS2322: Type '{ [x: string]: string | number; name: string; age: number; gender: string; }' is not assignable to type 'Person'.
//   Index signatures are incompatible.
//     Type 'string | number' is not assignable to type 'string'.
//       Type 'number' is not assignable to type 'string'.
上例中,任意属性的值允许是 string,但是可选属性 age 的值却是 number,number 不是 string 的子属性,所以报错了。

注意:一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型:
interface Person {
  name: string;
  age?: number; // 这里真实的类型应该为:number | undefined
  [propName: string]: string | number | undefined;
}

let tom: Person = {
  name: 'Tom',
  age: 25,
  gender: 'male'
};

4、鸭式辨型法(重点)
像鸭子一样走路并且嘎嘎叫的就叫鸭子,即具有鸭子特征的认为它就是鸭子,也就是通过制定规则来判定对象是否实现这个接口。
interface LabeledValue {
  label: string;
}
function printLabel(labeledObj: LabeledValue) {
  console.log(labeledObj.label);
}
let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj); // OK



interface LabeledValue {
  label: string;
}
function printLabel(labeledObj: LabeledValue) {
  console.log(labeledObj.label);
}
printLabel({ size: 10, label: "Size 10 Object" }); // Error

上面代码,在参数里写对象就相当于是直接给labeledObj赋值,这个对象有严格的类型定义,所以不能多参或少参。而当你在外面将该对象用另一个变量myObj接收,myObj不会经过额外属性检查,但会根据类型推论为let myObj: { size: number; label: string } = { size: 10, label: "Size 10 Object" };,然后将这个myObj再赋值给labeledObj,此时根据类型的兼容性,两种类型对象,参照鸭式辨型法,因为都具有label属性,所以被认定为两个相同,故而可以用此法来绕开多余的类型检查。


4、      绕开额外属性检查的方式(重点)a.鸭式辨型法
同上。

b.类型断言
interface Props { 
  name: string; 
  age: number; 
  money?: number;
}

let p: Props = {
  name: "兔神",
  age: 25,
  money: -100000,
  girl: false
} as Props; // OK

c.索引签名
interface Props { 
  name: string; 
  age: number; 
  money?: number;
  [key: string]: any;
}

let p: Props = {
  name: "兔神",
  age: 25,
  money: -100000,
  girl: false
}; // OK

6、接口与类型别名的区别
在大多数的情况下使用接口类型和类型别名的效果等价,但是在某些特定的场景下这两者还是存在很大区别。

a.      TypeScript 的核心原则之一是对值所具有的结构进行类型检查。 而接口的作用就是为这些类型命名和为你的代码或第三方代码定义数据模型。
b.      type(类型别名)会给一个类型起个新名字。 type 有时和 interface 很像,但是可以作用于原始值(基本类型),联合类型,元组以及其它任何你需要手写的类型。起别名不会新建一个类型 - 它创建了一个新 名字来引用那个类型。给基本类型起别名通常没什么用,尽管可以做为文档的一种形式使用。
  7、Objects / Functions
  两者都可以用来描述对象或函数的类型,但是语法不同。
  interface Point {
  x: number;
  y: number;
  }

  interface SetPoint {
  (x: number, y: number): void;
  }

  type Point = {
  x: number;
  y: number;
  };

  type SetPoint = (x: number, y: number) => void;

  8、Other Types(重点)
  与接口不同,类型别名还可以用于其他类型,如基本类型(原始值)、联合类型、元组。
  // primitive
  type Name = string;

  // object
  type PartialPointX = { x: number; };
  type PartialPointY = { y: number; };

  // union
  type PartialPoint = PartialPointX | PartialPointY;

  // tuple
  type Data = [number, string];

  // dom
  let div = document.createElement('div');
  type B = typeof div;

  9、接口可以定义多次,类型别名不可以
  interface Point { x: number; }
  interface Point { y: number; }
  const point: Point = { x: 1, y: 2 };

  10、拓展
  两者的扩展方式不同,但并不互斥。接口可以扩展类型别名,同理,类型别名也可以扩展接口。
  接口的扩展就是继承,通过 extends 来实现。类型别名的扩展就是交叉类型,通过 & 来实现。
  1.接口扩展接口
  interface PointX {
  x: number
  }

  interface Point extends PointX {
  y: number
  }
  2.类型别名扩展类型别名

  type PointX = {
  x: number
  }

  type Point = PointX & {
  y: number
  }
  3.接口扩展类型别名
  type PointX = {
  x: number
  }
  interface Point extends PointX {
  y: number
  }
  4.类型别名扩展接口
  interface PointX {
  x: number
  }
  type Point = PointX & {
  y: number
  }

14、泛型(重点,难点)

1、定义:T 是一个抽象类型,只有在调用的时候才确定它的值。
function identity<T>(arg: T): T {
  return arg;
}
其中 T 代表 Type,在定义泛型时通常用作第一个类型变量名称。但实际上 T 可以用任何有效名称代替。除了 T 之外,以下是常见泛型变量代表的意思:
K(Key):表示对象中的键类型;
V(Value):表示对象中的值类型;
E(Element):表示元素类型。

其实并不是只能定义一个类型变量,我们可以引入希望定义的任何数量的类型变量。比如我们引入一个新的类型变量 U,用于扩展我们定义的 identity 函数:
function identity <T, U>(value: T, message: U) : T {
  console.log(message);
  return value;
}
console.log(identity<Number, string>(68, "Semlinker"));

除了为类型变量显式设定值之外,一种更常见的做法是使编译器自动选择这些类型,从而使代码更简洁。我们可以完全省略尖括号,比如:
function identity <T, U>(value: T, message: U) : T {
  console.log(message);
  return value;
}
console.log(identity(68, "Semlinker"));

2、泛型约束
错误代码:
function trace<T>(arg: T): T {
  console.log(arg.size); // Error: Property 'size doesn't exist on type 'T'
  return arg;
}
报错的原因在于 T 理论上是可以是任何类型的,不同于 any,你不管使用它的什么属性或者方法都会报错(除非这个属性和方法是所有集合共有的)。那么直观的想法是限定传给 trace 函数的参数类型应该有 size 类型,这样就不会报错了。
如何解决?使用 extends 关键字可以做到这一点。简单来说就是你定义一个类型,然后让 T 实现这个接口即可。
interface Sizeable {
  size: number;
}
function trace<T extends Sizeable>(arg: T): T {
  console.log(arg.size);
  return arg;
}

3、泛型工具类型
a: typeof
  在类型上下文中获取变量或者属性的类型
interface Person {
  name: string;
  age: number;
}
const sem: Person = { name: "semlinker", age: 30 };
type Sem = typeof sem; // type Sem = Person

b: keyof
在 TypeScript 2.1 版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型。
interface Person {
  name: string;
  age: number;
}

type K1 = keyof Person; // "name" | "age"
type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join" 
type K3 = keyof { [x: string]: Person };  // string | number

interface StringArray {
  // 字符串索引 -> keyof StringArray => string | number
  [index: string]: string; 
}

interface StringArray1 {
  // 数字索引 -> keyof StringArray1 => number
  [index: number]: string;
}
为了同时支持两种索引类型,就得要求数字索引的返回值必须是字符串索引返回值的子类。其中的原因就是当使用数值索引时,JavaScript 在执行索引操作时,会先把数值索引先转换为字符串索引。所以 keyof { [x: string]: Person } 的结果会返回 string | number。

支持基本类型::
let K1: keyof boolean; // let K1: "valueOf"
let K2: keyof number; // let K2: "toString" | "toFixed" | "toExponential" | ...
let K3: keyof symbol; // let K1: "valueOf"

作用:(重点)
错误代码:
function prop(obj: object, key: string) {
  return obj[key];
}
正确代码:
function prop<T extends object, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}
代码解析:首先定义了 T 类型并使用 extends 关键字约束该类型必须是 object 类型的子类型,然后使用 keyof 操作符获取 T 类型的所有键,其返回类型是联合类型,最后利用 extends 关键字约束 K 类型必须为 keyof T 联合类型的子类型。
具体使用:
type Todo = {
  id: number;
  text: string;
  done: boolean;
}

const todo: Todo = {
  id: 1,
  text: "Learn TypeScript keyof",
  done: false
}

function prop<T extends object, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

const id = prop(todo, "id"); // const id: number
const text = prop(todo, "text"); // const text: string
const done = prop(todo, "done"); // const done: boolean

c: in
用来遍历枚举类型:
type Keys = "a" | "b" | "c"

type Obj =  {
  [p in Keys]: any
} // -> { a: any, b: any, c: any }

d: infer
在条件类型语句中,可以用 infer 声明一个类型变量并且对它进行使用。
type ReturnType<T> = T extends (
  ...args: any[]
  ) => infer R ? R : any;
  以上代码中 infer R 就是声明一个变量来承载传入函数签名的返回值类型,简单说就是用它取到函数返回值的类型方便之后使用。

  e: extends
  我们定义的泛型不想过于灵活或者说想继承某些类等,可以通过 extends 关键字添加泛型约束。
  interface Lengthwise {
  length: number;
  }

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

  4、索引类型(难点)
  错误代码:
  let person = {
  name: 'musion',
  age: 35
  }

  function getValues(person: any, keys: string[]) {
  return keys.map(key => person[key])
  }

  console.log(getValues(person, ['name', 'age'])) // ['musion', 35]
  console.log(getValues(person, ['gender'])) // [undefined]
  在上述例子中,可以看到getValues(persion, ['gender'])打印出来的是[undefined],但是ts编译器并没有给出报错信息,那么如何使用ts对这种模式进行类型约束呢?这里就要用到了索引类型,改造一下getValues函数,通过 索引类型查询和 索引访问 操作符:

  function getValues<T, K extends keyof T>(person: T, keys: K[]): T[K][] {
  return keys.map(key => person[key]);
  }

  interface Person {
  name: string;
  age: number;
  }

  const person: Person = {
  name: 'musion',
  age: 35
  }

  getValues(person, ['name']) // ['musion']
  getValues(person, ['gender']) // 报错:
  // Argument of Type '"gender"[]' is not assignable to parameter of type '("name" | "age")[]'.
  // Type "gender" is not assignable to type "name" | "age".
  代码解析:
  T[K]表示对象T的属性K所表示的类型,在上述例子中,T[K][] 表示变量T取属性K的值的数组

  // 通过[]索引类型访问操作符, 我们就能得到某个索引的类型
  class Person {
  name:string;
  age:number;
  }
  type MyType = Person['name'];  //Person中name的类型为string type MyType = string
  首先看泛型,这里有T和K两种类型,根据类型推断,第一个参数person就是person,类型会被推断为Person。而第二个数组参数的类型推断(K extends keyof T),keyof关键字可以获取T,也就是Person的所有属性名,即['name', 'age']。而extends关键字让泛型K继承了Person的所有属性名,即['name', 'age']。这三个特性组合保证了代码的动态性和准确性,也让代码提示变得更加丰富了

  5、映射类型
  定义:根据旧的类型创建出新的类型, 我们称之为映射类型。
  首先定义一个接口:
  interface TestInterface{
  name:string,
  age:number
  }
  把上面定义的接口里面的属性全部变成可选:
  // 我们可以通过+/-来指定添加还是删除

  type OptionalTestInterface<T> = {
  +readonly [p in keyof T]+?:T[p]
  }

  type newTestInterface = OptionalTestInterface<TestInterface>
  // type newTestInterface = {
  //    name?:string,
  //    age?:number
  // }
  由于生成只读属性和可选属性比较常用, 所以TS内部已经给我们提供了现成的实现 Readonly / Partial。

  6、Partial
  定义:Partial<T> 将类型的属性变成可选
  type Partial<T> = {
  [P in keyof T]?: T[P];
  };

  举例子:
  interface UserInfo {
  id: string;
  name: string;
  }
  // error:Property 'id' is missing in type '{ name: string; }' but required in type 'UserInfo'
  const xiaoming: UserInfo = {
  name: 'xiaoming'
  }
  使用 Partial<T>
  type NewUserInfo = Partial<UserInfo>;
  const xiaoming: NewUserInfo = {
  name: 'xiaoming'
  }
  这个  NewUserInfo 就相当于:
  interface NewUserInfo {
  id?: string;
  name?: string;
  }
  注意:只支持处理第一层的属性,但是 DeepPartial 支持嵌套。
  type DeepPartial<T> = {
  // 如果是 object,则递归类型
  [U in keyof T]?: T[U] extends object
  ? DeepPartial<T[U]>
  : T[U]
  };

  type PartialedWindow = DeepPartial<T>; // 现在T上所有属性都变成了可选啦

  7、Required
  定义:将类型的属性变成必选。
  type Required<T> = { 
  [P in keyof T]-?: T[P] 
  };
  其中 -? 是代表移除 ? 这个 modifier 的标识。再拓展一下,除了可以应用于 ? 这个 modifiers ,还有应用在 readonly ,比如 Readonly<T> 这个类型
  type Readonly<T> = {
  readonly [p in keyof T]: T[p];
  }


  8、Readonly
  定义:Readonly<T> 的作用是将某个类型所有属性变为只读属性,也就意味着这些属性不能被重新赋值。
  type Readonly<T> = {
  readonly [P in keyof T]: T[P];
  };

  9、Pick
  定义:从某个类型中挑出一些属性出来。
  type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
  };
  举例子:
  interface Todo {
  title: string;
  description: string;
  completed: boolean;
  }

  type TodoPreview = Pick<Todo, "title" | "completed">;

  const todo: TodoPreview = {
  title: "Clean room",
  completed: false,
  };

  10、Record(重点)
  定义:Record<K extends keyof any, T> 的作用是将 K 中所有的属性的值转化为 T 类型。
  type Record<K extends keyof any, T> = {
  [P in K]: T;
  };

  举例子:
  interface PageInfo {
  title: string;
  }

  type Page = "home" | "about" | "contact";

  const x: Record<Page, PageInfo> = {
  home: { title: "home" },
  about: { title: "about" },
  contact: { title: "contact" },
  };

  11、ReturnType
  定义:用来得到一个函数的返回值类型
  type ReturnType<T extends (...args: any[]) => any> = T extends (
  ...args: any[]
  ) => infer R
  ? R
  : any;

  12、Exclude
  定义:Exclude<T, U> 的作用是将某个类型中属于另一个的类型移除掉。
  type Exclude<T, U> = T extends U ? never : T;
  举例子:
  type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
  type T1 = Exclude<"a" | "b" | "c", "a" | "b">; // "c"
  type T2 = Exclude<string | number | (() => void), Function>; // string | number

  13、Extract
  定义:Extract<T, U> 的作用是从 T 中提取出 U。
  type Extract<T, U> = T extends U ? T : never;
  举例子:
  type T0 = Extract<"a" | "b" | "c", "a" | "f">; // "a"
  type T1 = Extract<string | number | (() => void), Function>; // () =>void

  14、Omit
  定义:Omit<T, K extends keyof any> 的作用是使用 T 类型中除了 K 类型的所有属性,来构造一个新的类型。
  type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
  举例子:
  interface Todo {
  title: string;
  description: string;
  completed: boolean;
  }

  type TodoPreview = Omit<Todo, "description">;

  const todo: TodoPreview = {
  title: "Clean room",
  completed: false,
  };

  15、NonNullable
  定义:NonNullable<T> 的作用是用来过滤类型中的 null 及 undefined 类型。
  type NonNullable<T> = T extendsnull | undefined ? never : T;
  举例子:
  type T0 = NonNullable<string | number | undefined>; // string | number
  type T1 = NonNullable<string[] | null | undefined>; // string[]

  16、Parameters<T> 的作用是用于获得函数的参数类型组成的元组类型。
  定义:type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any
  ? P : never;
  举例子:
  type A = Parameters<() =>void>; // []
  type B = Parameters<typeofArray.isArray>; // [any]
  type C = Parameters<typeofparseInt>; // [string, (number | undefined)?]
  type D = Parameters<typeofMath.max>; // number[]

15、tsconfig.json(重点)

tsconfig.json 是 TypeScript 项目的配置文件。如果一个目录下存在一个 tsconfig.json 文件,那么往往意味着这个目录就是 TypeScript 项目的根目录。

tsconfig.json 包含 TypeScript 编译的相关配置,通过更改编译配置项,我们可以让 TypeScript 编译出 ES6、ES5、node 的代码。

注意:tsconfig.json 重要字段

● files - 设置要编译的文件的名称;

● include - 设置需要进行编译的文件,支持路径模式匹配;

● exclude - 设置无需进行编译的文件,支持路径模式匹配;

● compilerOptions - 设置与编译流程相关的选项。

compilerOptions 选项、:

{
  "compilerOptions": {

    /* 基本选项 */
    "target": "es5",                       // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
    "module": "commonjs",                  // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
    "lib": [],                             // 指定要包含在编译中的库文件
    "allowJs": true,                       // 允许编译 javascript 文件
    "checkJs": true,                       // 报告 javascript 文件中的错误
    "jsx": "preserve",                     // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
    "declaration": true,                   // 生成相应的 '.d.ts' 文件
    "sourceMap": true,                     // 生成相应的 '.map' 文件
    "outFile": "./",                       // 将输出文件合并为一个文件
    "outDir": "./",                        // 指定输出目录
    "rootDir": "./",                       // 用来控制输出目录结构 --outDir.
    "removeComments": true,                // 删除编译后的所有的注释
    "noEmit": true,                        // 不生成输出文件
    "importHelpers": true,                 // 从 tslib 导入辅助工具函数
    "isolatedModules": true,               // 将每个文件做为单独的模块 (与 'ts.transpileModule' 类似).

    /* 严格的类型检查选项 */
    "strict": true,                        // 启用所有严格类型检查选项
    "noImplicitAny": true,                 // 在表达式和声明上有隐含的 any类型时报错
    "strictNullChecks": true,              // 启用严格的 null 检查
    "noImplicitThis": true,                // 当 this 表达式值为 any 类型的时候,生成一个错误
    "alwaysStrict": true,                  // 以严格模式检查每个模块,并在每个文件里加入 'use strict'

    /* 额外的检查 */
    "noUnusedLocals": true,                // 有未使用的变量时,抛出错误
    "noUnusedParameters": true,            // 有未使用的参数时,抛出错误
    "noImplicitReturns": true,             // 并不是所有函数里的代码都有返回值时,抛出错误
    "noFallthroughCasesInSwitch": true,    // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)

    /* 模块解析选项 */
    "moduleResolution": "node",            // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
    "baseUrl": "./",                       // 用于解析非相对模块名称的基目录
    "paths": {},                           // 模块名到基于 baseUrl 的路径映射的列表
    "rootDirs": [],                        // 根文件夹列表,其组合内容表示项目运行时的结构内容
    "typeRoots": [],                       // 包含类型声明的文件列表
    "types": [],                           // 需要包含的类型声明文件名列表
    "allowSyntheticDefaultImports": true,  // 允许从没有设置默认导出的模块中默认导入。

    /* Source Map Options */
    "sourceRoot": "./",                    // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
    "mapRoot": "./",                       // 指定调试器应该找到映射文件而不是生成文件的位置
    "inlineSourceMap": true,               // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
    "inlineSources": true,                 // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性

    /* 其他选项 */
    "experimentalDecorators": true,        // 启用装饰器
    "emitDecoratorMetadata": true          // 为装饰器提供元数据的支持
  }
}

16、技巧(重点)

1、尽量减少重复代码
垃圾代码:
interface Person {
  firstNamestring;
  lastNamestring;
}

interface PersonWithBirthDate {
  firstNamestring;
  lastNamestring;
  birthDate;
}
牛逼代码:
interface Person { 
  firstName: string; 
  lastName: string;
}

interface PersonWithBirthDate extends Person { 
  birth: Date;
}
type PersonWithBirthDate = Person & { birthDate };

有时候你可能还会发现自己想要定义一个类型来匹配一个初始配置对象的「形状」,比如:
const INIT_OPTIONS = {
  width: 640,
  height: 480,
  color: "#00FF00",
  label: "VGA",
};

interface Options {
  width: number;
  height: number;
  color: string;
  label: string;
}
对于 Options 接口来说,你也可以使用 typeof 操作符来快速获取配置对象的「形状」:
type Options = typeof INIT_OPTIONS;

有时它们会被语法所掩盖。比如有多个函数拥有相同的类型签名:
function get(url: string, opts: Options): Promise<Response> { /* ... */ } 
function post(url: string, opts: Options): Promise<Response> { /* ... */ }
对于上面的代码你可以提取统一的类型签名:
type HTTPFunction = (url: string, opts: Options) => Promise<Response>; 
const getHTTPFunction = (url, opts) => { /* ... */ };
const postHTTPFunction = (url, opts) => { /* ... */ };

2、使用更精确的类型替代字符串类型
垃圾代码:
interface Album {
  artiststring// 艺术家
  titlestring// 专辑标题
  releaseDatestring// 发行日期:YYYY-MM-DD
  recordingTypestring// 录制类型:"live" 或 "studio"
}
const dangerousAlbum = {
  artist"Michael Jackson",
  title"Dangerous",
  releaseDate"November 31, 1991"// 与预期格式不匹配
  recordingType"Studio"// 与预期格式不匹配
};

虽然 releaseDate 和 recordingType 的值与预期的格式不匹配,但此时 TypeScript 编译器并不能发现该问题。为了解决这个问题,你应该为 releaseDate 和 recordingType 属性定义更精确的类型,比如这样:
interface Album {\
  artiststring// 艺术家
  titlestring// 专辑标题
  releaseDateDate// 发行日期:YYYY-MM-DD
  recordingType"studio" | "live"// 录制类型:"live" 或 "studio"
}

重新定义 Album 接口之后,对于前面的赋值语句,TypeScript 编译器就会提示以下异常信息:
const dangerousAlbum = {
  artist"Michael Jackson",
  title"Dangerous",
  // 不能将类型“string”分配给类型“Date”。ts(2322)
  releaseDate"November 31, 1991"// Error
  // 不能将类型“"Studio"”分配给类型“"studio" | "live"”。ts(2322)\
  recordingType"Studio"// Error
};

为了解决上面的问题,需要为 releaseDate 和 recordingType 属性设置正确的类型,比如这样:
const dangerousAlbum = {
  artist"Michael Jackson",
  title"Dangerous",
  releaseDatenew Date("1991-11-31"),
  recordingType"studio",
};

3、定义的类型总是表示有效的状态
假设正在构建一个允许用户指定页码,然后加载并显示该页面对应内容的 Web 应用程序。首先,你可能会先定义 State 对象:
垃圾代码:
interface State {
  pageContentstring;
  isLoadingboolean;
  errorMsg?: string;
}
function renderPage(state: State) {
  if (state.errorMsg) {
    return `呜呜呜,加载页面出现异常了...${state.errorMsg}`;
  } else if (state.isLoading) {
    return `页面加载中~~~`;
  }
  return `<div>${state.pageContent}</div>`;
}

// 输出结果:页面加载中~~~
console.log(renderPage({isLoading: true, pageContent: ""}));
// 输出结果:<div>大家好</div>
console.log(renderPage({isLoading: false, pageContent: "大家好呀"}));

创建好 renderPage 函数,你可以继续定义一个 changePage 函数,用于根据页码获取对应的页面数据:
async function changePage(state: State, newPage: string) {
  state.isLoading = true;
  try {
    const response = await fetch(getUrlForPage(newPage));
    if (!response.ok) {
      throw new Error(`Unable to load ${newPage}: ${response.statusText}`);
      }
      const text = await response.text();
      state.isLoading = false;
      state.pageContent = text;
      } catch (e) {
      state.errorMsg = "" + e;
      }
      }
      对于以上的 changePage 函数,它存在以下问题:
      1、在 catch 语句中,未把 state.isLoading 的状态设置为 false2、未及时清理 state.errorMsg 的值,因此如果之前的请求失败,那么你将继续看到错误消息,而不是加载消息。

      出现上述问题的原因是,前面定义的 State 类型允许同时设置 isLoading 和 errorMsg 的值,尽管这是一种无效的状态。针对这个问题,你可以考虑引入可辨识联合类型来定义不同的页面请求状态:
      interface RequestPending {
      state: "pending";
      }

      interface RequestError {
      state: "error";
      errorMsg: string;
      }

      interface RequestSuccess {
      state: "ok";
      pageContent: string;
      }

      type RequestState = RequestPending | RequestError | RequestSuccess;

      interface State {
      currentPage: string;
      requests: { [page: string]: RequestState };
      }
      在以上代码中,通过使用可辨识联合类型分别定义了 3 种不同的请求状态,这样就可以很容易的区分出不同的请求状态,从而让业务逻辑处理更加清晰。接下来,需要基于更新后的 State 类型,来分别更新一下前面创建的 renderPage 和 changePage 函数:
      function renderPage(state: State) {
      const { currentPage } = state;
      const requestState = state.requests[currentPage];
      switch (requestState.state) {
      case "pending":
      return `页面加载中~~~`;
      case "error":
      return `呜呜呜,加载第${currentPage}页出现异常了...${requestState.errorMsg}`;
      case "ok":
      `<div>第${currentPage}页的内容:${requestState.pageContent}</div>`;
      }
      }
      async function changePage(state: State, newPage: string) {
      state.requests[newPage] = { state: "pending" };
      state.currentPage = newPage;
      try {
      const response = await fetch(getUrlForPage(newPage));
      if (!response.ok) {
      throw new Error(`无法正常加载页面 ${newPage}: ${response.statusText}`);
      }
      const pageContent = await response.text();
      state.requests[newPage] = { state: "ok", pageContent };
      } catch (e) {
      state.requests[newPage] = { state: "error", errorMsg: "" + e };
      }
      }

      在 changePage 函数中,会根据不同的情形设置不同的请求状态,而不同的请求状态会包含不同的信息。这样 renderPage 函数就可以根据统一的 state 属性值来进行相应的处理。因此,通过使用可辨识联合类型,让请求的每种状态都是有效的状态,不会出现无效状态的问题。