Genluo / Precipitation-and-Summary

梳理体系化的知识点&沉淀日常开发和学习
MIT License
16 stars 1 forks source link

TypeScript团队分享 #94

Open Genluo opened 4 years ago

Genluo commented 4 years ago

介绍

TypeScript是由微软开发的一个JavaScript超集,本质是向javascript这门弱类型动态语言添加了静态类型和面向对象。作者是安德斯-海尔斯伯格,C#之父。从发布至今,typescript一直倍受关注,star数直线上升,并且被越来越多的个人和组织认可,社区也积极跟进,目前我们常用的React、Vue、NestJS等等都对TS有了很好的支持

优势

1. 降低代码风险

使用TS带来的静态类型检查能力能够检查出很多错误,可以在开发时间检测出很多问题,提高项目上线的稳定性,但是需要避免项目成为“AnyScript”

2. Code Document

随着项目的演进和发展,code中提供的类型标注的好处才能凸显出来。类型就是代码中最好的注释,本质上相当于强制约定写了一份文档,同时Ts中的Language Server针对JSdoc也提供良好的提示功能,一些良好的开源库其中80%都是代码注释

3. 超集

只要是合法的JavaScript语言类型,也就一定是合法的TypeScript,同时使用TypeScript可以提前享受ES将来支持或者不支持的一些新的特性,因为最终都会将TypeScript代码转化成JavaScript代码

4. 辅助代码设计和代码重构

  1. 可以尝试面向接口编程,设计接口就相当于设计代码结构
  2. 在进行代码重构的时候:
    1. 强类型和静态类型检查会帮上大忙,通过这两个能力可以很快发现一些接口或者参数不兼容的情况
    2. 能够提前暴露问题。同时由于vscode对TS的支持,相较于JS带来更好的重构支持

5. IDE智能提示功能

TypeScript、VsCode都是来自微软,VsCode也是使用TypeScript开发的,所以VsCode上两者有着非常好的结合

成本

经过统计,大多数人不想尝试TypeScript的问题有下面几点:

  1. 请求动态数据接口定义的编写
  2. 复杂的项目配置,接入TS的同时需要为其他相关配套工具进行配置,成本较高
  3. 工程化和研发效率的问题构成了较高的ts研发成本,成本就意味着收益降低,效率和成本的博弈成为开发者采用的TS的主要思考点

1. 数据接口定义的编写

(1) 利用TypeScript中的语法减少数据接口的定义

// get-user.ts
export const getUser = (userId: string): Promise<{ id: string }> => {
    return new Promise(resolve => {
        resolve({ id: userId })
    })
}

// biz.ts
type Unwrap<T> = T extends Promise<infer U>
    // 如果类型匹配是Promise类型
    ? U
    // 如果匹配是一个返回Promise的方法
    : T extends (...args: any) => Promise<infer U>
    ? U
    // 如果返回是一个返回非Promise的方法
    : T extends (...args: any) => infer U
    ? U
    // 其他未知情况
    : T

import { getUser } from './get-user'

// 获取getUser的返回类型
type User = Unwrap<typeof getUser>

const setUser = (user: User) => {
    console.log(user)
}

getUser('123456').then((user: User) => {
    setUser(user)
})

(2) 通过语言协议,直接生成API的定义

image

(3) 前后端一体项目

前后端在同一个仓库中,服务端定义的接口可以直接在客户端中使用,TS更加适合这种场景

2. 复杂的项目配置

当我们在项目中使用到webpack、babel、jest、eslint这些通用的东西,需要额外进行配置使其支持TypeScript,这种没有特别好的方式,但是目前我们可以使用脚手架封装这些处理,比如现有的:

3. 工程化和研发效率问题

首先不可否认,项目采用TypeScript开发带来的复杂度,短期来看,首次使用TypeScript的成本比较高,但是长期来看,同时根据项目的特点不同,使用TypeScript甚至可以提升项目的研发效率,具体来看,有下面者几点来考虑:

image

开发实践

1. 代码测试(Jest)

(1) 测试文件支持ES6+

