TypeScript 版本特性及技巧

一、版本特性

4.1

  • 模板字符串中可以使用字符串类型
type World = "world";
type Greeting = `hello ${World}`;

// example1
type VerticalAlignment = "top" | "middle" | "bottom";
type HorizontalAlignment = "left" | "center" | "right";
declare function setAlignment(value: `${VerticalAlignment}-${HorizontalAlignment}`): void;

// example2
let person = makeWatchedObject({
  firstName: "Homer",
  age: 42, // give-or-take
  location: "Springfield",
});
person.on("firstNameChanged", () => {
  console.log(`firstName was changed!`);
});

type PropEventSource = {
    on(eventName: `${string & keyof T}Changed`, callback: () => void): void;
};
declare function makeWatchedObject(obj: T): T & PropEventSource;
  • Key Remapping in Mapped Types
// 新语法 k in keyof T as NewKeyType
type MappedTypeWithNewKeys = {
    [K in keyof T as NewKeyType]: T[K]
}
// example1
type Getters = {
    [K in keyof T as `get${Capitalize}`]: () => T[K]
};
interface Person {
    name: string;
    age: number;
    location: string;
}

type LazyPerson = Getters;
// equal to
type LazyPerson = {
  getName: () => string;
  getAge: () => number;
  getLocation: () => string;
}

// example2
type RemoveKindField = {
    [K in keyof T as Exclude]: T[K]
};
interface Circle {
    kind: "circle";
    radius: number;
}
type KindlessCircle = RemoveKindField;
  • Recursive Conditional Types
type ElementType = T extends ReadonlyArray ? ElementType : T;

function deepFlatten(x: T): ElementType[] {
  throw "not implemented";
}

// All of these return the type 'number[]':
deepFlatten([1, 2, 3]);
deepFlatten([[1], [2, 3]]);
deepFlatten([[1], [[2]], [[[3]]]]);
  • --noUncheckedIndexedAccess 默认关闭,不随 --strictNullChecks 打开
interface Options {
  path: string;
  permissions: number;
  [propName: string]: string | number;
}

const opt:Options
opt.xxx  string|number|undefined

// 影响 for 循环,不影响for-of,forEach
function screamLines(strs: string[]) {
  // This will have issues
  for (let i = 0; i < strs.length; i++) {
    console.log(strs[i].toUpperCase());
Object is possibly 'undefined'.
  }
}
  • paths without baseUrl
  • checkJs Implies allowJs
  • Breaking Changes
// resolve’s Parameters Are No Longer Optional in Promises
new Promise((resolve) => {
  resolve() // error
});

// fix 
new Promise((resolve) => {
  resolve() // work
});

4.0

  • 可变元祖类型
    1、元祖类型可以使用扩展运算符了,之前只能对数组类型用
    2、扩展元算符可以用在任意位置了,之前只能用在最后一个参数
type IT = [string,boolean]
function tail(arr: readonly [any, ...IT]) {
  const [_ignored, ...rest] = arr;
  return rest;
}

type Strings = [string, string];
type Numbers = [number, number];
type StrStrNumNumBool = [...Strings, ...Numbers, boolean];

// 不限参数长度
type Strings = [string, string];
type Numbers = number[];
type Unbounded = [...Strings, ...Numbers, boolean];
//   ==> type Unbounded = [string, string, ...(number | boolean)[]]
  • 标记的元祖类型
    下面这种写法
function foo(...args: [string, number]): void {
  // ...
}

是这种提示:


TypeScript 版本特性及技巧_第1张图片

而这种写法的提示语意上更为明确

type IRange = [start: string, end: number];
function foo(...args: IRange): void {
  // ...
}
foo()
TypeScript 版本特性及技巧_第2张图片
  • 构造函数的类属性推断
    现在类属性在构造函数中被初始化后会被推断出类型,之前一直是 any
  • 短路分配运算符
    新加了&&=,||=,和??=
if(!a){
   a=b
}
===>
a||=b
  • catch 的 error 参数类型由 any 转为了 unknown
  • 启动时的部分编译功能
    4.0 支持在 VSCode 启动时优先编译当前工作区打开的文件,而不是等所有的项目都编译一遍才开始提供类型等信息

3.9

  • 修复 Promise.all 类型推断错误问题
interface Lion {
  roar(): void;
}
interface Seal {
  singKissFromARose(): void;
}

async function visitZoo(
  lionExhibit: Promise,
  sealExhibit: Promise
) {
  let [lion, seal] = await Promise.all([lionExhibit, sealExhibit]);
  lion.roar(); // uh oh
  // lion is possibly 'undefined'.
}
  • 提供 ts-expect-error 注释
    与 ts-ignore 的区别在于,如果下一行没有 ts 错误,ts-ignore 啥也不干,但 ts-expect-error 本身会报错
  • 条件表达式中的未调用函数检查
    条件表达式中只写函数名而没加括号会报错
  • 解析可选链接和非null断言中的差异
// before, this transform is a bug
foo?.bar!.baz;  =>  (foo?.bar).baz;  
// now
foo?.bar!.baz;  =>  foo?.bar.baz;  
  • 交叉类型和可选属性的更严格检查
interface A {
  a: number; // notice this is 'number'
}
interface B {
  b: string;
}
interface C {
  a?: boolean; // notice this is 'boolean'
  b: string;
}
declare let x: A & B;
declare let y: C;
// before is ok , now is error
y = x;
  • getter/setter 不再可枚举
  • 类型参数any不再扩展any
function foo(arg: T) {
  arg.spfjgerijghoied; // before is ok , now is  error!
}

3.x

  • 支持 reference 配置
  • ReadonlyArray 类型
    数组内元素不能增加、修改和删除
