typeScript 小记

96 阅读13分钟

名义型类型 & 结构型类型

  • 名义类型系统中,数据类型的兼容性或等价性是通过明确的声明和/或类型的名称来决定的。
  • 结构类型系统中,只对值所具有的结构进行类型检查。简而言之,要判断两个类型是否是兼容的,只需要看两个类型的结构是否兼容就可以了,不需要关心类型的名称是否相同。
interface Duck {
    sayGa: boolean;
}
​
class Animal {
    sayGa: boolean;
    horn: boolean;
    tail: boolean;
}
​
let d: Duck;
// OK, because of structural typing
d = new Animal();

在程序设计中,鸭子类型(英语:duck typing)是动态类型的一种风格。 在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由"当前方法和属性的集合"决定。

简而言之:「如果它走起路来像鸭子,叫起来也是鸭子,那么我们就可以认为它就是鸭子,即使它头上有犄角!」。

名义类型使用 🌰

// 名义类型 Nominal Typing// 一个计算纬度的函数的类型
type calcByDimension = (dimension: TParamOf2D) => voidtype TParamOf2D = { x: number; y: number }
type TParamOf3D = { x: number; y: number; z: number }
​
// 因为 TParamOf3D中存在 x y属性,所以能通过类型校验
const vector3D: TParamOf3D = { x: 1, y: 1, z: 1 }
const calc: calcByDimension = (a) => {}
calc(vector3D) // 并没有抛出错误type TParamOf2D = { x: number; y: number } & { __type: '2d' }
type TParamOf3D = { x: number; y: number; z: number } & { __type: '3d' }
​
// 虽然存在 x y 属性,但是 __type属性的类型不一致导致无法通过类型校验
const vector3D: TParamOf3D = { x: 1, y: 1, z: 1, __type: '3d' }
const calc: calcByDimension = (a) => {}
​
// error:类型“TParamOf3D”的参数不能赋给类型“TParamOf2D”的参数。ts(2345)
calc(vector3D)

字符串索引类型

  • ts 支持 string 和 number 索引类型,允许同时使用这两种类型的索引

  • number 索引类型对应的 类型 必须是 string 索引类型对应类型的 子类型。

    因为 js 规定: 对象的键名一律为字符串。使用索引获取对应的值时,非字符串的索引会被转化为字符串。number 索引签名一般用于数组,但是 js 中数组本身也是对象,所以数组的索引(下标)实际上也就是只能为字符串

// 可以看到symbol不受影响
interface IIndex {
  [key: string]: string | number
  [index: number]: number
  [s: symbol]: number | symbol | string | boolean
}
​
// 不过就算 number 索引的值为子类型,两种索引依然不能重名,否则运行时会报错,除非子类型的数据结构和父类型是一致的。
interface Animal {
  type: string
}
​
interface Dog extends Animal {
  name: string
}
​
interface DogData {
  [x: string]: Animal
  [x: number]: Dog
}
​
let animal: Animal = { type: 'dog' }
let dog: Dog = { type: 'dog', name: 'hotDog' }
​
let corgi: DogData = {}
// Error: 类型 "Animal" 中缺少属性 "name",但类型 "Dog" 中需要该属性。ts(2741)
corgi['1'] = animal
corgi[1] = dog

extends

  • 泛型约束
// 定义方法获取传入参数的length属性
function getLength<T>(arg: T) {
  return arg.length;
}
getLength(true)  // throw error: arr上不存在length属性// -----------------------------------------------interface IHasLength {
  length: number;
}
​
// 利用 extends 关键字在声明泛型时约束泛型需要满足的条件
function getLength<T extends IHasLength>(arg: T) {
  return arg.length;
}
const fn = (a: any,b:any,c:any) => {}
const res = getLength(fn) // res: 3
  • 条件类型
// 如果 T 是 U 的子类型,那么结果为X,否则为Y
T extends U ? X : Y
​
// 简单的类型匹配
type TypeName<T> =
    T extends boolean   ? "boolean" :
    T extends undefined ? "undefined" :
    T extends Function  ? "function" :
    "object";
