# TypeScript 基础

# 使用ts原因

在编写阶段就可以检测出代码的大部分问题,实现的一种静态检查的类型的工具。

一个静态类型需要标记出哪些代码是一个错误,哪怕实际生效的js并不会立刻报错。

export const user = {
  name: "Daniel",
  age: 26,
};
user.love ='money'// 类型“{ name: string; age: number; }”上不存在属性“love”。
  • 比如拼写错误:
const announcement = "Hello World!";
 
// How quickly can you spot the typos?
announcement.toLocaleLowercase();
 
// We probably meant to write this...
announcement.toLocaleLowerCase();
  • 函数未被调用:
function flipCoin() {
  // Meant to be Math.random()
  return Math.random < 0.5;
// Operator '<' cannot be applied to types '() => number' and 'number'.
}
  • 基本的逻辑错误:
const value = Math.random() < 0.5 ? "a" : "b";
if (value !== "a") {
  // ...
} else if (value === "b") { //此条件将始终返回 "false",因为类型 ""a"" 和 ""b"" 没有重叠。
  // This condition will always return 'false' since the types '"a"' and '"b"' have no overlap.
}

这些都可提前发现。

# ts变量声明&&类型注解&&类型推断

变量是一种使用方便的占位符,用于引用计算机内存地址。变量声明语法:冒号 : 前面是变量名称,后面是变量类型。

  • 声明变量的类型及初始值,声明了类型后TypeScript就会进行类型检测,声明的类型可以称之为类型注解;
var/let/const 标识符: 数据类型 = 赋值;
// var [变量名] : [类型] = 值;//:后注解类型
var uname:string = "Runoob";

TS的const声明必须初始化赋值

let count: number;//类型注解,先不赋值,之后赋值需遵从数据定义的类型
count = 1;
let countInference = 123;//类型推论,如明确知道,可省略让ts自己判断

# 类型断言

TS允许覆盖它的推断.类型断言主要用于当TS推断出来类型并不满足需求,需要手动指定一个类型。

有使用关键字 as 和标签 <> 两种方式,因后者会与JSX 语法冲突,优先使用 as 来进行类型断言。

interface User {
    nickname: string,
    admin: boolean,
    groups: number[]
  }
  
  const user = {} as User //这里通过 as 关键字进行类型断言,将变量 user 的类型覆盖为 User 类型
  
  //const user = <User>{} // User类型 使用 <User>{} 这种标签形式,将变量 user 强制断言为 User 类型
  user.nickname = 'Evan' 
  user.admin = true       
  user.groups = [2, 6]
  
/**
 * 类型断言 “尖括号”语法
 */
let strLength: number = (<Array<number>>someValue).length;
let strLength1: number = (<string[]>someValue).length;
// let strLength2: number = (someValue as string[]).length;

有时候TypeScript无法获取具体的类型信息,需要使用类型断言(Type Assertions)。比如通过 document.getElementById,TypeScript只知道该函数会返回 HTMLElement ,但并不知道它具体的类型:

const el = document.getElementById("why") as HTMLImageElement
el.src = "url地址"
# 双重断言

双重断言极少有应用场景

interface User {
  nickname: string,
  admin: boolean,
  group: number[]
}

const user = 'Evan' as any as User

typeScript 仅仅允许类型断言转换为一个 更加具体或者更不具体 的类型。这个规则可以阻止一些不可能的强制类型转换,比如:

const x = "hello" as number;
// Conversion of type 'string' to type 'number' may be a mistake because neither type
// sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.

有的时候,这条规则会显得非常保守,阻止了原本有效的类型转换。如果发生了这种事情,可以使用双重断言,先断言为 any (或者是 unknown),然后再断言为期望的类型:

export let t1 =('jjj' as any) as number
t1=40
console.log(t1)

非必须不要这样操作!

# 非空断言 !

如果编译器不能够去除 null 或 undefined,可以使用非空断言 ! 手动去除。

function fixed(name: string | null): string {
    function postfix(epithet: string) { 
// postfix() 是一个嵌套函数,因为编译器无法去除嵌套函数的 null (除非是立即调用的函数表达式),
// 所以 TypeScript 推断第 3 行的 name 可能为空。
      return name!.charAt(0) + '.  the ' + epithet; // name 被断言为非空
    }
// 而 name = name || "Bob" 这行代码已经明确了 name 不为空,所以可以直接给 name 断言为非空(第 3 行)
    name = name || "Bob"
    return postfix("great")
  }
	
  let s=fixed("ttt")
  console.log(s)//t.  the great

# TS数据类型

object可以用来声明对象,当然也可以用具体的对象类型来声明

let f:object ={}
let s:any;// √
let a:null

s.aaa //√

a //在赋值前使用了变量“a”。error

如果只声明不赋值,直接使用的话,当声明类型为undefined或者any时才可以,即使是null,也需要赋值

  • symbol:使用 Symbol() 创建新的 symbol 类型
