TypeScript自测清单(可以自测一下)

·  阅读 88

TypeScript自测清单

一、核心概念

  1. TypeScript的火其实是一种必然,因为随着JavaScript的发展越来越好, 许多大型项目都需要使用JavaScript来进行来发,但是JavaScript是动态的语言,容易在运行时产生一些难以修复的 Bug,因此急需一个静态的类型语言;这就是为什么TypeScript现在会出现并且火起来了。

  2. TypeScript是可以进行类型编程的,属于图灵完备的语言

  3. 元组就是元素个数和类型都确定的数组类型;Tuple

  4. 接口可以用来描述函数、构造器、索引类型(对象、class、数组)等复合类型

  5. 特殊类型

    • void 代表空,可以是 null 或者 undefined,一般是用于函数返回值。
    • any 是任意类型,任何类型都可以赋值给它,它也可以赋值给任何类型(除了 never)。
    • unknown 是未知类型,任何类型都可以赋值给它,但是它不可以赋值给别的类型
    • never 代表不可达,比如函数抛异常的时候,返回值就是 never。
  6. infer 可以推断 某个类型的一部分

  7. 映射类型

    • 索引查询 keyof T
    • 索引遍历 K in keyof T
    • 索引访问 T[K]
  8. 索引变化(也就是 key 也可以发生变化, 使用 as ,重映射)

    type MapType<T> = {
      [Key in keyof T as `#${Key & string}`]: T[Key];
    };
    复制代码
  9. 提取

    // 使用类型体操去除字符串左右的空字符
    type TrimRight<Str extends string>
      = Str extends `${infer Use}${' '|'\n'|'\t'}`
      ? TrimRight<Use>
      : Str
    
    type TrimLeft<Str extends string> =
      Str extends `${' '|'\n' | '\t'}${infer Use}`
      ? TrimLeft<Use>
      :Str
    
    type Trim<Str extends string> = TrimRight<TrimLeft<Str>>
    复制代码
    • TypeScript 类型的模式匹配是通过类型 extends 一个模式类型,把需要提取的和部分放到通过 infer 声明的局部变量里,后面可以通过这个局部变量拿到类型做各种后续的处理。
  10. 重构

    • 重构的含义是指:将原来某种类型进行拆分组装,形成一个新的类型;看下面这个例子;

      // 实现一个可以添加一个类型的类型;
      
      type Push<BaseArr extends unknown[], Ele> = [...BaseArr, Ele];
      
      // 这个Push类型就具备有添加一个元素的能力了,并且构建了一个新的类型;这个就是重构;
      复制代码
    • 函数类型的重构

      // 给传过来的某一个函数类型的参数进行重构
      type AppendArgument<Fun extends Function , Arg>
        = Fun extends (...args:infer Args)=>infer ReturnType
        ? (...args:[...args,Arg])=>ReturnType
        : never
      复制代码
    • 索引类型的重构

      // 过滤一个索引类型,根据固定的值将其过滤掉
      type FilterKey<Base extends object, value> = {
        [K in keyof Base as K extends value ? never : K]: Base[K];
      };
      
      // 这样既可以将包含在value中的给过滤掉;实际上omit就是这个作用
      复制代码
  11. 递归复用

    • promise 的递归

      // 写一个类型:可以取出深层次 Promise的值、
      
      type DeepPromiseValue<T> = T extends Promise<infer Value>
        ? DeepPromiseValue<Value>
        : T;
      复制代码
    • 翻转数组

      // 翻转一个数组类型
      type Reverse<Arr extends unknown[]> = Arr extends [
        infer First,
        ...infer Rest
      ]
        ? [...Reverse<Rest>, First]
        : Arr;
      复制代码
    • 构建数组

      // 构建一个数组 告诉我长度就行
      type BuildArr<
        Length extends number,
        Ele = unknown,
        Arr = []
      > = Arr["length"] extends Length
        ? Arr
        : BuildArr<Length, Ele, [...Arr, Ele]>;
      复制代码
    • 在类型体操中,遇到数量不确定的问题,要条件反射的想到递归。

  12. 数组长度

    • 基于构建数组实现加法

      type Add<One extends number, Two extends number> = [
        ...BuildArr<One>,
        ...BuildArr<Two>
      ]["length"];
      
      // 需要基于 BuildArr来进行实现;
      复制代码
    • 实现减法

      type SubTract<
        One extends number,
        Two extends number
      > = BuildArr<One> extends [...infer Rest, ...BuildArr[Two]]
        ? Rest["length"]
        : never;
      复制代码
  13. 联合分散

    • 当类型参数为联合类型,并且在条件类型左边直接引用该类型参数的时候,TypeScript 会把每一个元素单独传入来做类型运算,最后再合并成联合类型,这种语法叫做分布式条件类型

      type Add<T> = T extends string ? `${T}#` : never;
      
      type Test = Add<"a" | "b">;
      复制代码
    • 对联合类型的处理和对单个类型的处理没什么区别,TypeScript 会把每个单独的类型拆开传入。不需要像数组类型那样需要递归提取每个元素做处理。

    • isUnion 的实现

    type isUnion<A, B = A> = A extends A
      ? [B] extends [A]
        ? false
        : true
      : never;
    复制代码

    当 A 是联合类型的时候:A extends A 这种写法是为了触发分布式条件类型,让每个类型单独传入处理的,没别的意义。

    • 将数组类型变为联合类型
    type ToUnion<T extends unknown[]> = T[number];
    复制代码
    • BEM 练习
    type BEM<
      Block extends string,
      Element extends string[],
      Modifiers extends string[]
    > = `${Block}__${Element[number]}--${Modifiers[number]}`;
    
    type BemResult = BEM<"song", ["xiao", "da"], ["peng", "yong"]>;
    复制代码
  14. 特殊类型的特征

    • IsAny any 类型与任何类型的交叉都是 any,也就是 1 & any 结果是 any。
    type IsAny<T> = "string" extends "string" & any ? true : false;
    复制代码
    • IsEqual
    type IsEqual<A, B> = (<T>() => T extends A ? 1 : 2) extends <
      T
    >() => T extends B ? 1 : 2
      ? true
      : false;
    复制代码
    • IsNever:never 在条件类型中也比较特殊,如果条件类型左边是类型参数,并且传入的是 never,那么直接返回 never.

      type TestNever<T> = T extends string ? true : false;
      
      type Test = TestNever<never>; // never
      复制代码

      正因为如此我们才需要使用下面的方式,使用中括号包起来,使其失去上面的特性;

      type IsNever<T> = [T] extends [never] ? true : false;
      复制代码
    • isTuple

      我们之前提到过,元组就是数组的元素类型和长度都确定的数组而已,因此他们都具有length这个属性,因此当我们去访问他们的时候,会有不一样的体现方式;

      type TupleLength = [1, 2, 3]["length"]; // 3
      type ArrLength = number["length"]; // number
      复制代码

      可以得到一个结果,那就是元素的 length 属性是一个具体的值类型,但是数组的 length 属性是一个不具体的 number 类型;利用这个点,实现下面这个效果;

      type isTuple<T> = T extends readonly [...params:infer Eles]
        ? NotEqual<Eles['length'] , number>
        : false
      
      type NotEqual<A , B> =
        (<T>()=> T extends A ? 1 : 2)
        extends
        (<T>()=> T extends B ? 12)
        ? false
        : true
      复制代码
    • 过滤可选索引

    一个小知识点

    type TestA = {} extends { name: string } ? true : false; // false
    type TestB = {} extends { name?: string } ? true : false; // true
    复制代码

    一个空对象类型可以分配给一个可选(也就是有可能会有一种情况让这个类型变为一个空对象类型),但不可以分配给一个不可能成为空对象类型的类型;

    于是,就可以做下面这样的判断

    type GetOptional<Obj extends Record<string, any>> = {
      [Key in keyof Obj as {} extends Pick<Obj, Key> ? Key : never]: Obj[Key];
    };
    复制代码
    • keyof 只能拿到 class 的 public 索引,private 和 protected 的索引会被忽略。
  15. 函数重载

    • 函数重载的几种方式
    // 方式一
    declare function Add(a:number , b:number)=>number
    declare function Add(a:sring , b:string)=>string
    
    
    // 方式二
    interface Add {
      (a:number , b:number)=>number
      (a:sring , b:string)=>string
    }
    
    // 方式三
    type Add = (a:number , b:number)=>number & (a:sring , b:string)=>string
    复制代码