​
type T2 = TypeName<true>;    // "boolean"
type T3 = TypeName<() => void>;  // "function"
type T4 = TypeName<string[]>;    // "object"
​
​
// 补充个isAny,T 后置
type IsAny<T> = 0 extends 1 & T ? true : false
  • 分发

分发一定是需要在 extends 产生的类型条件判断中,并且是前置类型

只有联合类型才会产生分发

分发一定要满足所谓的裸类型中才会产生效果

type GetSomeType1<T extends string | number | [string]> = T extends string[]
  ? 'a'
  : 'b';
// 可以简单理解为 T 是裸类型,而[T]不是
type GetSomeType2<T extends string | number | [string]> = [T] extends string[]
  ? 'a'
  : 'b';
​
type res1 = GetSomeType1<number | [string]>  // a | b
type res2 = GetSomeType2<[string] | number>; // b
​
​
// 位置不同,结果不同
// 官方高级类型
type Exclude<T, K> = T extends K ? never : T
type r1 = Exclude<'a' | 'b' | 'c' | 'd',   'a' | 'c'>
​
type Include<T, K> = T extends K ? T : nevertype r2 = Include<'a' | 'b' | 'c' | 'd',   'a' | 'c'>

infer

可以简单理解为一个 类型的 变量,它只能在条件类型中使用并且判定结果为 true 时生效

// 内置高级工具类型:
type ReturnType<T> = T extends (args: any) => infer R ? R : never
type ParamType<T> = T extends (args: infer P) => any ? P : nevertype fn = (a: number) => stringtype rt = ReturnType<fn> // type rt = string
type pt = ParamType<fn> // type pt = number

infer 推断联合类型 🌰

type Foo<T> = T extends { a: infer U; b: infer U } ? U : nevertype T10 = Foo<{ a: string; b: string }> // T10类型为 string
type T11 = Foo<{ a: string; b: number }> // T11类型为 string | number// 获取数组中所有元素的联合类型
type Arr2Union<T> = T extends (infer R)[] ? R : nevertype Arr = [string, number, symbol]
type Union = Arr2Union<Arr> // Union 类型为 string | number | symbol          

通过infer 获取字符串首字母并将其转为大写 🌰

type FirstLetter2Upcase<S extends string> = S extends `${infer F}${infer Rest}` ? `${Capitalize<F>}${Rest}` : never

由于在字符串中 infer 并不能像在数组中直接获取到最后一项,所以如果要想将 字符串中最后一个字符转为大写就需要借助泛型。具体见泛型一节将字符串中最后一个字符转为大写

never

使用 never 避免出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码。

// A.ts

export type TRole = 'Root' | 'Admin' | 'Default'

function handleValue(value: TRole) {
  switch (value) {
    case 'Root':
      break
    case 'Admin':
      break
    case 'Default':
      break
    default:
      break
  }
}

当在 A.ts 中的向 TRole 增加了‘Default 类型后,如果不在 B.ts 中增加对应的 Default 分支就会报错:不能将类型“string”分配给类型“never”。

// B.ts
import { TRole } from './never'

function handleValue(value: TRole) {
  switch (value) {
    case 'Root':
      break
    case 'Admin':
      break
    default:
      // 此处 value 的类型被收敛为 never
      const neverCheck: never = value
      break
  }
}

使用 never 来收敛条件类型

type FnArguments<T> = T extends (...args: infer A) => any ? A : never

type FnReturnType<T> = T extends (...args: any) => infer R ? R : never

枚举

  • 普通枚举(数字枚举、字符串枚举、异构枚举)

    数字枚举的值会以第一个元素的值递增;数字枚举可以反向枚举;

// index.ts
	enum numberEnum {
    a = 2, 
    b,
    c,
  }
  
  enum stringEnum {
    a = 'a',
    b = 'b',
    c = 'c'
  }
	console.log('数字枚举', numberEnum); // { '2': 'a', '3': 'b', '4': 'c',     a: 2, b: 3, c: 4 }
  console.log('字符串枚举', stringEnum); // { a: 'a', b: 'b', c: 'c' }

	console.log('正向枚举', numberEnum.b) // 3
  console.log('反向枚举', numberEnum[3]); // b

// 异构枚举
enum anyEnum {
  first = 1,
  second = '2',
  three = Math.random() // 计算成员
}

编译后的 js 文件