如果想在测试代码中使用ES6+相关规范,则需要配置babel,首先安装相关babel库:

  1. 安装:npm instal -D babel-core babel-preset-env babel-jest

  2. 添加babel的配置文件,内容如下:

// babel.config.json
{
  "presets": ["@babel/preset-env"]
}
  1. 修改Jest的配置,需要使用babel-jest来测试相关代码。Jest中的配置文件需要相关配置如下:
// jest.config.js
{
  "jest": {
      transform: {
        "^.+\\.jsx?$": "babel-jest"
      },
    }
}

(2) 测试文件支持TypeScript测试

jest使用文档

如果想要使用jest来测试TypeScript代码,需要首先使用babel处理TypeScript

  1. 安装: npm install -D @babel/preset-typescript
  2. 配置babel对js的解析,相关配置如下
// babel.config.js
{
  "presets": ["@babel/preset-env", "@babel/preset-typescript"]
}

如果需要直接在运行时测试你的代码,可以直接使用 ts-jest,同时因为Jest默认不能够导入Ts文件,所以同时还需要对Jest进行一些配置

  1. 安装:npm install ts-jest @types/jest --save-dev
  2. 配置Jest的测试文件入口,内容如下:
// jest.config.js
module.exports = {
    "moduleFileExtensions": [
      "js",
      "jsx",
      "json",
      "ts",
      "tsx"
    ],
    transform: {
      "^.+\\.jsx?$": "babel-jest",
      "^.+\\.tsx?$": "ts-jest"
    },

    "testMatch": [
        "**/__tests__/**/*.(js|ts)?(x)",
        "**/?(*.)(spec|test).(js|ts)?(x)"
     ]
}

2. VsCode插件推荐

3. Tips

(1) 代码注释