二、案例实战

  1. 手写 paramString

需求:接受一个字符串类型,形如a=1&b=2这样的类型,将其转化为{a:1 , b:2}这样的类型

const str = "a=1&b=2&c=3";

// 实现函数
function parse(str: string): object {
  const itemArr = str.split("&");
  const res = {};
  itemArr.forEach((item) => {
    const [key, value] = item.split("=");
    if (res[key]) {
      if (Array.isArray(res[key])) {
        res[key] = [...res[key], value];
      } else {
        res[key] = [res[key], value];
      }
    } else {
      res[key] = value;
    }
  });
  return res;
}

console.log(parse(str));
// 定义类型

type ParseItem<Str extends string> = Str extends `${infer Key}=${infer Value}`
  ? Record<Key, Value>
  : {};

type MergeItem<A, B> = {
  [Key in keyof A | keyof B]: Key extends keyof A
    ? Key extends keyof B
      ? A[Key] extends unknown[]
        ? B[Key] extends unknown[]
          ? [...A[Key], ...B[Key]]
          : [...A[Key], B[Key]]
        : [A[Key], B[Key]]
      : A[Key]
    : Key extends keyof B
    ? B[Key]
    : never;
};

// type MergeValue<A , B> = [A , B]

type ParseStr<Str extends string> =
  Str extends `${infer OneItem}&${infer OtherItem}`
    ? MergeItem<ParseItem<OneItem>, ParseStr<OtherItem>>
    : ParseItem<Str>;