// index.js 
var numberEnum = void 0;
    (function (numberEnum) {
        numberEnum[numberEnum["a"] = 2] = "a";
        numberEnum[numberEnum["b"] = 3] = "b";
        numberEnum[numberEnum["c"] = 4] = "c";
    })(numberEnum || (numberEnum = {}));
    var stringEnum = void 0;
    (function (stringEnum) {
        stringEnum["a"] = "a";
        stringEnum["b"] = "b";
        stringEnum["c"] = "c";
    })(stringEnum || (stringEnum = {}));
  • 常量枚举

    常量枚举只能使用常量枚举表达式,并且不同于常规的枚举,它们在编译阶段会被删除。 常量枚举成员在使用的地方会被内联进来。 之所以可以这么做是因为,常量枚举不允许包含计算成员。

  const enum constanceEnum {
    a = 2,
    b = Math.random(), //Error: const enum member initializers must be constant expressions.ts(2474)
  }
// ts文件	
// 换成常量枚举后
	const enum constanceEnum {
    a = 2,
    b,
  }
// js文件
function select(value) {
        switch (value) {
            case 2 /* constanceEnum.a */:
                break;
            case 3 /* constanceEnum.b */:
                break;
            default:
                break;
        }
    }

断言

  • 基本断言
const person = {};
// 直接添加属性,报错
person.name = 'cdb'; // error: 'name' 属性不存在于 ‘{}’


interface Person {
  name: string;
}
const person = {} as Person; // ok
person.name = 'cdb';
  • 非空断言
// 非空断言
let nonValueOrString: string | null | undefined;

nonValueOrString.split('') //error: nonValueOrString可能为 “null” 或“未定义”。ts(18049)
nonValueOrString!.split('') // ok
  • 确定赋值断言

let x: number;
const sum = x.toString() // error: 在赋值前使用了变量“x”。ts(2454)
x = 2

// 下面 2 种都是OK的,相当于告诉ts, 代码运行到此处时 x 必定有值
let x!: number;
const sum = x
x = 2

let x: number;
const sum = x! // ok
x = 2

// 非空 & 复制断言的区别?
  • 双重断言(慎用)
`一般用于父子类型的转换`
interface IAnimal {
  legs: number
}
interface IDeer extends IAnimal {
  deerChirp: boolean
}
interface IHorse extends IAnimal {
  horseChirp: boolean
}

const normalDeer: IDeer = { legs: 4, deerChirp: true }
const normalHorse: IHorse = { legs: 4, horseChirp: true }

const dubDeer: IDeer = { legs: 4, deerChirp: true };

(dubDeer as IHorse).horseChirp = true; // error: 类型 "IDeer" 到类型 "IHorse" 的转换可能是错误的,因为两种类型不能充分重																								 叠。如果这是有意的,请先将表达式转换为 "unknown"。ts(2352)
(dubDeer as IAnimal as IHorse).horseChirp = true;  // ok
  • 常量断言

    as const 是TypeScript 中的一个修饰符,用来修改类型推断的行为。当 as const 修饰符用在变量声明或表达式的类型上时,它会强制 TypeScript 将变量或表达式的类型视为不可变的(immutable)。这意味着,如果你尝试对变量或表达式进行修改,TypeScript 会报错。

const obj = {
  dx: 'huang',
  xd: 'ou'
} as const
obj.xd = '111'  // error: 无法为“xd”赋值,因为它是只读属性。ts(2540)     


const arr = ['1', true, { o: 'o' }] as const
arr.push('1') // error: 类型“readonly ["1", true, { readonly o: "o"; }]”上不存在属性“push”。ts(2339)
arr[0] = 2  // error: 无法为“0”赋值,因为它是只读属性。ts(2540)


// 使用 as const 后, obj 和 arr 对应的类型分别如下:
const obj: {
    readonly dx: "huang";
    readonly xd: "ou";
}
const arr: readonly ["1", true, { readonly o: "o"; }]

readonly 和 as const 都能将类型声明为仅可读,而 as const 还能将类型转换成常量

interface Foo {
  readonly a: {
      b: number,
  },
}
const f: Foo = {
  a: { b: 1 },
};

f.a = { b: 2 }; // Error: 无法分配到 "a" ,因为它是只读属性