const sym1: symbol = Symbol()
const sym2: symbol = Symbol('foo')
const sym3: symbol = Symbol('foo')

# TS undefined

在js中当定义了一个变量,但没有赋予任何值的时候,他就是undefined类型。

新建demo01.ts文件,下入下面代码:

//声明数值类型的变量age,但不予赋值
var age:number
console.log(age) //在赋值前使用了变量“age”。error

写完后保存代码,进行运行任务,然后生成demo01.js,在终端中使用node demo01.js来进行查看运行结果。控制台输出了undefined,跟预想的一样。这只是在tsc demo01.ts之后得到的js才生效。ts这样写会报异常,运行ts-node demo01.ts会报错。

# ts枚举值和反向映射

  • 枚举类型用于定义数值集合
enum Color {Red, Green, Blue};
let c: Color = Color.Blue;
console.log(c);    // 输出 2

枚举还可以自定义对应下标,如果设置的有字符串等,则之后的值都需要设置具体的索引值,否则会报错,如果为数字,如果之后的都没有设,则可以递增。如果多个索引一致取最后一个

enum Color {
    Red='21', 
    Green=3,
    Blue,
    Orange=3
};
console.log(Color[4])//Blue
console.log(Color.Orange)// 3 
console.log(Color.Green)// 3
console.log(Color[3])// Orange
  • 反向映射:就是指枚举的取值,不但可以正向的 Months.Jan 这样取值,也可以反向的 Months[1] 这样取值。
enum Months {
  Jan = 1,
  Feb,
  Mar,
  Apr
}
console.log(Months)
/*{
  '1': 'Jan',
  '2': 'Feb',
  '3': 'Mar',
  '4': 'Apr',
  Jan: 1,
  Feb: 2,
  Mar: 3,
  Apr: 4
}
*/
console.log(Months.Mar === 3) // true
// 那么反过来能取到 Months[3] 的值吗?
console.log(Months[3])  // 'Mar'
// 所以
console.log(Months.Mar === 3)     // true
console.log(Months[3] === 'Mar')  // true

var Months;
(function (Months) {
    Months[Months["Jan"] = 1] = "Jan";
    Months[Months["Feb"] = 2] = "Feb";
    Months[Months["Mar"] = 3] = "Mar";
    Months[Months["Apr"] = 4] = "Apr";
})(Months || (Months = {}));
console.log(Months);
console.log(Months.Mar === 3); // true
// 那么反过来能取到 Months[3] 的值吗?
console.log(Months[3]); // 'Mar'
// 所以
console.log(Months.Mar === 3); // true
console.log(Months[3] === 'Mar'); // true
  • 枚举合并:分开声明名称相同的枚举类型,会自动合并
enum Months {
  Jan = 1,
  Feb,
  Mar,
  Apr
}

enum Months {
  May = 5,
  Jun
}

console.log(Months.Apr) // 4
console.log(Months.Jun) // 6
// type Direction = "left" | "Right" | "Top" | "Bottom"

enum Direction {
  LEFT = "LEFT",
  RIGHT = "RIGHT",
  TOP = "TOP",
  BOTTOM = "BOTTOM"
}

let name: string = "abc"
let d: Direction = Direction.BOTTOM

function turnDirection(direction: Direction) {
  console.log(direction)
  switch (direction) {
    case Direction.LEFT:
      console.log("改变角色的方向向左")
      break;
    case Direction.RIGHT:
      console.log("改变角色的方向向右")
      break;
    case Direction.TOP:
      console.log("改变角色的方向向上")
      break;
    case Direction.BOTTOM:
      console.log("改变角色的方向向下")
      break;
    default:
      const foo: never = direction;
      break;
  }
}

turnDirection(Direction.LEFT)
turnDirection(Direction.RIGHT)
turnDirection(Direction.TOP)
turnDirection(Direction.BOTTOM)

export {}
  1. 字符串枚举成员不会生成反向映射。
  2. 枚举类型被编译成一个对象,它包含了正向映射( name -> value)和反向映射( value -> name)。
  3. 通过关键字 enum 来声明枚举类型。
  4. TypeScript 仅支持基于数字和字符串的枚举。
  5. 通过枚举类型编译后的结果,了解到其本质上就是 JavaScript 对象。

# TS never类型

  • never 是其它类型(包括 null 和 undefined)的子类型,代表从不会出现的值。这意味着声明为 never 类型的变量只能被 never 类型所赋值,在函数中它通常表现为抛出异常或无法执行到终止点(例如无限循环)
//抛出异常的函数表达式,其函数返回值类型为 never
function error(message:string): never {
    throw new Error(message)
}  

举个例子,当有一个union type:

interface Foo {
  type: 'foo'
}

interface Bar {
  type: 'bar'
}

type All = Foo | Bar

在 switch 当中判断 type,TS 是可以收窄类型的 (discriminated union):

