首页

TypeScript 运行时类型检查指南

seo达人

为什么需要额外的类型检查?

TypeScript 只在编译期执行静态类型检查!实际运行的是从 TypeScript 编译的 JavaScript,这些生成的 JavaScript 对类型一无所知。编译期静态类型检查在代码库内部能发挥很大作用,但对不合规范的输入(比如,从 API 处接收的输入)无能为力。


运行时检查的严格性

至少需要和编译期检查一样严格,否则就失去了编译期检查提供的保证。

如有必要,可以比编译期检查更严格,例如,年龄需要大于等于 0。

运行时类型检查策略

定制代码手动检查

灵活

可能比较枯燥,容易出错

容易和实际代码脱节

使用校验库手动检查

比如使用 joi:


import Joi from "@hapi/joi"const schema = Joi.object({    firstName: Joi.string().required(),    lastName: Joi.string().required(),    age: Joi.number().integer().min(0).required()});

灵活

容易编写

容易和实际代码脱节

手动创建 JSON Schema

例如:


{  "$schema": "http://json-schema.org/draft-07/schema#",  "required": [    "firstName",    "lastName",    "age"  ],  "properties": {    "firstName": {      "type": "string"    },    "lastName": {      "type": "string"    },    "age": {      "type": "integer",      "minimum": 0    }  }}

使用标准格式,有大量库可以校验。

JSON 很容易存储和复用。

可能会很冗长,手写 JSON Schema 可能会很枯燥。

需要确保 Schema 和代码同步更新。

自动创建 JSON Schema

基于 TypeScript 代码生成 JSON Schema

-- 比如 typescript-json-schema 这个工具就可以做到这一点(同时支持作为命令行工具使用和通过代码调用)。

-- 需要确保 Schema 和代码同步更新。

基于 JSON 输入示例生成

-- 没有使用已经在 TypeScript 代码中定义的类型信息。

-- 如果提供的 JSON 输入示例和实际输入不一致,可能导致错误。

-- 仍然需要确保 Schema 和代码同步更新。

转译

例如使用 ts-runtime。


这种方式会将代码转译成功能上等价但内置运行时类型检查的代码。


比如,下面的代码:


interface Person {    firstName: string;    lastName: string;    age: number;}const test: Person = {    firstName: "Foo",    lastName: "Bar",    age: 55}

会被转译为:


import t from "ts-runtime/lib";const Person = t.type(    "Person",    t.object(        t.property("firstName", t.string()),        t.property("lastName", t.string()),        t.property("age", t.number())    ));const test = t.ref(Person).assert({    firstName: "Foo",    lastName: "Bar",    age: 55});

这一方式的缺陷是无法控制在何处进行运行时检查(我们只需在输入输出的边界处进行运行时类型检查)。


顺便提一下,这是一个实验性的库,不建议在生产环境使用。


运行时类型派生静态类型

比如使用 io-ts 这个库。


这一方式下,我们定义运行时类型,TypeScript 会根据我们定义的运行时类型推断出静态类型。


运行时类型示例:


import t from "io-ts";const PersonType = t.type({  firstName: t.string,  lastName: t.string,  age: t.refinement(t.number, n => n >= 0, 'Positive')})

从中提取相应的静态类型:


interface Person extends t.TypeOf<typeof PersonType> {}

以上类型等价于:


interface Person {    firstName: string;    lastName: string;    age: number;}

类型总是同步的。

io-ts 很强大,比如支持递归类型。

需要将类型定义为 io-ts 运行时类型,这在定义类时不适用:

-- 有一种变通的办法是使用 io-ts 定义一个接口,然后让类实现这个接口。然而,这意味着每次给类增加属性的时候都要更新 io-ts 类型。

不容易复用接口(比如前后端之间使用同一接口),因为这些接口是 io-ts 类型而不是普通的 TypeScript 类型。

基于装饰器的类校验

比如使用 class-validator 这个库。


基于类属性的装饰器。

和 Java 的 JSR-380 Bean Validation 2.0 (比如 Hibernate Validator 就实现了这一标准)很像。

-- 此类 Java EE 风格的库还有 typeorm (ORM 库,类似 Java 的 JPA)和 routing-controllers (用于定义 API,类似 Java 的 JAX-RS)。

代码示例:


import { plainToClass } from "class-transformer";import {     validate, IsString, IsInt, Min } from "class-validator";class Person {    @IsString()    firstName: string;    @IsString()    lastName: string;    @IsInt()    @Min(0)    age: number;}const input: any = {    firstName: "Foo",    age: -1};const inputAsClassInstance = plainToClass(    Person, input as Person);validate(inputAsClassInstance).then(errors => {    // 错误处理代码});

类型总是同步的。

需要对类进行检查时很有用。

可以用来检查接口(定义一个实现接口的类)。

注意:class-validator 用于具体的类实例。在上面的代码中,我们使用它的姊妹库 class-transformer 将普通输入转换为 Person 实例。转换过程本身不进行任何类型检查。

蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务

Typescript 内置的模块导入兼容方式

seo达人

一、前言

前端的模块化规范包括 commonJS、AMD、CMD 和 ES6。其中 AMD 和 CMD 可以说是过渡期的产物,目前较为常见的是commonJS 和 ES6。在 TS 中这两种模块化方案的混用,往往会出现一些意想不到的问题。


二、import * as

考虑到兼容性,我们一般会将代码编译为 es5 标准,于是 tsconfig.json 会有以下配置:


{

 "compilerOptions": {

   "module": "commonjs",

   "target": "es5",

 }

}

代码编译后最终会以 commonJS 的形式输出。

使用 React 的时候,这种写法 import React from "react" 会收到一个莫名其妙的报错:


Module "react" has no default export

这时候你只能把代码改成这样:import * as React from "react"。

究其原因,React 是以 commonJS 的规范导出的,而 import React from "react" 这种写法会去找 React 模块中的 exports.default,而 React 并没有导出这个属性,于是就报了如上错误。而 import * as React 的写法会取 module.exports 中的值,这样使用起来就不会有任何问题。我们来看看 React 模块导出的代码到底是怎样的(精简过):


...

var React = {

 Children: {

   map: mapChildren,

   forEach: forEachChildren,

   count: countChildren,

   toArray: toArray,

   only: onlyChild

 },


 createRef: createRef,

 Component: Component,

 PureComponent: PureComponent,

 ...

}


module.exports = React;

可以看到,React 导出的是一个对象,自然也不会有 default 属性。


二、esModuleInterop

为了兼容这种这种情况,TS 提供了配置项 esModuleInterop 和 allowSyntheticDefaultImports,加上后就不会有报错了:


{

 "compilerOptions": {

   "module": "commonjs",

   "target": "es5",

   "allowSyntheticDefaultImports": true,

   "esModuleInterop": true

 }

}

其中 allowSyntheticDefaultImports 这个字段的作用只是在静态类型检查时,把 import 没有 exports.default 的报错忽略掉。

而 esModuleInterop 会真正的在编译的过程中生成兼容代码,使模块能正确的导入。还是开始的代码:


import React from "react";

现在 TS 编译后是这样的:


var __importDefault = (this && this.__importDefault) || function (mod) {

   return (mod && mod.__esModule) ? mod : { "default": mod };

};


Object.defineProperty(exports, "__esModule", { value: true });


var react_1 = __importDefault(require("react"));

编译器帮我们生成了一个新的对象,将模块赋值给它的 default 属性,运行时就不会报错了。


三、Tree Shaking

如果把 TS 按照 ES6 规范编译,就不需要加上 esModuleInterop,只需要 allowSyntheticDefaultImports,防止静态类型检查时报错。


{

 "compilerOptions": {

   "module": "es6",

   "target": "es6",

   "allowSyntheticDefaultImports": true

 }

}

什么情况下我们会考虑导出成 ES6 规范呢?多数情况是为了使用 webpack 的 tree shaking 特性,因为它只对 ES6 的代码生效。


顺便再发散一下,讲讲 babel-plugin-component。


import { Button, Select } from 'element-ui'

上面的代码经过编译后,是下面这样的:


var a = require('element-ui');

var Button = a.Button;

var Select = a.Select;

var a = require('element-ui') 会引入整个组件库,即使只用了其中的 2 个组件。

babel-plugin-component 的作用是将代码做如下转换:


// 转换前

import { Button, Select } from 'element-ui'

// 转换后

import Button from 'element-ui/lib/button'

import Select from 'element-ui/lib/select'

最终编译出来是这个样子,只会加载用到的组件:


var Button = require('element-ui/lib/button');

var Select = require('element-ui/lib/select');

四、总结

本文讲解了 TypeScript 是如何导入不同模块标准打包的代码的。无论你导入的是 commonJS 还是 ES6 的代码,万无一失的方式是把 esModuleInterop 和 allowSyntheticDefaultImports 都配置上。

将 Gatsby 项目迁移到 TypeScript

seo达人

之前花了些时间将gatsby-theme-gitbook迁移到 Typescript,以获得在 VSCode 中更好的编程体验.

整体差不多已经完成迁移,剩下将 Gatsby 的 API 文件也迁移到 TS,这里可以看到 gatsby#21995 官方也在将核心代码库迁移到 Typescript,准备等待官方将核心代码库迁移完成,在迁移 API 文件.


这篇文章用XYShaoKang/gatsby-project-config,演示如何将 gatsby 迁移到 TypeScript,希望能帮到同样想要在 Gatsby 中使用 TS 的同学.


迁移步骤:


TS 配置

配置 ESLint 支持 TS

完善 GraphQL 类型提示

初始化项目

gatsby new gatsby-migrate-to-typescript XYShaoKang/gatsby-project-config

cd gatsby-migrate-to-typescript

yarn develop

TS 配置

安装typescript

添加typescript.json配置文件

修改 js 文件为 tsx

补全 TS 声明定义

安装typescript

yarn add -D typescript

添加配置文件tsconfig.json

// https://www.typescriptlang.org/v2/docs/handbook/tsconfig-json.html

{

 "compilerOptions": {

   "target": "esnext", // 编译生成的目标 es 版本,可以根据需要设置

   "module": "esnext", // 编译生成的目标模块系统

   "lib": ["dom", "es2015", "es2017"], // 配置需要包含的运行环境的类型定义

   "jsx": "react", // 配置 .tsx 文件的输出模式

   "strict": true, // 开启严格模式

   "esModuleInterop": true, // 兼容 CommonJS 和 ES Module

   "moduleResolution": "node", // 配置模块的解析规则,支持 node 模块解析规则

   "noUnusedLocals": true, // 报告未使用的局部变量的错误

   "noUnusedParameters": true, // 报告有关函数中未使用参数的错误

   "experimentalDecorators": true, // 启用装饰器

   "emitDecoratorMetadata": true, // 支持装饰器上生成元数据,用来进行反射之类的操作

   "noEmit": true, // 不输出 js,源映射或声明之类的文件,单纯用来检查错误

   "skipLibCheck": true // 跳过声明文件的类型检查,只会检查已引用的部分

 },

 "exclude": ["./node_modules", "./public", "./.cache"], // 解析时,应该跳过的路晋

 "include": ["src"] // 定义包含的路径,定义在其中的声明文件都会被解析进 vscode 的智能提示

}

将index.js改成index.tsx,重新启动服务,查看效果.


其实 Gatsby 内置了支持 TS,不用其他配置,只要把index.js改成index.tsx就可以直接运行.添加 TS 依赖是为了显示管理 TS,而tsconfig.json也是这个目的,当我们有需要新的特性以及自定义配置时,可以手动添加.

补全 TS 声明定义

打开index.tsx,VSCode 会报两个错误,一个是找不到styled-components的声明文件,这个可以通过安装@types/styled-components来解决.

另外一个错误绑定元素“data”隐式具有“any”类型。,这个错误是因为我们在tsconfig.json中指定了"strict": true,这会开启严格的类型检查,可以通过关闭这个选项来解决,只是我们用 TS 就是要用它的类型检查的,所以正确的做法是给data定义类型.

下面来一一修复错误.


安装styled-components的声明文件


yarn add -D @types/styled-components

修改index.tsx


import React, { FC } from 'react'

import styled from 'styled-components'

import { graphql } from 'gatsby'

import { HomeQuery } from './__generated__/HomeQuery'


const Title = styled.h1`

 font-size: 1.5em;

 margin: 0;

 padding: 0.5em 0;

 color: palevioletred;

 background: papayawhip;

`


const Content = styled.div`

 margin-top: 0.5em;

`


interface PageQuery {

 data: {

   allMarkdownRemark: {

     edges: Array<{

       node: {

         frontmatter: {

           title: string

         }

         excerpt: string

       }

     }>

   }

 }

}


const Home: FC<PageQuery> = ({ data }) => {

 const node = data.allMarkdownRemark.edges[0].node


 const title = node.frontmatter?.title

 const excerpt = node.excerpt


 return (

   <>

     <Title>{title}</Title>

     <Content>{excerpt}</Content>

   </>

 )

}


export default Home


export const query = graphql`

 query HomeQuery {

   allMarkdownRemark {

     edges {

       node {

         frontmatter {

           title

         }

         excerpt

       }

     }

   }

 }

`

这时候会出现一个新的错误,在excerpt: string处提示Parsing error: Unexpected token,这是因为 ESLint 还无法识别 TS 的语法,下面来配置 ESLint 支持 TS.


配置 ESLint 支持 TypeScript

安装依赖


yarn add -D @typescript-eslint/parser @typescript-eslint/eslint-plugin

配置.eslintrc.js


module.exports = {

 parser: `@typescript-eslint/parser`, // 将解析器从`babel-eslint`替换成`@typescript-eslint/parser`,用以解析 TS 代码

 extends: [

   `google`,

   `eslint:recommended`,

   `plugin:@typescript-eslint/recommended`, // 使用 @typescript-eslint/eslint-plugin 推荐配置

   `plugin:react/recommended`,

   `prettier/@typescript-eslint`, // 禁用 @typescript-eslint/eslint-plugin 中与 prettier 冲突的规则

   `plugin:prettier/recommended`,

 ],

 plugins: [

   `@typescript-eslint`, // 处理 TS 语法规则

   `react`,

   `filenames`,

 ],

 // ...

}

在.vscode/settings.json中添加配置,让VSCode使用ESLint扩展格式化ts和tsx文件


// .vscode/settings.json

{

 "eslint.format.enable": true,

 "[javascript]": {

   "editor.defaultFormatter": "dbaeumer.vscode-eslint"

 },

 "[javascriptreact]": {

   "editor.defaultFormatter": "dbaeumer.vscode-eslint"

 },

 "[typescript]": {

   "editor.defaultFormatter": "dbaeumer.vscode-eslint"

 },

 "[typescriptreact]": {

   "editor.defaultFormatter": "dbaeumer.vscode-eslint"

 }

}

完善 GraphQL 类型提示

// index.tsx

import React, { FC } from 'react'

// ...

interface PageQuery {

 data: {

   allMarkdownRemark: {

     edges: Array<{

       node: {

         frontmatter: {

           title: string

         }

         excerpt: string

       }

     }>

   }

 }

}


const Home: FC<PageQuery> = ({ data }) => {

 // ...

}


export default Home


export const query = graphql`

 query HomeQuery {

   allMarkdownRemark {

     edges {

       node {

         frontmatter {

           title

         }

         excerpt

       }

     }

   }

 }

`

我们看看index.tsx文件,会发现PropTypes和query结构非常类似,在Gatsby运行时,会把query查询的结果作为组件prop.data传入组件,而PropTypes是用来约束prop存在的.所以其实PropTypes就是根据query写出来的.


如果有依据query自动生成PropTypes的功能就太棒了.

另外一个问题是在query中编写GraphQL查询时,并没有类型约束,也没有智能提示.


总结以下需要完善的体验包括:


GraphQL 查询编写时的智能提示,以及错误检查

能够从 GraphQL 查询生成对应的 TypeScript 类型.这样能保证类型的唯一事实来源,并消除 TS 中冗余的类型声明.毕竟如果经常需要手动更新两处类型,会更容易出错,而且也并不能保证手动定义类型的正确性.

实现方式:


通过生成架构文件,配合Apollo GraphQL for VS Code插件,实现智能提示,以及错误检查

通过graphql-code-generator或者apollo生成 TS 类型定义文件

如果自己去配置的话,是挺耗费时间的,需要去了解graphql-code-generator的使用,以及Apollo的架构等知识.

不过好在社区中已经有对应的 Gatsby 插件集成了上述工具可以直接使用,能让我们不用去深究对应知识的情况下,达到优化 GraphQL 编程的体验.

尝试过以下两个插件能解决上述问题,可以任选其一使用


gatsby-plugin-codegen

gatsby-plugin-typegen

另外还有一款插件gatsby-plugin-graphql-codegen也可以生成 TS 类型,不过配置略麻烦,并且上述两个插件都可以满足我现在的需求,所以没有去尝试,感兴趣的可以尝试一下.


注意点:


Apollo不支持匿名查询,需要使用命名查询

第一次生成,需要运行Gatsby之后才能生成类型文件

整个项目内不能有相同命名的查询,不然会因为名字有冲突而生成失败

下面是具体操作


安装vscode-apollo扩展

在 VSCode 中按 Ctrl + P ( MAC 下: Cmd + P) 输入以下命令,按回车安装


ext install apollographql.vscode-apollo

方式一: 使用gatsby-plugin-codegen

gatsby-plugin-codegen默认会生成apollo.config.js和schema.json,配合vscode-apollo扩展,可以提供GraphQL的类型约束和智能提示.

另外会自动根据query中的GraphQL查询,生成 TS 类型,放在对应的tsx文件同级目录下的__generated__文件夹,使用时只需要引入即可.

如果需要在运行时自动生成 TS 类型,需要添加watch: true配置.


安装gatsby-plugin-codegen


yarn add gatsby-plugin-codegen

配置gatsby-config.js


// gatsby-config.js

module.exports = {

 plugins: [

   // ...

   {

     resolve: `gatsby-plugin-codegen`,

     options: {

       watch: true,

     },

   },

 ],

}

重新运行开发服务生成类型文件


yarn develop

如果出现以下错误,一般是因为没有为查询命名的缘故,给查询添加命名即可,另外配置正确的话,打开对应的文件,有匿名查询,编辑器会有错误提示.


fix-anonymous-operations.png


这个命名之后会作为生成的类型名.


修改index.tsx以使用生成的类型


gatsby-plugin-codegen插件会更具查询生成对应的查询名称的类型,保存在对应tsx文件同级的__generated__目录下.


import { HomeQuery } from './__generated__/HomeQuery' // 引入自动生成的类型

// ...


// interface PageQuery {

//   data: {

//     allMarkdownRemark: {

//       edges: Array<{

//         node: {

//           frontmatter: {

//             title: string

//           }

//           excerpt: string

//         }

//       }>

//     }

//   }

// }


interface PageQuery {

 data: HomeQuery // 替换之前手写的类型

}


// ...

将自动生成的文件添加到.gitignore中


apollo.config.js,schema.json,__generated__能通过运行时生成,所以可以添加到.gitignore中,不用提交到 git 中.当然如果有需要也可以选择提交到 git 中.

# Generated types by gatsby-plugin-codegen

__generated__

apollo.config.js

schema.json

方式二: 使用gatsby-plugin-typegen

gatsby-plugin-typegen通过配置生成gatsby-schema.graphql和gatsby-plugin-documents.graphql配合手动创建的apollo.config.js提供GraphQL的类型约束和智能提示.

根据GraphQL查询生成gatsby-types.d.ts,生成的类型放在命名空间GatsbyTypes下,使用时通过GatsbyTypes.HomeQueryQuery来引入,HomeQueryQuery是由对应的命名查询生成


安装gatsby-plugin-typegen


yarn add gatsby-plugin-typegen

配置


// gatsby-config.js

module.exports = {

 plugins: [

   // ...

   {

     resolve: `gatsby-plugin-typegen`,

     options: {

       outputPath: `src/__generated__/gatsby-types.d.ts`,

       emitSchema: {

         'src/__generated__/gatsby-schema.graphql': true,

       },

       emitPluginDocuments: {

         'src/__generated__/gatsby-plugin-documents.graphql': true,

       },

     },

   },

 ],

}

//apollo.config.js

module.exports = {

 client: {

   tagName: `graphql`,

   includes: [

     `./src/**/*.{ts,tsx}`,

     `./src/__generated__/gatsby-plugin-documents.graphql`,

   ],

   service: {

     name: `GatsbyJS`,

     localSchemaFile: `./src/__generated__/gatsby-schema.graphql`,

   },

 },

}

重新运行开发服务生成类型文件


yarn develop

修改index.tsx以使用生成的类型


gatsby-plugin-codegen插件会更具查询生成对应的查询名称的类型,保存在对应tsx文件同级的__generated__目录下.


// ...


// interface PageQuery {

//   data: {

//     allMarkdownRemark: {

//       edges: Array<{

//         node: {

//           frontmatter: {

//             title: string

//           }

//           excerpt: string

//         }

//       }>

//     }

//   }

// }


interface PageQuery {

 data: GatsbyTypes.HomeQueryQuery // 替换之前手写的类型

}


// ...

将自动生成的文件添加到.gitignore中


__generated__能通过运行时生成,所以可以添加到.gitignore中,不用提交到 git 中.当然如果有需要也可以选择提交到 git 中.

# Generated types by gatsby-plugin-codegen

__generated__

日历

链接

blogger

蓝蓝 http://www.lanlanwork.com

存档