f.a.b = 2; // 这里则没问题

const bar = {
  a: { b: 1 },
} as const;

bar.a = {b:2} // 无法为“a”赋值,因为它是只读属性。ts(2540)
bar.a.b = 2; // 无法为“b”赋值,因为它是只读属性。ts(2540)

类型谓词

类型保护有以下几种:

  1. typeof 适用于判断基本类型
  2. instanceof 适用于判断类
  3. in
  4. is

interface Bird {
  fly: () => any
  eat: () => any
}

interface Fish {
  swim: () => any
  eat: () => any
}

type BF = Fish | Bird

// 保护
function isFish(pet: BF): pet is Fish {
  return (pet as Fish).swim !== undefined
}

function isBird(pet: BF) {
  return (pet as Bird).fly !== undefined
}

function getPet(): BF {
  return { eat: () => true, fly: () => true }
}
const pet = getPet()

// -------如何才能放心的在对应的分支里调用对应的方法?----------------
if ((pet as Bird).fly) {
  pet.fly()  // error: 类型“{ swim: () => any; eat: () => any; }”上不存在属性“fly”。ts(2339)
  (pet as Bird).fly() // ok
}


if (isBird(pet)) {
  pet.fly()
} else if (isFish(pet)) {
  pet.swim()
}

变型

变型 是发生在父子类型之间的

  • 集合论中,属性更少的是子集
  • 类型系统中,属性更多的是子类型

父类型比子类型涵盖的范围更广; 子类型比父类型更具体

协变: 允许子类型变型为父类型

逆变: 允许父类型变型为子类型

双向协变: 协变 + 逆变

不变: 不允许变型(即两种类型没有父子关系)

// 基础例子
interface Animal {
  eat: boolean;
}

interface Duck extends Animal{
  sayGa: boolean;
}

协变

// 协变
let a: Animal = {eat: true};
let d: Duck = {eat: true, sayGa: true};

a = d // 兼容,这就是一个协变
d = a // error: 类型 "Animal" 中缺少属性 "sayGa",但类型 "Duck" 中需要该属性。ts(2741)

逆变

let visitAnimal = (animal: Animal): Duck => {
  animal.eat = true;
  return {
    sayGa: true,
    eat: true,
  }
}

let visitDuck = (duck: Duck): Animal => {
  duck.sayGa = true;
  return {
    eat: true,
  }
}

// error: 不能将类型“(duck: Duck) => Animal”分配给类型“(animal: Animal) => Duck”。参数“duck”和“animal” 的类型不兼容。
visitAnimal = visitDuck

// 兼容, 逆变成功
visitDuck = visitAnimal

总结

// 抽象一下
type one = (duck: Duck) => Animal
type two = (animal: Animal) => Duck

type res1 = one extends two ? true : false // false
type res2 = two extends one ? true : false // true

// 函数参数是逆变:Animal 可以变型为 Duck,父类型 -> 子类型
// 函数返回值是协变:Duck 可以变型为 Animal,子类型 -> 父类型

但是其实 TS 里的函数参数是允许双向协变的

let needDuck = (duck: Duck) => {
  duck.eat
  if (Math.random() >= 0.5) {
    duck.sayGa
  }
}

let needAnimal = (animal: Animal) => {
  animal.eat
}

// 逆变 的情况下
needAnimal  = needDuck // 多的不能给少的, 我只需要一个eat,duck完全可以满足,为什么不行? 所以允许双向协变

needDuck = needAnimal // 少的可以给多的

为什么 ts 允许函数参数双向协变:因为 ts 是结构型类型语言,如果 Duck[] 可以赋值给 Animal[],那么就意味着Duck[]可以赋值给 Animal[] ,从而导致设计上就允许了双向协变,这是 ts 设计者为了维持结构化类型兼容的一种取舍。但毕竟双向协变是不安全的,

所以在 TS (2.6+) 后 ,你可以通过开启 strictFunctionTypesstrict 来修复这个问题。设置之后,函数参数就是逆变而不再是双向协变的了。

不变


type A = {
  a: number
}
type B = {
  b: string
}

let AA: A = {a: 1}
let BB: B = { b: '2' };