function handleValue(val: All) {
  switch (val.type) {
    case 'foo':
      // 这里 val 被收窄为 Foo
      break
    case 'bar':
      // val 在这里是 Bar
      break
    default:
      // val 在这里是 never
      const exhaustiveCheck: never = val
      break
  }
}

注意在 default 里面把被收窄为 never 的 val 赋值给一个显式声明为 never 的变量。如果一切逻辑正确,那么这里应该能够编译通过。但是假如后来有一天别人改了 All 的类型:

type All = Foo | Bar | Baz

他忘记了在 handleValue 里面加上针对 Baz 的处理逻辑,这个时候在default branch 里面 val 会被收窄为 Baz,导致无法赋值给 never,产生一个编译错误。所以通过这个办法,可以确保 handleValue 总是穷尽 (exhaust) 了所有 All 的可能类型。

# ts类型保护(类型收窄)

类型保护就是让编译器缩小类型范围(类型收窄),在编译阶段规避掉一些不必要的错误,利用 typeof instanceof in 或者字面量等

class User {
  public nickname: string | undefined
  public group: number | undefined
}

class Log {
  public count: number = 10
  public keyword: string | undefined
}

function typeGuard(arg: User | Log) {
  if (arg instanceof User) {
    arg.count = 15 // Error, User 类型无此属性
  }

  if (arg instanceof Log) {
    arg.count = 15 // OK
  }
}

ts会要求做的事情,必须对每个联合的成员都是有效的。举个例子,如果有一个联合类型 string | number , 不能使用只存在 string 上的方法:

function printId(id: number | string) {
  console.log(id.toUpperCase());
   //类型“string | number”上不存在属性“toUpperCase”。
	//类型“number”上不存在属性“toUpperCase”。
}

解决方案是用代码收窄联合类型,就像你在 JavaScript 没有类型注解那样使用。当 TypeScript 可以根据代码的结构推断出一个更加具体的类型时,类型收窄就会出现。

举个例子,TypeScript 知道,对一个 string 类型的值使用 typeof 会返回字符串 "string":

function printId(id: number | string) {
  if (typeof id === "string") {
    console.log(id.toUpperCase());
  } else {
    console.log(id);
  }
}
  • 利用typeof:注意null这种特殊情况
function printAll(strs: string | string[] | null) {
  if (strs&&typeof strs === "object") {
    for (const s of strs) {
		  // Object is possibly 'null'.
      console.log(s);
    }
  } else if (typeof strs === "string") {
    console.log(strs);
  } else {
    // do nothing
  }
}
  • 真值收窄:可以在条件语句中使用任何表达式,比如 && 、||、! 等,举个例子,像 if 语句就不需要条件的结果总是 boolean 类型(注意:在基本类型上的真值检查很容易导致错误)
function printAll(strs: string | string[] | null) {
  if (strs) {
    if (typeof strs === "object") {
      for (const s of strs) {
        console.log(s);
      }
    } else if (typeof strs === "string") {
      console.log(strs);
    }
  }
}

DANGER

如果传入的是空字符串,真值检查判断为 false,就会进入错误的处理分支。

  • 等值收窄:使用 switch 语句和等值检查比如 === !== == != 去收窄类型。
  • in操作符收窄:in 操作符可以判断一个对象是否有对应的属性名
type Fish = { swim: () => void };
type Bird = { fly: () => void };
type Human = { swim?: () => void; fly?: () => void };
 
function move(animal: Fish | Bird | Human) {
  if ("swim" in animal) {
    animal; // (parameter) animal: Fish | Human
  } else {
    animal; // (parameter) animal: Bird | Human
  }
}
  • instanceof 收窄
function printTime(time: string | Date) {
  if (time instanceof Date) {
    //(parameter) time: Date
    console.log(time.toUTCString())
  } else {
    //(parameter) time: string
    console.log(time)
  }
}
  • 赋值语句收窄:TypeScript 可以根据赋值语句的右值,正确的收窄左值。
let x = Math.random()*10>5?100:'only';
x=1000
//x=true//不能将类型“boolean”分配给类型“string | number”。
  • 控制流分析:基于可达性(reachability) 的代码分析就叫做控制流分析(control flow analysis)。在遇到类型保护和赋值语句的时候,TypeScript 就是使用这样的方式收窄类型。
  • 类型判断式(类型谓词 type predicates):所谓 predicate 就是一个返回 boolean 值的函数。

Ts 中的 is 关键字,它被称为类型谓词,用来判断一个变量属于某个接口或类型

类型谓词

// 判断参数是否为string类型, 返回布尔值
function isString(s:unknown):boolean{
  return typeof s === 'string'
}

// 参数转为大写函数
// 直接使用转大写方法报错, str有可能是其他类型
function upperCase(str:unknown){
  str.toUpperCase()
  // 类型“unknown”上不存在属性“toUpperCase”。
}