通过/* /形式的注释可以给TS类型做标记,编辑器会有更好的提示

image

(2) 显式泛型

函数泛型不一定的非得自动推导出来,有时候显示指定类型就好,同时显示制定泛型可以有效缩小类型,具体代码如下:

image

(3) Type和interface的选用

(4) 通过is进行类型推断

类型保护允许你使用更小范围下的对象类型。目前TypeScript中支持使用

function isString(val: any): boolean {
  return typeof val === 'string'
}
 // 编译不报错,在运行时报错,foo 没有 toSome 方法
function example(foo: any) {
  if (isString(foo)) {
    console.log('a string' + foo)
    console.log(foo.length)
    console.log(foo.toSome(2))
  }
}

 // 编译时报错,运行时报错
function isString(val: any): val is string {
  return typeof val === 'string'
}

function example(foo: any) {
  if (isString(foo)) {
    console.log('it is a string' + foo)
    console.log(foo.length)
    console.log(foo.toSome(2))
  }
}

高度抽象的自定义类型保护函数

image

(5) 使用interface声明多态函数

export interface Connect {
    // tslint:disable:no-unnecessary-generics
    (): InferableComponentEnhancer<DispatchProp>;

    <TStateProps = {}, no_dispatch = {}, TOwnProps = {}, State = DefaultRootState>(
        mapStateToProps: MapStateToPropsParam<TStateProps, TOwnProps, State>
    ): InferableComponentEnhancerWithProps<TStateProps & DispatchProp, TOwnProps>;

    <no_state = {}, TDispatchProps = {}, TOwnProps = {}>(
        mapStateToProps: null | undefined,
        mapDispatchToProps: MapDispatchToPropsNonObject<TDispatchProps, TOwnProps>
    ): InferableComponentEnhancerWithProps<TDispatchProps, TOwnProps>;
}

(6) 擅用映射类型和索引类型

索引类型:编译器就能检查使用了动态属性名的代码

  1. 索引类型查询操作符(keyof T)
  2. 索引访问操作符(T[K])
  3. 泛型约束(K extends T)
let person = {
    name: 'musion',
    age: 35
}
type Person = typeof person;

function getValues(person: Person, keys: string[]) {
    return keys.map(key => person[key])
}

console.log(getValues(person, ['name', 'age'])) // ['musion', 35]
console.log(getValues(person, ['gender'])) // [undefined]
function getValues<T, K extends keyof T>(person: T, keys: K[]): T[K][] {
  return keys.map(key => person[key]);
}

interface Person {
    name: string;
    age: number;
}

const person: Person = {
    name: 'musion',
    age: 35
}

getValues(person, ['name']) // ['musion']
getValues(person, ['gender']) // 报错:
interface API {
  '/user': { name: string },
  '/menu': { foods: Food[] },
}
const get = <URL extends keyof API>(url: URL): Promise<API[URL]> => {
  return fetch(url).then(res => res.json())
}

映射类型:从旧的类型创建新类型的中方式

type Record<K extends keyof any, T> = {
    [P in K]: T;
};
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;

(7) 类的类型定义

interface Node {
    name: string;
}
class Node {
    id: string;
    constructor() {
        this.name='name';
        this.id = 'id';
    }
}

const a:Node = {
    name: 'name',
    id: 'id'
}
interface RenderConstructor {
    new (data: Data[], helpFunMap: HelpFun): RenderInterface;
}
interface RenderInterface {
    renderAst(ast: Node): any;
}
type ParamType = any;
type OptionsType = { [key: string]: any};

const Render: RenderConstructor = class Render implements RenderInterface {}

(8) React在TypeScript中的使用指南

interface IProps {
  color: string,
  size?: string,
}
interface IState {
  count: number,
}
class App extends React.PureComponent<IProps, IState> {
  public readonly state: Readonly<IState> = {
    count: 1,
  }
  public render () {
    return (
      <div>Hello world</div>
    )
  }
  public componentDidMount () {
    this.state.count = 2
  }
}
interface Classof<T> {
    new (...args[]): T
}

新特性

1. 可变元祖类型

type Arr = readonly any[];

function concat<T extends Arr, U extends Arr>(arr1: T, arr2: U): [...T, ...U] {
  return [...arr1, ...arr2];
}

function tail<T extends any[]>(arr: readonly [any, ...T]) {
  const [_ignored, ...rest] = arr;
  return rest;
}

// 类型解构
type Strings = [string, string];
type Numbers = [number, number];

// [string, string, number, number]
type StrStrNumNum = [...Strings, ...Numbers];

2. 元组标记

type Range = [start: number, end: number];

3. 短路赋值语法

a &&= b; // a = a && b
a ||= b; // a = a || b
a ??= b; // a = a ?? b

4. catch error unknown

try {
  // ...
} catch (e: unknown) {
  // error!
  // Property 'toUpperCase' does not exist on type 'unknown'.
  console.log(e.toUpperCase());

  if (typeof e === "string") {
    // works!
    // We've narrowed 'e' down to the type 'string'.
    console.log(e.toUpperCase());
  }
}

5. 其他升级

局部 TS Server 快速启动功能, 打开大型项目时,TS Server 要准备很久,Typescript 4 在 VSCode 编译器下做了优化,可以提前对当前打开的单文件进行部分语法响应。

优化自动导入, 现在 package.json,dependencies 字段定义的依赖将优先作为自动导入的依据,而不再是遍历 node_modules 导入一些非预期的包。

TS架构(self-hosting)

image

核心编译器部分,其中主要完成 Parseing、Type Checking、Transformation of your TypeScript code to JavaScript code

在核心编译器上的基础上提供批量编译命令,它能针对不同运行时采取不同的文件读写策略,比如tsc命令实际就是TS提供的独立编译器,他会处理我们命令行中指定的文件,然后送入核心的编译器中进行编译

在核心编译器基础上额外提供批量编译命令,语言服务支持典型的编辑器操作,语言服务被设计用来专门处理这样的场景:在长时间存在的编译上下文中,源文件会随着时间不断的变化。从这个角度来说,相比于市面上的其他编译器接口,语言服务在对待程序和源码的处理方式上提供了较为不同视角。包括:( It generates information that helps editors and other tools provide better assistance features such as IntelliSense or automated refactoring)

  1. 自动补全、函数签名提示、格式化和亮度、着色等
  2. 基本重构功能,比如重命名
  3. 调试接口助手,断点验证等
  4. TS的增量编译

独立服务器对编译器和语言服务层进行封装,对外暴露了一种基于JSON协议的,称之为语言服务协议(LSP)VsCode就是一个典型的使用语言服务的编辑器,它通过LSP和语言服务通信,从而实现良好的编码体验

1. 灵活的结构化类型系统?

目前有两种类型系统,标明类型系统和结构化类型系统,TypeScript采用的结构化类型系统,也被称之为鸭子类型,因为 JavaScript 本身的动态性,TypeScript 中的类型更像是一种「约束」,它尊重已有的 JavaScript 设计范式,同时尽可能添加一点静态约束,这种约束不会影响到代码的表达能力。或者说,TypeScript 会以 JavaScript 的表达能力为先、以 JavaScript 的运行时行为先,而静态约束则次之。所以开发者的学习成本并不是很高,因为几乎每个特性都可以对应JavaScript社区中一种常见的范式。在JavaScript中我们通常只关注一个对象是否我们需要的属性和方法,这种类型系统不再关注一个变量被标称的类型(由哪一个构造器构造),而是 在进行类型检查时,将对象拆开,抽丝剥茧,逐个去比较组成这个对象的每一个不可细分的成员。如果一个对象有着一个类型所要求的所有属性或方法,那么就可以当作这个类型来使用。

编程语言的类型系统总是需要在灵活和复杂、简单和死板之间做出权衡,TypeScript 则给出了一个完全不同的答案 —— 将编译期的检查和运行时的行为分别看待。这是 TypeScript 饱受争议的一点,有人认为这样非常没有安全感,即使通过了编译期检查在运行时依然有可能得到错误的类型,也有人认为 这是一个非常切合工程实际的选择 —— 你可以用 any 来跳过类型检查,添加一些过于复杂或无法实现的代码,虽然这破坏了类型安全,但确实又解决了问题。

核心编译器主要由5部分来组成,分别如下:

  1. 词法扫描器(scanner.js):分析源文件,生成对应的token流

  2. 语法解析器(parser):根据 TS 语法,从一系列源文件生成对应的抽象语法树

  3. 类型联合器(binder):合并同一类型名称的所有声明,例如在不同文件中的同名接口,这使得类型系统可以直接使用合并后的类型。

  4. 类型检查器(Checker):解析每种类型结构,检查语义并生成恰当的检查结果。

  5. 代码生成器(Emitter):把 .ts.d.ts 文件转换成 .js.d.ts.map 等文

    预处理器(Pre-processor):编译上下文(Compilation)指的是和程序相关的所有文件。编译器会按序检查所有传入的待编译入口文件,然后会把这些文件中直接或间接 import 的文件,以及 /// <reference path=... /> 指向的文件,纳入到编译过程,从而构成了最终的编译上下文。通过遍历文件索引图,会得到一个已排序的源文件列表,这些文件列表就构成了整个应用程序。在解析 import 时,编译器会优先查找 .ts.d.ts 文件,以确保处理的是最新的文件。编译器默认使用跟 Node.js 类似的模块定位方式,它会逐路径往上查找能匹配到指定模块名的 .ts.d.ts 文件。如果没有定位到对应的模块,编译器也不一定抛出错误,因为该模块可能在环境模块中被声明了,比如 path 等 Node.js 内置模块。

babel中的代码处理流程:

源码 ~~扫描器~~> Tokens ~~解析器~~> AST ~~发射器~~> JavaScript

TypeScript中的代码处理流程:

  1. 生成AST语法树
源码 ~~ 扫描器 ~~> Token 流 ~~ 解析器 ~~> AST
  1. 生成符号流
AST ~~ 绑定器 ~~> Symbols(符号)
  1. 类型验证
AST + 符号 ~~ 检查器 ~~> 类型验证
  1. 输出JavsScript
AST + 检查器 ~~ 发射器 ~~> JavaScript 代码

一、 预处理器会找出所有 import 语句 和 reference 指令所依赖的文件,并把它们都列为待编译文件。

二、 解析器解析所有待编译文件,生成 AST Node 。这仅仅是以树的形式来抽象表示待编译文件。SourceFile 对象除了是表示文件的 AST 外,还有额外的信息,比如文件名、源码等。不过此时的 SourceFile 并没有包含类型信息。

三、 类型联合器遍历 AST ,生成并绑定 Symbol 。每个具名实体类型都会创建一个 Symbol。要注意的是,不同的多个声明节点可能有相同的类型名称。这就意味着不用的 Node 可以有相同的 Symbol,每个 Symbol 会跟踪所有跟它有关的 Node 。举例来说,对于相同名称的 classinterface ,它们的类型会合并,并且指向相同的 Symbol 。类型联合器也会处理好作用域,以确保每个 Symbol 处于正确的作用域范围内。

四、 生成 Symbol 之后,通过调用 createSourceFile 就可以生成具有 SymbolSourceFile 了。不过,Symbol 表示的是单个文件中的具名实体类型,由于来自多个文件的同名类型声明可以合并,因此下一步需要通过 Program 对象来构建一个囊括所有文件的全局 Symbol 视图。

五、 Program 使用 createProgram 接口生成,它包括所有 SourceFile 以及 CompilerOptions

六、 针对 Program 创建一个 TypeChecker ,它是 TS 类型系统的核心。它主要负责理清来自多个文件的 Symbol 之间的关系,绑定 TypeSymbol,以及生成语义诊断信息(比如错误信息)。具体来说,TypeChecker 做的第一件事是把来自不同 SourceFileSymbol 整合到单个视图中,然后会创建一张 Symbol 表,用来记录所有的 Symbol ,来自不同文件的同名 Symbol 会在这个记录过程中完成合并。一旦 TypeChecker 完成初始化,它就可以处理关于当前 Program 的任何类型问题了

2. 如何和VsCode结合实现良好的语言提示,代码重构能力?

参考

Genluo commented 4 years ago

:fire: 如何在 window 对象上显式设置属性(声明合并)

对于使用过 JavaScript 的开发者来说,对于 window.MyNamespace = window.MyNamespace || {}; 这行代码并不会陌生。为了避免开发过程中出现冲突,我们一般会为某些功能设置独立的命名空间。

然而,在 TS 中对于 window.MyNamespace = window.MyNamespace || {}; 这行代码,TS 编译器会提示以下异常信息:

Property 'MyNamespace' does not exist on type 'Window & typeof globalThis'.(2339)

以上异常信息是说在 Window & typeof globalThis 交叉类型上不存在 MyNamespace 属性。那么如何解决这个问题呢?最简单的方式就是使用类型断言:

(window as any).MyNamespace = {};

虽然使用 any 大法可以解决上述问题,但更好的方式是扩展 lib.dom.d.ts 文件中的 Window 接口来解决上述问题,具体方式如下:

declare interface Window {
  MyNamespace: any;
}

window.MyNamespace = window.MyNamespace || {};

下面我们再来看一下 lib.dom.d.ts 文件中声明的 Window 接口:

/**
 * A window containing a DOM document; the document property 
 * points to the DOM document loaded in that window. 
 */