function foo(arr: ReadonlyArray) {
  arr.slice(); // okay
  arr.push("hello!"); // error!
}

也可写成 readonly string[],同时也支持 tuples 类型:readonly [string,number]

  • const 断言
    as const 可以将一些类型变为只读:
// Type '"hello"'
let x = "hello" as const;

// Type 'readonly [10, 20]'
let y = [10, 20] as const;

// Type '{ readonly text: "hello" }'
let z = { text: "hello" } as const;
  • 3.5 引入 Omit 类型
type Person = {
  name: string;
  age: number;
  location: string;
};

type QuantumPerson = Omit;

// equivalent to
type QuantumPerson = {
  name: string;
  age: number;
};
  • 3.7 引入可选链
// Before
if (foo && foo.bar && foo.bar.baz) {
  // ...
}
// After
if (foo?.bar?.baz) {
  // ...
}
  • 3.7 引入空位合并
let x = foo ?? bar();
// equal to
let x = foo !== null && foo !== undefined ? foo : bar();

注意和 || 运算不同,|| 包含了更多 falsy 类的值,如 false,'',0 等

  • 3.8 引入类型导入和导出
import type { SomeThing } from "./some-module.js";
export type { SomeThing };
  • 3.8 真-私有字段
    之前都是 private,但这只在编译阶段有约束,编译成JS后仍可访问。3.8 的私有字段语法为: #valuableName,编译成JS后无法访问,实现上用到了 WeakMap
class C {
  #foo = 10;
  cHelper() {
    return this.#foo;
  }
}

二、小技巧

  • 枚举 key-value 互取
enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green; // 2
let colorName: string = Color[Color.Green];  // 'Green'
  • Record
    如果定义一个对象类型,直接 Record,而不是用 interface,interface写起来麻烦
  • Partial
    将一个已定义好的接口必选属性改为可选属性,类似的还有 Readonly,Required,Omit,Pick
  • 回调函数类型
type Cb = ()=>void
  • 当为一个默认值定义类型时,可以用 type,而不是 interface。能少写代码
// bad
interface IState{
      loading: boolean;
      data:any[];
}
const defalutState:IState={
    loading:true,
    data:[]
}
// good
const defalutState = {
    loading:true,
    data:[]
}
type IState = typeof defalutState
  • 导入其它非 js 类的文件,需要自己声明模块
declare module "*!text" {
    const content: string;
    export default content;
}
// Some do it the other way around.
declare module "json!*" {
    const value: any;
    export default value;
}

import fileContent from "./xyz.txt!text";
import data from "json!http://example.com/data.json";
  • 类型冲突时,可以用 reference 指定使用哪个类型文件,比如,@types/node 和 @types/webpack-env 这两个文件都声明了 NodeJS,但有时 ts 找不到 webpack-env 里对 NodeJS 的 Module 接口扩展,导致 module.hot 会报错
  • 巧用查找类型
interface Person {
  addr: {
    city: string,
    street: string,
    num: number,
  }
}

当需要使用 addr 的类型时, 可以直接 Person["addr"]

  • 巧用ClassOf
abstract class Animal extends React.PureComponent {
  /* Common methods here. */
}
class Cat extends Animal {}
class Dog extends Animal {}

// `AnimalComponent` must be a class of Animal.
const renderAnimal = (AnimalComponent: Animal) => {
  return ; // WRONG!
}

上面的代码是错的,因为 Animal 是实例类型,不是类本身。应该

interface ClassOf {
  new (...args: any[]): T;
}
const renderAnimal = (AnimalComponent: ClassOf) => {
  return ; // Good!
}

renderAnimal(Cat); // Good!
renderAnimal(Dog); // Good!

  • 为第三方库写类型文件

首先我们需要明确包使用的导出规范,global/umd/commonjs/module 等
对于 global 导出的包我们使用:

declare namesapce MyLib {
  class A {}
  
  // 我们可以直接在代码中使用
  // const a = new MyLib.A()
}

对于 umd/commonjs 导出的包我们使用:

declare module 'my-lib' {
  namespace MyLib {
    class A {}
    
    class B {}

    // 使用时
    // 我们可以使用
    // import * as MyLib from 'my-lib'
    // const a = new MyLib.A();

    // 如果开启了 ES Module 融合模式 (esModuleInterop=true)
    // 我们可以使用
    // import { A } from 'my-lib'
    // const a = new A()
  }
  export = MyLib
}

对于 ES Module 导出的包我们使用:

declare module 'my-lib' {
  class MyLib {}
  
  export default MyLib
  
  // or other exorts
  export class A {}
  
  // 我们可以使用
  // import MyLib, {A} from 'my-lib'
  // const lib = new MyLib()
  // const a = new A()
}
  • 常量枚举和普通枚举区别
    常量枚举在编译阶段会被删除
export const enum T{
    a=1
}
T.a  // 会被编译为 1 /*a*/

enum T{
    a=1
}
T.a 会被编译为
var ITest;
(function (ITest) {
    ITest[ITest["a"] = 1] = "a";
})(ITest || (ITest = {}));
ITest.a
  • 一份较全的 tsconfig.json 配置(编译上下文配置)
{
  "compilerOptions": {

    /* 基本选项 */
    "target": "es5",                       // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', '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          // 为装饰器提供元数据的支持

    /* 编译文件选择 */
    "files":[],
    "include":[],
    "exclude":[],
  }
}

参考

  • https://zhuanlan.zhihu.com/p/58123993
  • https://zhuanlan.zhihu.com/p/39620591
  • Writing a Custom TypeScript AST Transformer

你可能感兴趣的:(TypeScript 版本特性及技巧)