AA.b = 'str' // error: 类型“A”上不存在属性“b”。ts(2339)

// ***** 强行变型 ******
(AA as unknow as B).b = 'string'
(AA as any as B).b = 'string'

泛型

简单理解就是将类型抽象成了一个变量,只不过这个变量是类型

使用的地方: 接口、类、函数

泛型位置不同的区别

例子:实现一个 Foreach函数, 定义callback遍历方法 两种方式 应该采用哪一种?

// 第一种
type Callback = <T>(item: T) => void
// 第二种
type Callback<T> = (item: T) => void;

const MyForEach = <T>(arr: T[], callback: Callback) => {
  for (let i = 0; i < arr.length - 1; i++) {
    callback(arr[i])
  }
};

forEach(['1', 2, 3, '4'], (item) => {});

应该用第二种,因为TS 是一种静态类型检测,并不会执行你的代码

解析嵌套泛型

// 定义几个普通的类型
type obj = {
  name: 'name'
  age: 23
}

type nestedObj = {
  a: obj
  b: '123'
  c: () => string
}

type TTuple = ['string', 'number']

// 一个接收泛型的工具类型
type TToolType<S> = S extends 'string' ? obj : nestedObj

type res1 = TToolType<2>
// type res1 = {
//     a: obj;
//     b: '123';
//     c: () => string;
//     d: TTuple;
// }

type res2 = ExpandNestedGeneric<TToolType<2>>
// type res2 = {
//     a: {
//         name: 'name';
//         age: 23;
//     };
//     b: '123';
//     c: () => string;
//     d: ["string", "number"];
// }


// 解析嵌套泛型工具类型
export type ExpandNestedGeneric<T> = T extends object
  ? T extends Function
    ? T
    : T extends infer O // 不用infer也是可以的
    ? { [K in keyof O]: ExpandNestedGeneric<O[K]> }
    : never
  : T

 将字符串中最后一个字符转为大写


type testString = 'hello, is me'

// first,  先反转字符串,再大写首字母,再反转一遍字符串
type ReverseStr<S extends string, resStr extends string = ''> = S extends `${infer FirstLetter}${infer RestLetter}` ? ReverseStr<RestLetter, `${FirstLetter}${resStr}`> : resStr


type UpcaseFirstLetter<S extends string> = S extends `${infer FirstLetter}${infer RestLetter}` ? `${Capitalize<FirstLetter>}${RestLetter}` : never

type res = ReverseStr<UpcaseFirstLetter<ReverseStr<testString>>>


// second, 字符串中不可以通过infer拿到最后一项,但是 [] 可以,所以先将每个字符都存进数组里,再数组中利用infer取得最后一项(即最后一个字符)并转为大写后,再把此时的数组转为字符串
type Str2Arr<S extends string, resArr extends any[] = []> = S extends `${infer FirstLetter}${infer RestLetter}` ? Str2Arr<RestLetter, [...resArr, FirstLetter]> : resArr

type Arr2Str<A extends any[], resStr extends string = ''> = A extends [infer First extends string, ...infer Rest] ? Arr2Str<Rest, `${resStr}${First}`> : resStr

type UpcaseLastItemInArr<A extends any[]> = A extends [...infer Head, infer LastItem extends string] ? Arr2Str<[...Head, Capitalize<LastItem>]> : never

type res2 = UpcaseLastItemInArr<Str2Arr<testString>>

什么时候用interface,什么时候用type ?

摘自 AntdPro > Typescript 一节

推荐任何时候都是用 type, type 使用起来更像一个变量,与 interface 相比,type 的特点如下:

  • 表达功能更强大,不局限于 object/class/function
  • 要扩展已有 type 需要创建新 type,不可以重名
  • 支持更复杂的类型操作

基本上所有用 interface 表达的类型都有其等价的 type 表达。在实践的过程中,我们也发现了一种类型只能用 interface 表达,无法用 type 表达,那就是往函数上挂载属性。

但是实验后, interface 与 type 并没有区别 o_O!....

interface FuncWithAttachment {
  (param: string): boolean;
  someProperty: number;
}

const testFunc: FuncWithAttachment = {};
const result = testFunc('mike'); // 有类型提醒
testFunc.someProperty = 3; // 有类型提醒