interface Window extends EventTarget, AnimationFrameProvider, GlobalEventHandlers, 
  WindowEventHandlers, WindowLocalStorage, WindowOrWorkerGlobalScope, WindowSessionStorage {
    // 已省略大部分内容
    readonly devicePixelRatio: number;
    readonly document: Document;
    readonly top: Window;
    readonly window: Window & typeof globalThis;
    addEventListener(type: string, listener: EventListenerOrEventListenerObject, 
      options?: boolean | AddEventListenerOptions): void;
    removeEventListener<K extends keyof WindowEventMap>(type: K, 
      listener: (this: Window, ev: WindowEventMap[K]) => any, 
      options?: boolean | EventListenerOptions): void;
    [index: number]: Window;
}

在上面我们声明了两个相同名称的 Window 接口,这时并不会造成冲突。TypeScript 会自动进行接口合并,即把双方的成员放到一个同名的接口中。

Genluo commented 4 years ago

:fire: 函数重载

函数重载或方法重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力。

function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
function add(a: Combinable, b: Combinable) {
  // type Combinable = string | number;
  if (typeof a === 'string' || typeof b === 'string') {
    return a.toString() + b.toString();
  }
  return a + b;
}

类方法重载

class Calculator {
  add(a: number, b: number): number;
  add(a: string, b: string): string;
  add(a: string, b: number): string;
  add(a: number, b: string): string;
  add(a: Combinable, b: Combinable) {
  if (typeof a === 'string' || typeof b === 'string') {
    return a.toString() + b.toString();
  }
    return a + b;
  }
}

