TypeScript技巧和难点

129 阅读11分钟

一、any/unknown/never

any:任意类型,不受任何约束,编译时会跳过类型检查,不推荐使用

unknown:可以理解为更安全的any,所有类型都可以分配给unknown,但是必须要在判断完它是什么类型之后才能继续使用

never:主要用于错误检查或抛出错误

1、unknown的使用

const unknownAge: unknown = 10;

// Error: Type 'unknown' is not assignable to type 'number'
const numberAge: number = unknownAge;

// 判断类型后使用
const age = typeof unknownAge === "number" ? unknownAge : 0;
function magicFunction(param: any) {
  console.log(Math.round(param));
  console.log(param.charAt(0));
  console.log(param.push(1));
}

function magicFunction(param: unknown) {
  // Error: Argument of type 'unknown' is not assignable to parameter of type 'number'
  console.log(Math.round(param));
  
  // Error: 'param' is of type 'unknown'
  console.log(param.charAt(0));
  
  // Error: 'param' is of type 'unknown'
  console.log(param.push(1));
}

// 判断类型后使用
function magicFunction(param: unknown) {
  if (typeof param === "number") {
    console.log(Math.round(param));
  } else if (typeof param === "string") {
    console.log(param.charAt(0));
  } else if (Array.isArray(param)) {
    console.log(param.push(1));
  } else {
    throw new Error("Error");
  }
}

2、never的使用

function fn(msg: string): never {
  throw new Error(msg);
}
// type IconProps = ReactElement | { url: string; onError: () => void };
 
// 使用never避免出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码
// else分支编译会报错:Type 'string' is not assignable to type 'never'
type IconProps = ReactElement | { url: string; onError: () => void } | string;

const renderIcon = (icon: IconProps) => {
  if (isValidElement(icon)) {
    return icon;
  } else if (typeof icon === "object") {
    return <div>***</div>;
  } else {
    const exhaustiveCheck: never = icon;
    throw new Error(`unknown type: ${exhaustiveCheck}`);
  }
}

3、object和Object与{}

object:所有非原始类型,也就是说不能把string、boolean、number、bigint、symbol、null和undefined赋值给object

Object:拥有toString、hasOwnProperty方法的类型,所有原始类型、非原始类型都可以赋给Object。在严格模式下,null和undefined不能赋给Object

{}和Object一样

二、readonly

readonly只能标记基础类型(string number),Readonly<Type>用于标记对象和数组等

三、类型断言

1、非空断言 ! 

用于断言操作对象是非null和非undefined

let x: number;
initialize();

// Error: Variable 'x' is used before being assigned
// console.log(2 * x);

console.log(2 * x!);

function initialize() {
  x = 10;
}

2、const断言

表示对象和数组里面的内容都是常量,不允许修改

const Status = {
  success: 1,
  error: 2,
};

Status.success = 20;

// const Status: {  readonly success: 1;  readonly error: 2;  }
const Status = {
  success: 1,
  error: 2,
} as const;

// Error: Cannot assign to 'success' because it is a read-only property
Status.success = 20;
const arr = [1, 2, 3, 4];
arr[2] = 10;


// const arr: readonly [1, 2, 3, 4]
const arr = [1, 2, 3, 4] as const;
// Error: Cannot assign to '2' because it is a read-only property
arr[2] = 10;

四、类型操作

1、is关键字

2、in关键字

遍历类型

type OptionsFlags<Type> = {
  [Property in keyof Type]: boolean;
};

type Features = {
  darkMode: () => void;
  newUserProfile: () => void;
};

type FeatureOptions = OptionsFlags<Features>;

3、索引访问类型

数组用[number]访问,对象用["属性名"]访问

const MyArray = [
  { name: "Alice", age: 15 },
  { name: "Bob", age: 23 },
  { name: "Eve", age: 38 },
];
// 数组访问类型
type arr = (typeof MyArray)[number];


type arrs = ["Kars", "Esidisi", "Wamuu", "Santana"];
// type arrsKey = "Kars" | "Esidisi" | "Wamuu" | "Santana"
type arrsKey = arrs[number];


const tuple = ["tesla", "model 3", "model X", "model Y"] as const;
// 元组访问类型
// type tupleKey = "tesla" | "model 3" | "model X" | "model Y"
type tupleKey = (typeof tuple)[number];


type Person = {
  name: string;
  age: number;
};
// 对象访问类型
type age = Person["age"];

4、条件类型

type IType = "a" | "b" | "c";
type IType2 = "a" | "c";

// type Diff1 = "a" | "b" | "c" extends "a" | "c" ? never : "a" | "b" | "c"
// type Diff1 = "a" | "b" | "c"
type Diff1 = IType extends IType2 ? never : IType;

5、分布式条件类型

针对extends左边的联合类型会被自动分发

(string | number) extends T ? A : B
// 等价于
(string extends T ? A : B) | (number extends T ? A : B)

(string | number | boolean) extends T ? A : B	
// 等价于
(string extends T ? A : B) | (number extends T ? A : B) | (boolean extends T ? A : B)
type Diff2<T, U> = T extends U ? never : T;