// 判断参数是否为字符串,是在调用转大写方法
function ifUpperCase(str:unknown){

  if(isString(str)){
    str.toUpperCase()
    // (parameter) str: unknown
    // 报错:类型“unknown”上不存在属性“toUpperCase”
  }
}

此时,可以在判断是否为string类型的函数返回值类型使用is关键词(即类型谓词)

// 判断参数是否为string类型, 返回布尔值
function isString(s:unknown):s is string{
  return typeof s === 'string'
}

// 判断参数是否为字符串,是在调用转大写方法
function ifUpperCase(str:unknown){

  if(isString(str)){
    str.toUpperCase()
    // (parameter) str: string
  }
}

s is string不仅返回boolean类型判断参数s是不是string类型, 同时明确的string类型返回到条件为true的代码块中.

因此当判断条件为true, 即str为string类型时, 代码块中str类型也转为更明确的string类型

类型谓词的主要特点是:

  • 返回类型谓词,如 s is string;
  • 包含可以准确确定给定变量类型的逻辑语句,如 typeof s === 'string'。

利用类型谓词实际应用

// 接口 interfaceA
interface interfaceA {
  name: string;
  age: number;
}

// 接口 interfaceB
interface interfaceB {
  name: string;
  phone: number;
}

// 推断类型
const obj1 = { name: "andy", age: 2 };
// const obj1: {name: string;age: number;}

const obj2 = { name: "andy", phone: 2 };
// const obj2: {name: string;phone: number;}

// 创建数组
// arr1, 创建两个interfaceA[]数组, 数组每一项都是 obj1
const arr1 = new Array<interfaceA>(2).fill(obj1);
// const arr1: interfaceA[]

// arr2, 创建两个interfaceB[]数组, 数组每一项都是 obj2
const arr2 = new Array<interfaceB>(2).fill(obj2);
// const arr2: interfaceB[]

// 合并两种类型数组,
//  arr3类型就是一个联合数组
const arr3 = [...arr1, ...arr2];
// const arr3: (interfaceA | interfaceB)[]


const target = arr3[0];
// const target: interfaceA | interfaceB  

// Ok获取两个结构共有的属性
console.log(target.name);

// 获取两个接口不同的属性报错:
console.log(target.phone);
// 报错: 类型“interfaceA”上不存在属性“phone”

console.log(target.age);
// 报错:  类型“interfaceB”上不存在属性“age”

示例代码中我们定义了一个接口interfaceA,一个接口interfaceB,各自对应一个具体的数据类型,但有时需要将这2个数据数组进行合并,TypeScript会自动推断这个数据的类型为联合类型数组

const arr3: (interfaceA | interfaceB)[]

通过数组操作取出数组里的元素时,这个元素的类型其实也是这个联合类型,需要取值的时候,由于name属于interfaceA和interfaceB共有的,即交叉属性

// 类型“interfaceA | interfaceB”上不存在属性“phone”。
// 类型“interfaceA”上不存在属性“phone”。ts(2339)
  1. 解决方案一:将所有非交叉属性设置 为可选属性:这种方式也不是特别好, 因为对于interfaceB来说,可能phone属性就是必选的,定义成可选属性是一种逃避,且不安全
// 接口 interfaceA
interface interfaceA {
  name: string;
  age?: number;
}

// 接口 interfaceB
interface interfaceB {
  name: string;
  phone?: number;
}
  1. 解决方案二:断言:但也不是特别好,因为难道在一个作用域下,每次取值都要断言,麻烦且有风险,
// 使用断言
console.log((target as interfaceB).phone);
console.log((<interfaceA>target).age);
  1. 解决方案三:条件判断:缺点同上
// 通过使用in运算符 的条件判断, 缩小target类型
if('phone' in target){
  console.log(target.phone);
  // const target: interfaceB
}


if('age' in target){
  console.log(target.age);
  // const target: interfaceA
}
  1. 使用TypeScript中的自定义类型保护和类型谓词

需要创建一个函数,在这个函数的方法体中,不仅要检查target 变量是否含有 age属性,而且还要告诉 TypeScript 编译器,如果上述逻辑语句的返回结果是 true,那么当前判断的target 变量值的类型是 interfaceA类型

创建一个自定义类型保护函数 —— isInterfaceA,它的具体实现如下:

// 联合类型
type interfaceAB = interfaceA | interfaceB;
// 自定义类型保护函数
const isInterfaceA = (item: interfaceAB): item is interfaceA => {
  return (item as interfaceA).age !== undefined;
};

// 判断target 属于哪个类型
if (isInterfaceA(target)) {
  console.log(target.age); //target的类型为interfaceA
} else {
  console.log(target.phone); //target的类型为interfaceB
}

通过泛型解决类型谓词复用问题 如果你要检查的类型很多,那么为每种类型创建和维护唯一的类型保护可能会变得很繁琐。针对这个问题,我们可以利用 TypeScript 的另一个特性 —— 泛型,来解决复用问题:

例如:定义通用类型保护函数

// 通过泛型定义通用类型保护函数
function isOfType<T>(
  target: unknown,
  prop: keyof T
): target is T {
  return (target as T)[prop] !== undefined;
}

// 类型保护
if (isOfType<interfaceA>(target, "age")) {
  console.log(target.age);
} 
if (isOfType<interfaceB>(target, "phone")) {
  console.log(target.phone);
}

is 关键字经常用来封装”类型保护函数”,通过和函数返回值的比较,从而缩小参数的类型范围,所以类型谓词 is 也是一种类型保护。

# TS unknown类型

unknown类型:unknown类型只能分配给 any类型和unknown类型 本身

unknown类型是TS3新增的类型,这个类型与any类型类似,可以设置任何的类型值,随后可以更改类型。因此可以将变量先设置为字符串类型,然后再将其设置为数字类型,如果事先不检查类型,使用any类型,调用了不存在的方法,编译时不会报错,代码运行时才会发现错误。但是使用unknown 类型不一样,如果不进行类型判断,执行相关操作编译器就会报错。

// 编译器能顺利编译,当运行 node any,会报异常
let val: any = 22;
val = "string value";
val = new Array();
val.doesnotexist(33);
console.log(val);
// 编译时Property 'push' does not exist on type 'unknown'.
let val: unknown = 22;
val = "string value";
val = new Array();
val.push(33);
console.log(val);
let val: unknown = 22;
val = "string value";
val = [];

if (val instanceof Array) {
  val.push(33);
  console.log(val);
}
let value: unknown

let value1: unknown = value   // OK
let value2: any = value       // OK

let value3: boolean = value   // Error
let value4: number = value    // Error
let value5: string = value    // Error
let value6: object = value    // Error
let value7: any[] = value     // Error
let value: unknown

value = true             // OK
value = 10               // OK
value = []               // OK
value = null             // OK
value = new TypeError()  // OK
......

unknown 类型在被确定为某个类型之前,不能被进行诸如函数执行、实例化等操作,一定程度上对类型进行了保护。

# TS联合类型 |

var val:string|number 
  1. 如果一个值是联合类型,那么只能访问联合类型的共有属性或方法。
interface Dog {
  name: string;
  eat: () => void;
  destroy: () => void;
}

interface Cat {
  name: string;
  eat: () => void;
  climb: () => void;
}

let pet: Dog | Cat;
pet!.name; // OK
pet!.eat(); // OK
pet!.climb(); // Error
  • 如果创建一个pet对象,只写了公共的方法,如果同时没有Dog1的destroy和Cat1的climb,就会报错,需要选择一个存在,也不能两个同时存在。
interface Dog1 {
  name: string;
  eat: () => void;
  destroy: () => void;
}

interface Cat1 {
  name: string;
  eat: () => void;
  climb: () => void;
}

let pet:  Cat1|Dog1 ;

// 不能将类型“{ name: string; eat(): void; }”分配给类型“Dog1 | Cat1”。
// 类型 "{ name: string; eat(): void; }" 中缺少属性 "climb",但类型 "Cat1" 中需要该属性。ts(2322)
pet ={
  name:'32',
  eat(){

  },
  // destroy(){
  //   console.log('destory')
  // },
  // climb(){
  //   console.log('climb')
  // }
}

console.log(pet)
// pet.destroy()

export {pet}

# 可辨识联合

函数 area() 的参数是一个 Rectangle | Circle | Parallelogram 联合类型。

其中,每个接口都有一个 type 属性,根据其不同的字符串字面量类型引导到不同的 case 分支,这种情况我们称之为 『可辨识联合(Discriminated Union)』

interface Rectangle {
  type: "rectangle";
  width: number;
  height: number;
}

interface Circle {
  type: "circle";
  radius: number;
}

interface Parallelogram {
  type: "parallelogram";
  bottom: number;
  height: number;
}

function area(shape: Rectangle | Circle | Parallelogram) {
  switch (shape.type) {
    case "rectangle":
      return shape.width * shape.height;
    case "circle":
      return Math.PI * Math.pow(shape.radius, 2);
    case "parallelogram":
      return shape.bottom * shape.height;
  }
}

const shape: Circle = {
  type: "circle",
  radius: 10,
};

console.log(area(shape));

# TS 交叉类型 &

交叉类型 & :& 的意思理解成 and ,A & B 表示同时包含 A 和 B 的结果

interface Admin {
  id: number;
  administrator: string;
  timestamp: string;
}

interface User {
  id: number;
  groups: number[];
  createLog: (id: number) => void;
  timestamp: number;
}

let t: Admin & User;

t!.administrator; // 合法 Admin.administrator: string
t!.groups; // 合法 User.groups: number[]
t!.id; // 合法 id: number
t!.timestamp; // 合法 timestamp: never


  • 应用场景:混入之类的