const calculator = new Calculator();
const result = calculator.add('Semlinker', ' Kakuqo');

这里需要注意的是,当 TypeScript 编译器处理函数重载时,它会查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。 因此,在定义重载的时候,一定要把最精确的定义放在最前面。另外在Calculator 类中,add(a: Combinable, b: Combinable){ } 并不是重载列表的一部分,因此对于 add 成员方法来说,我们只定义了四个重载方法。

Genluo commented 4 years ago

😐object 和 Object 和 {} 之间有什么区别

object 类型是:TypeScript 2.2 引入的新类型,它用于表示非原始类型。 Object 类型:它是所有 Object 类的实例的类型,它由以下两个接口来定义:

// node_modules/typescript/lib/lib.es5.d.ts
interface ObjectConstructor {
  /** Invocation via `new` */
  new(value?: any): Object;
  /** Invocation via function calls */
  (value?: any): any;
  readonly prototype: Object;
  getPrototypeOf(o: any): any;
  // ···
}

declare var Object: ObjectConstructor;

{} 类型:类型描述了一个没有成员的对象。当你试图访问这样一个对象的任意属性时,TypeScript 会产生一个编译时错误

Genluo commented 4 years ago

TypeScript 操作符

1. ! 非空断言操作符

在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符 ! 可以用于断言操作对象是非 null 和非 undefined 类型。具体而言,x! 将从 x 值域中排除 null 和 undefined 。同时这个操作符也用于下面这种情况:

let x!: number; // 明确x已被赋值,负责下方console调用会出现:Variable 'x' is used before being assigned.(2454)
initialize();
console.log(2 * x); // Ok

function initialize() {
  x = 10;
}
function onClick(callback?: () => void) {
  callback!();      // 参数是可选入参,加了这个感叹号!之后,TS编译不报错,但是编译之后并不会做任何防空判断,仅仅确保编译不报错

这个符号的场景,特别适用于我们已经明确知道不会返回空值的场景,从而减少冗余的代码判断,如 React 的 Ref。

function Demo(): JSX.Elememt {
  const divRef = useRef<HTMLDivElement>();
  useEffect(() => {
    divRef.current!.scrollIntoView();    // 当组件Mount后才会触发useEffect,故current一定是有值的
  }, []);
  return <div ref={divRef}>Demo</div>
}

2. ?. 运算符 (Optional Chaining)

TypeScript 3.7 实现了呼声最高的 ECMAScript 功能之一:可选链(Optional Chaining)。有了可选链后,我们编写代码时如果遇到 null 或 undefined 就可以立即停止某些表达式的运行。可选链的核心是新的 ?. 运算符

3. ??空值合并运算符

当左侧操作数为 null 或 undefined 时,其返回右侧的操作数,否则返回左侧的操作数

3. ?: 可选属性

interface Person {
  name: string;
  age?: number;
}

let lolo: Person  = {
  name: "lolo"  
}

4. _ 数字分隔符

TypeScript 2.7 带来了对数字分隔符的支持,正如数值分隔符 ECMAScript 提案中所概述的那样。对于一个数字字面量,你现在可以通过把一个下划线作为它们之间的分隔符来分组数字:

const inhabitantsOfMunich = 1_464_301;
const distanceEarthSunInKm = 149_600_000;
const fileSystemPermission = 0b111_111_000;
const bytes = 0b1111_10101011_11110000_00001101;

但是javascript中的函数并不能处理数字分隔符,需要进行解析

5. ??= 判断复制符

let a = false;
a ??= true;
console.log(a); // false

let b: any = undefined;
b ??= false;
console.log(b); // false

let c: any = null;
c ??= false;
console.log(c); // false

完整文章

作者:阿宝哥 链接:https://juejin.im/post/6875091047752400910 来源:掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Genluo commented 3 years ago

数组 转换 成 union

const ALL_SUITS = ['hearts', 'diamonds', 'spades', 'clubs'] as const; // TS 3.4
type SuitTuple = typeof ALL_SUITS; // readonly ['hearts', 'diamonds', 'spades', 'clubs']
type Suit = SuitTuple[number];  // union type : 'hearts' | 'diamonds' | 'spades' | 'clubs'

ts3.4 新语法 as const创建不可变(常量)元组类型/数组,所以TypeScript可以安全地采用窄文字类型['a', 'b']而不是更宽('a' | 'b')[]或甚至string[]类型

作者:晓黑板前端技术 链接:https://juejin.im/post/6895538129227546638 来源:掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Genluo commented 3 years ago

相关React上的应用

React Hooks in TypeScript

Genluo commented 3 years ago

业务开发过程中常用的类型

Genluo commented 3 years ago

工具类型

1. 类型函数执行

export type ConditionalKeys<Base, Condition> = NonNullable<
    // Wrap in `NonNullable` to strip away the `undefined` type from the produced union.
    {
        // Map through all the keys of the given base type.
        [Key in keyof Base]:
            // Pick only keys with types extending the given `Condition` type.
            Base[Key] extends Condition
                // Retain this key since the condition passes.
                ? Key
                // Discard this key since the condition fails.
                : never;

    // Convert the produced object into a union type of the keys which passed the conditional test.
    }[keyof Base]
>;

2. opaque类型

Read more about opaque types.

Genluo commented 3 years ago

【问题】索引记录

1. 源码复现

// 控制器属性
export interface ControllerProps {
  event?: {
    /**
     * 控制条点击触发时间
     *
     * @remarks
     * 如果controller=true,只提示点击操作
     */
    onOperation: OnOperation;
  };
}

// 播放器属性
export interface PlayProps {
  event?: VideoNativeEvent;
}

// native播放器事件
export interface VideoNativeEvent {
  onFovChanged?: (data: Promise<Event<FovChangeData>>) => void;
  onPrepared?: () => void;
  onPlaying?: () => void;
  onFinish?: () => void;
  onEnded?: () => void;
  onError?: () => void;
  onPause?: () => void;
}

type EventCompose<EventList extends Record<string, (...args: any) => any>, EventNames extends keyof EventList> = {
  [EventName in EventNames]: EventList[EventName];
};

export function getEventCompose(
  type: EventType.controllerEvent,
): EventCompose<ControllerProps['event'], keyof ControllerProps['event']>; // 成功
export function getEventCompose(
  type: EventType.playerEvent,
): EventCompose<PlayProps['event'], keyof PlayProps['event']>; // 报错

2. 报错详情

image

3. 原因说明

  1. interface定义的是接口,代表着鸭子类型,只需要实现其中的定义即可进行赋值,在这个情况下不确定PlayProps['event'] 对应是否还有其他索引值,所以这不能赋值给Record<string, (...args: any) => any>

  2. ControllerProps['event']代表的是一个对象,对象的属性是固定的,所以可以赋值给Record<string, (...args: any) => any>,不会有任何报错

3. 解决方法

  1. VideoNativeEvent从interface变为 type
  2. 添加索引,这样进行定义:event?: VideoNativeEvent & {[index: string]: any};
Genluo commented 2 years ago

TypeScript声明的三种来源

tsc 在编译的时候,会分别加载 lib 的,@types 下的,还有 include 和 files 的文件,进行类型检查。这就是 ts 类型声明的三种来源。

TypeScript 有三种存放类型声明的地方:

其中,npm 包也可以同时存放 ts 类型,通过 packages.json 的 types 字段指定路径即可。

常见的是 vue 的类型是存放在 npm 包下的,而 react 的类型是在 @types/react 里的。因为源码一个是 ts 写的,一个不是。

巧合的是,TS 声明模块的方式也是三种:

Genluo commented 1 year ago

Typescript 类型编程