// type T1 = Diff2<"a", "a" | "c"> | Diff2<"b", "a" | "c"> | Diff2<"c", "a" | "c">
// type T1 = "b"
type T1 = Diff2<"a" | "b" | "c", "a" | "c">;

条件类型与分布式条件类型的区别是否有泛型,条件类型没有泛型,分布式条件类型有泛型

6、非裸类型的分布式条件类型

type Diff3<T, U> = [T] extends [U] ? never : T;

// type T2 = ["a" | "b" | "c"] extends ["a" | "c"] ? never : "a" | "b" | "c"
// type T2 = "a" | "b" | "c"
type T2 = Diff3<"a" | "b" | "c", "a" | "c">;

分布式条件类型是有前提的。条件类型中待检查的类型(即extends左边的类型)必须是裸类型。即没有被诸如数组,元组或者函数包裹,没有对T进行取值操作

// 分布式条件类型
type GetSomeType<T extends string | number> = T extends string ? "a" : "b";

// let someTypeThree: string extends string ? "a": "b" | number extends string ? "a": "b"
// let someTypeThree: "a" | "b"
let someTypeThree: GetSomeType<string | number>;


// 非裸类型的分布式条件类型
type GetSomeTypes<T extends string | number> = [T] extends string ? "a" : "b";

// let someTypeThrees: [string | number] extends string ? "a" : "b"
// let someTypeThrees: "b"
let someTypeThrees: GetSomeTypes<string | number>;


// 非裸类型的分布式条件类型
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;

// type StrArrOrNumArr = [string | number] extends [any] ? (string | number)[] : never;
// type StrArrOrNumArr = (string | number)[]
type StrArrOrNumArr = ToArrayNonDist<string | number>;


interface Cat {
  type: "cat";
  breeds: "Abyssinian" | "Shorthair" | "Curl" | "Bengal";
}

interface Dog {
  type: "dog";
  breeds: "Hound" | "Brittany" | "Bulldog" | "Boxer";
  color: "brown" | "white" | "black";
}

// 非裸类型的分布式条件类型
type LookUp<T extends { type: string }, U extends string> = T["type"] extends U
  ? T
  : never;

// type MyDog = "cat" | "dog" extends "dog" ? Cat | Dog : never
// type MyDog = never
type MyDog = LookUp<Cat | Dog, "dog">;

// 分布式条件类型
type LookUp1<T extends { type: string }, U extends string> = T extends unknown
  ? T["type"] extends U
    ? T
    : never
  : never;

// type MyDog1 = Dog
type MyDog1 = LookUp1<Cat | Dog, "dog">;

7、交叉类型 &

如果是基本类型,则取交集,如果是对象,则取对象的属性交集

// type User = never
type User = string & number;


// 没有同名属性交叉
// type IntersectionType = { id: number; name: string; age: number }
type IntersectionType = { id: number; name: string } & { age: number };
const mixed: IntersectionType = {
  id: 1,
  name: "name",
  age: 18,
};

// 有同名属性,且属性为基本类型
// type IntersectionTypeConfict = { id: number; name: never; age: number }
type IntersectionTypeConfict = { id: number; name: string } & {
  age: number;
  name: number;
};
const mixedConflict: IntersectionTypeConfict = {
  id: 1,
  // Type 'number' is not assignable to type 'never'
  name: 2,
  age: 2,
};


// 有同名属性,且属性有交集
// type IntersectionTypeConfict = { id: number; name: 2; age: number }
type IntersectionTypeConfict = { id: number; name: 2 } & {
  age: number;
  name: number;
};

let mixedConflict: IntersectionTypeConfict = {
  id: 1,
  name: 2,
  age: 2,
};
mixedConflict = {
  id: 1,
  // Type '22' is not assignable to type '2'
  name: 22,
  age: 2,
};


interface A {
  x: { d: true };
}
interface B {
  x: { e: string };
}
interface C {
  x: { f: number };
}
// 有同名属性,且属性为对象
// type ABC = { x: { d: true; e: string; f: number } }
type ABC = A & B & C;
let abc: ABC = {
  x: {
    d: true,
    e: "",
    f: 666,
  },
};

8、数字分隔符_

let num:number = 1_2_345.6_78_9

// 编译成JS
"use strict";
let num = 12345.6789;

五、infer

infer代表待推断类型,必须和extends条件约束类型一起使用

// 函数参数类型推断为P
type ParamType<T> = T extends (...args: infer P) => any ? P : T;

type GetAge = (name: string, age: number) => void;

// type AgeParamType = [name: string, age: number]
type AgeParamType = ParamType<GetAge>;


// 数组中的元素类型推断为Item
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;


// 函数返回值的类型推断为Return
type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
  ? Return
  : never;

// type Num = number
type Num = GetReturnType<() => number>;

// type Str = string
type Str = GetReturnType<(x: string) => string>;

六、extends