type Test1 = ParseStr<"a=1&b=2&c=3&a=2">;

type Test2 = ParseItem<"a=3">;
复制代码
  1. aaa-bbb-ccc 转为 aaaBbbCcc 这样的类型
  • 补充一个知识点:内置类型Capitalize可以让传入的首字母大写
type Test = Capitalize<"aaa">; // Aaa
复制代码
type KebabCaseToCameCase<Str extends string> =
  Str extends `${infer First}-${infer Rest}`
    ? `${First}${KebaCaseToCameCase<Capitalize<Rest>>}`
    : Str;

// test
type Test = KebabCaseToCameCase<"aaaa-bbb-ccc">; // type Test = "aaaaBbbCcc"
复制代码
  1. aaaBbbCcc 转为 aaa-bbb-ccc 这样的类型
type CameCaseToKebabCase<Str extends string> =
  Str extends `${infer First}${infer Rest}`
    ? First extends Lowercase<First>
      ? `${First}${CameCaseToKebabCase<Rest>}`
      : `-${Lowercase<First>}${CameCaseToKebabCase<Rest>}`
    : Str;

// test
type Test = CameCaseToKebabCase<"aaaBbbCcc">; // type Test = ""aaa-bbb-ccc"
复制代码
  1. 实现一个类型,将[1,2,3,4,5,6 ....] 转变为 [[1,2], [3,4], [5,6] ....]
type Chunk<
  Arr extends unknown[],
  ItemLen extends number,
  CurItem extends unknown[] = [],
  Res extends unknown[] = []
> = Arr extends [infer First, ...infer Rest]
  ? CurItem["length"] extends ItemLen
    ? Chunk<Rest, ItemLen, [First], [...Res, CurItem]> // 说明CurItem上限已到
    : Chunk<Rest, ItemLen, [...CurItem, First], Res>
  : [...Res, CurItem];

// test
type Test = Chunk<[1, 2, 3, 4, 5, 6, 7, 8], 2>; // type Test = [[1, 2], [3, 4], [5, 6], [7, 8]]
复制代码
  1. 将联合类型转为元组类型 将 "a" | "b" | "c" 转为 ["a" , "b" , "c"]

    • 知识点一:如何讲一个联合类型转为交叉类型

    实际上,联合类型在一般的运算当中,都是协变的;也就是说联合类型在进行构建的时候,会将联合类型的每一个元素传入做运算再联合起来;但是如果使用函数参数的话,这种现象会被破坏,结果将不再是所有的结果联合,,这里就是因为函数参数是逆变的,会返回联合类型的几个类型的子类型,也就是更具体的交叉类型。

    type UnitToIntersection<U> = (
      U extends U ? (x: U) => unknown : never
    ) extends (x: infer R) => unknown
      ? R
      : never;
    
    type UnionToFunIntersection<T> = UnitToIntersection<
      T extends any ? () => T : never
    >;
    复制代码
    • 实现:
    type UnionToTuple<T> = UnionToIntersection<
      T extends any ? () => T : never
    > extends () => infer ReturnType
      ? [...UnionToTuple<Exclude<T, ReturnType>>, ReturnType]
      : [];
    复制代码

三、鸣谢

本来这篇文章是不打算发表的,但是想一想,这些笔记可能对看的伙伴还是有帮助的,就好好整理了一下,在这里要特别感谢zxg_神说要有光大佬写的小册,从这本书当中我真的收获很多,也以你为榜样,希望有一天也能和你一样写出高质量有价值的文章来;

欢迎评论区讨论TypeScript的问题,一起努力;

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改