function extend<T, U>(first: T, second: U): T & U {
  for(const key in second) {
    (first as T & U)[key] = second[key] as any
  }
  return first as T & U
}
class Person {
  constructor(public name: string) { }
}
class ConsoleLogger {
  log() {
	  console.log('log')
  } 
}

let jim = extend(new Person('Jim'), new ConsoleLogger())
let n = jim.name
jim.log()//log

合并后即可访问 Person 类实例的 name 属性,也可以调用 ConsoleLogger 类实例的 log() 方法。

# TS 类型别名

顾名思义,一个可以指代任意类型的名字,类型别名会给类型起个新名字。类型别名有时和接口很像,但是 可以作用于原始值,联合类型,元组以及其它任何需要手写的类型。

类型别名不会新建一个类型,而是创建一个新名字来引用此类型。

type brand = string
type used = true | false

const str: brand = 'imooc'
const state: used = true
interface Admin {
  id: number;
  administrator: string;
  timestamp: string;
}

interface User {
  id: number;
  groups: number[];
  createLog: (id: number) => void;
  timestamp: number;
}

type T = Admin & User;
type Tree<T, U> = {
  left: T,
  right: U
}
  • 接口可以实现 extends 和 implements,类型别名可以借助&进行继承,实现类应用type也可以借助implements
  • 类型别名并不会创建新类型,是对原有类型的引用,而接口会定义一个新类型。
  • 接口只能用于定义对象类型,而类型别名的声明方式除了对象之外还可以定义交叉、联合、原始类型等。
  • 类型别名是最初 TypeScript 做类型约束的主要形式,后来引入接口之后,TS推荐尽可能的使用接口来规范代码。

  • 字面量类型:字面量(literal)是用于表达源代码中一个固定值的表示法(notation)
let protagonist: 'Sherlock'

protagonist = 'Sherlock'
protagonist = 'Watson' // Error, Type '"Watson"' is not assignable to type '"Sherlock"'

# TS索引类型 Index Types

是对象类型的高级编制,通过使用索引类型,TS编译器能够检查使用动态属性名的代码,在实际开发中,一般会结合索引类型查询和索引访问操作符使用。

  1. 索引类型可以让 TS 编译器覆盖检测到使用了动态属性名的代码
  2. 索引类型查询操作符 keyof 和索引访问操作符 T[K]
  3. keyof 可以获取对象/接口的 可访问索引字符串字面量类型
interface User {
  id: number,
  phone: string,
  nickname: string,
  readonly department: string,
}

class Token{
  private secret: string | undefined
  public accessExp: number = 60 * 60
  public refreshExp: number = 60 * 60 * 24 * 30 * 3
}

//let user: keyof User // let user: "id" | "phone" | "nickname" | "department"

let user: keyof User ='nickname'
console.log(user)//nickname
type token = keyof Token // type token = "accessExp" | "refreshExp"
class Token{
  public secret: string = 'ixeFoe3x.2doa'
  public accessExp: number = 60 * 60
  public refreshExp: number = 60 * 60 * 24 * 30 * 3
  public br :boolean =false
}

type token = keyof Token
type valueType = Token[token] // type valueType = string | number|boolean
type secret = Token['secret'] // type secret = string
//参见 ts-extends类型约束
function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
  return names.map(n => o[n])
}

interface Person {
  name: string
  position: string
  age: number
}
let person: Person = {
  name: 'Evan',
  position: 'Software Engineer',
  age: 27
}

let values: unknown[] = pluck(person, ['name', 'age'])
console.log(values)//[ 'Evan', 27 ]

# K extends keyof T和keyof T注意区别

keyof T 是 T 类型的键集,比如:上面因为直接用 keyof 看不出来 KA1 的类型集,用 Extract 排除一个不存在的类型 unknown 之后可以看出来。实际 KA1 和 KA 是等价的类型。

in 可以理解为 for ... in,表示从 keyof T 中去遍历每一个类型,用上述的例子就是分别是 "a"、"b" 和 "c" 类型。注意这里他们不是 string 类型,而是 literal string 类型,是某个具体的字符串类型。比如 "a" 类型的变量就只能是 "a" 值。

这里提到了“子类型”,与之相对的是“父类型”。简单的理解,可以从逻辑上这么认为

子类型的值一定也是父类型。比如“猫”是“动物”的子类型,一只具体的 “猫”一定也是“动物” 反之不成立,某个具体的“动物”不一定是“猫”,比如一只具体的“狗”。 而 K extend keyof T 表示 K 是 T 的子类型,这里是一个类型约束声明。比如 type T = "a" | "b" | "c";,那么 K 可以是 "a",也可以是 "a" | "c" 或者 "a" | "b" | "c" 等

# TS映射类型(in keyof) Partial Readonly

ts可以使用泛型来做类型映射,将对象或数组中类型转换为另一个类型。(Mappod type通过转换旧类型中的每个属性来创建新类型)