如果是联合类型使用extends,会把每个类型拆分出来单独比较,如果是对象使用extends,会把对象作为一个整体

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

type string1 = "a" | "b" | "c";
type srring2 = "a" | "d";

// type str = "b" | "c"
type str = Exclude<string1, srring2>;


type obj1 = {
  name: string;
  age: number;
};

type obj2 = {
  name: string;
  sex: string;
};

// type obj = {  name: string;  age: number;  }
type obj = Exclude<obj1, obj2>;


type obj3 = {
  name: string;
  age: number;
};

type obj4 = {
  name: string;
};

// type obj5 = never
type obj5 = Exclude<obj3, obj4>;

七、元组

元组和数组的区别是:元组的数量和类型是确定的

let employee: [string, number];

// 类型必须匹配且数量必须为2
employee = ["hello", 10];


// 元组类型的可选元素
type Point = [number, number?, number?];

const x: Point = [10]; // 一维坐标点
const xy: Point = [10, 20]; // 二维坐标点
const xyz: Point = [10, 20, 10]; // 三维坐标点


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


// 只读的元组类型
const point: readonly [number, number] = [10, 20];

八、字面量类型

字面量不仅可以表示值,还可以表示类型,即所谓的字面量类型。3种类型:字符串字面量类型、数字字面量类型、布尔字面量类型,定义单个的字面量类型并没有太大的用处,主要应用场景是可以把多个字面量类型组合成一个联合类型

let specifiedStr: "this is string" = "this is string";
let specifiedNum: 1 = 1;
let specifiedBoolean: true = true;

interface Config {
  size: "small" | "big";
  isEnable: true | false;
  margin: 0 | 2 | 4;
}

const定义的类型为字面量类型

// let str: string
let str = "this is string";

// const specifiedStr: "this is string"
const specifiedStr = "this is string";

九、接口与类型

官方推荐用interface,其他无法满足需求的情况下用type

1、相同点

都可以描述Object和Function

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

type SetPoint = (x: number, y: number) => void;
interface Point {
  x: number;
  y: number;
}

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

都可以被继承

// interface继承interface
interface Person {
  name: string;
}

interface Student extends Person {
  stuNo: number;
}
  
  
// interface继承type
type Person = {
  name: string;
};

interface Student extends Person {
  stuNo: number;
}
  
  
//type继承type
type Person = {
  name: string;
};

type Student = Person & { stuNo: number };
  
  
//type继承interface
interface Person {
  name: string;
}

type Student = Person & { stuNo: number };

类可以实现interface以及type(除联合类型外)

interface ICat {
  setName(name: string): void;
}

class Cat implements ICat {
  setName(name: string): void {
    // todo
  }
}


type ICat = {
  setName(name: string): void;
};

class Cat implements ICat {
  setName(name: string): void {
    // todo
  }
}

2、不同点

type可以定义基本类型,联合类型,元组

// 基本类型
type userName = string;
type stuNo = number;

// 联合类型
type Student = { stuNo: number } | { classId: number };

// 元组
type Data = [number, string];

多次声明一个同名的interface

interface Point { x: number; }
interface Point { y: number; }
const point: Point = { x: 1, y: 2 };

十、泛型

1、泛型常见字母

T:代表Type,定义泛型时通常用作第一个类型变量名称

K:代表Key,表示对象中的键类型;

V:代表Value,表示对象中的值类型;

E:代表Element,表示的元素类型;

十一、枚举

enum device {
  phone,
  notebook,
}

// 编译成JS
"use strict";
var device;
(function (device) {
  device[device["phone"] = 0] = "phone";
  device[device["notebook"] = 1] = "notebook";
})(device || (device = {}));


enum deviceStr {
  phone = "1",
  notebook = "2",
}

// 编译成JS
"use strict";
var deviceStr;
(function (deviceStr) {
  deviceStr["phone"] = "1";
  deviceStr["notebook"] = "2";
})(deviceStr || (deviceStr = {}));

const枚举,会在编译阶段删除该对象,且不能访问该枚举对象(对象被删除啦),只能访问该枚举对象成员,慎用

const enum obj {
  A = 1,
  B = "18",
}

// 编译成JS为空

// Error:'const' enums can only be used in property or index access expressions or the right hand side of an import declaration or export assignment or type query.
console.log(obj);
console.log(obj.A, obj.B);

十二、tsconfig.json

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

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

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

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          // 为装饰器提供元数据的支持  
}

十三、参考资料

1.TS 在项目中的N个实用小技巧 - 文字稿

2.TypeScript常用的新玩法

3.2021TypeScript史上最强学习入门文章(2w字)

4.TypeScript中interface和type的区别,你真的懂了吗

5.如何进阶TypeScript功底?一文带你理解TS中各种高级语法

6.[1.9W字总结]一份通俗易懂的TS教程,入门+实战

7.一篇让你完全够用TS的指南

8.聊聊TypeScript中枚举对象(Enum)

9.死磕TypeScript高级技巧之全网终极总结

10.TypeScript 终极初学者指南

11.TS的脚步已经拦不住,代码撸起来