映射类型可以将已知类型的每个属性都变为可选的或者只读的。使用 Readonly 与 Partial 关键字

interface Person{
  name: string
  age: number
}

type PersonOptional = Partial<Person>
type PersonReadonly = Readonly<Person>

type PersonOptional = {
  name?: string
  age?: number
}
type PersonReadonly = {
  readonly name: string
  readonly age: number
}

  • 两个关键字的源码分析
type Readonly<T> = {
  readonly [K in keyof T]: T[K]
}
type Partial<T> = {
  [K in keyof T]?: T[K]
}
  1. 首先,先执行 keyof Props 获取对象类型 Props所有建的联合类型('a'|'b'|'c')
  2. 然后,key in ...就表示Key可以是Props中所有的键名称中的任意一个。

# 条件类型

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

条件类型用来表达非均匀类型映射,可以根据一个条件表达式来进行类型检测,从两个类型中选出其中一个:

T extends U ? X : Y

类似三目运算符:若 T 是 U 的子类型,则类型为 X,否则类型为 Y。若无法确定 T 是否为 U 的子类型,则类型为 X | Y。

declare function f<T extends boolean>(x: T): T extends true ? string : number
//在条件不确定的情况下,得到了联合类型 string | number
const x = f(Math.random() < 0.5) // const x: string | number

const y = f(true) // const y: string
const z = f(false) // const z: number

# 可分配条件类型

在条件类型 T extends U ? X : Y 中,当泛型参数 T 取值为 A | B | C 时,这个条件类型就等价于 (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y),这就是可分配条件类型。

可分配条件类型(distributive conditional type)中被检查的类型必须是裸类型参数(naked type parameter)。裸类型表示没有被包裹(Wrapped) 的类型,(如:Array<T>、[T]、Promise<T> 等都不是裸类型),简而言之裸类型就是未经过任何其他类型修饰或包装的类型。

  • 应用场景
  1. Exclude<T, U> – 从 T 中剔除可以赋值给 U 的类型。
  2. Extract<T, U> – 提取 T 中可以赋值给 U 的类型。
  3. NonNullable<T> – 从 T 中剔除 null 和 undefined。
  4. ReturnType<T> – 获取函数返回值类型。
  5. InstanceType<T> – 获取构造函数类型的实例类型。
type T00 = Exclude<'a' | 'b' | 'c' | 'd', 'a' | 'c' | 'f'>  // 'b' | 'd'

Exclude实现源码:

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

# ts字面量类型

可以将类型声明为更具体的数字或者字符串

let x: "hello" = "hello";
// OK
x = "hello";
// ...
x = "howdy";
// Type '"howdy"' is not assignable to type '"hello"'.

如果以const且没加类型注解,默认是字面量类型

字面量类型本身并没有什么用,如果结合联合类型,就显得有用多了。举个例子,当函数只能传入一些固定的字符串时:

function printText(s: string, alignment: "left" | "right" | "center") {
  // ...
}
printText("Hello, world", "left");

# 字面量赋值[函数对象参数躲避检查]

TypeScript在字面量直接赋值的过程中,为了进行类型推导会进行严格的类型限制。但是之后如果将一个变量标识符赋值给其他的变量时,会进行freshness擦除操作。

interface IPerson {
  name: string
  age: number
  height: number
}

function printInfo(person: IPerson) {
  console.log(person)
}

// 代码会报错
// printInfo({
//   name: "why",
//   age: 18,
//   height: 1.88,
//   address: "广州市"
// })

const info = {
  name: "why",
  age: 18,
  height: 1.88,
  address: "广州市"
}

printInfo(info)

# 字面量推断

当初始化变量为一个对象的时候,TypeScript 会假设这个对象的属性的值未来会被修改.

declare function handleRequest(url: string, method: "GET" | "POST"): void;

const req = { url: "https://example.com", method: "GET" };
handleRequest(req.url, req.method);

//req.method: 类型“string”的参数不能赋给类型“"GET" | "POST"”的参数。

在上面这个例子里,req.method 被推断为 string ,而不是 "GET",因为在创建 req 和 调用 handleRequest 函数之间,可能还有其他的代码,或许会将 req.method 赋值一个新字符串比如 "Guess" 。所以 TypeScript 就报错了。

  1. 解决方案1:利用const声明为常量,下图可以看出,method就是POST常量不会改变
type Method = 'GET' | 'POST'
function request(url: string, method: Method) {}

type Request = {
  url: string,
  method: Method
}

const options = {
  url: "https://www.coderwhy.org/abc",
  method: "POST"
} as const

request(options.url, options.method)
  1. 解决方案2:type约束
type Method = 'GET' | 'POST'
function request(url: string, method: Method) {}

type Request = {
  url: string,
  method: Method
}

const options:Request = {
  url: "https://www.coderwhy.org/abc",
  method: "POST"
} 
request(options.url, options.method)
  1. 解决方案3:as类型断言
request(options.url, options.method as Method)

# ts is关键字

is 关键字一般用于函数返回值类型中,判断参数是否属于某一类型,并根据结果返回对应的布尔类型。

详见类型保护中的应用

使用原因: 但是由于函数嵌套 TypeScript 不能进行正确的类型判断。

通过 is 关键字将类型范围缩小为 string 类型,这也是一种代码健壮性的约束规范。

# ts infer关键字 推断

  • 想在获取数组里的元素类型
type Ids = number[];
type Names = string[];

type Unpacked<T> = T extends Names ? string : T extends Ids ? number : T;

type idType = Unpacked<Ids>; // idType 类型为 number
type nameType = Unpacked<Names>; // nameType 类型为string

let fnum:idType = 1000
console.log(fnum)//1000

如果T是某个待推断类型的数组,则返回推断的类型,否则返回T


type ElementOf<T> = T extends Array<infer E> ? E : boolean;

type Tuple = string[];
// Tuple是string[],推断出E是string
type TupleToUnion = ElementOf<Tuple>

let k20t1:TupleToUnion ='30'

console.log(k20t1)//“30”

// number extends Array<?> =>不成立,走boolean
let k20t2:ElementOf<number> = true
console.log(k20t2)//56423 数字

infer可以推断出联合类型

type Foo21<T> = T extends { a: infer U; b: infer U } ? U : never;

type T11 = Foo21<{ a: string; b: number }>; // T11类型为 string | number

let a1:T11 = '30'
let b1:T11 = 'demo'
let d1:Foo21<{ a: string; b: number }> = 1999

//let c1:T11 = true //Type 'boolean' is not assignable to type 'string | number'.
console.log(a1,b1,d1)//"30" "demo" 1999
type T1 = { name: string };
type T2 = { age: number };

type K2<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void }
  ? U
  : never;

interface Props {
  a: (x: T1) => void;
  b: (x: T2) => void;
}

type k3 = K2<Props>;

const app: k3 = {
  name: "k17",
  age: 30,
};
console.log(app); //{ name: 'k17', age: 30 }

容易混淆的点

  1. TypeScript 中描述类型要用 小写 ,比如 boolean、number、string等;
  2. 大写开头的如 Boolean、Number、String 代表的是 JavaScript 的构造函数:
let a1: Number = new Number('10') // a === 10 为 false
let b1: number = Number('10') // b === 10 为 true

//let c1: number = new Number('10')
// 不能将类型“Number”分配给类型“number”。“number”是基元,但“Number”是包装器对象。如可能首选使用“number”。

// a1 instanceof Number // true

console.log('a1',typeof a1)
console.log('b1',typeof b1)

// b1 instanceof Number // false 
//The left-hand side of an 'instanceof' expression must be of type 'any', an object type or a type parameter.

第 1 行,通过 new Number('10') 得到的是一个构造函数,本质是一个对象。

第 2 行,Number('10') 与 10 都是声明一个数字 10 的方法,本质就是一个数字。

第 4 - 5 行,instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。a 是一个对象,它的 __proto__ 属性指向该对象的构造函数的原型对象 Number,所以为 true。b 是一个数字,所以为 false。

当用构造函数定义的a。虽然是对象,但是a.__proto__会被认为错误,提示a是数字类型 ,可以通过any强制修改类型

let a: Number = new Number('10') // a === 10 为 false
let b: number = Number('10') // b === 10 为 true

// a instanceof Number // true
// Object.getPrototypeOf(a) 
console.log(a===10)
console.log(b===10)

console.log(typeof a )

// console.log(a.__proto__)//类型“Number”上不存在属性“__proto__”。

console.log((a as any).__proto__ === Object.getPrototypeOf(a))//true

# keyof和typeof在ts中应用

如果该类型具有字符串或数字索引签名,则keyOf将返回这些类型:

type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish;
// type A = number

type Mapish = { [k: string]: boolean };
type M = keyof Mapish;
//type M = string | number

请注意,在本例中,M是字符串|数字-这是因为JavaScript对象键始终被强制为字符串,因此obj[0]始终与obj['0']相同。

# keyof应用场景

  1. 获取对象所有属性的类型
type Person = {
    id: number;
    name: string;
    age: number;
  };
  
type P2 = Person[keyof Person];  
//type P2 = string | number
let ff:P2 = 100

TIP

  • Person['key'] 是查询类型(Lookup Types), 可以获取到对应属性类型的类型;
  • Person[keyof Person]本质上是执行 Person['id' | 'name' | 'age'];
  • 由于联合类型具有分布式的特性,Person['id' | 'name' | 'age'] 变成了 Person['id'] | Person['name'] | Person['age'];
  • 最后得到的结果就是 number | string.

TS教程 (opens new window) typescript (opens new window)

最后更新: 3/3/2025, 8:38:29 AM