"无 Typescript" 编程
一旦陷入 Typescript 就离不开它了。安全感、智能提示、代码重构..
但是还有很多旧的项目还在用着 JavaScript 呀?一时间很难迁移过来
使用 Typescript 最终还是要转译为 JavaScript 代码,我不想整这一套构建工具,喜欢直接写完直接跑、直接调试、直接发到 npm..
...
现代 VSCode 对 JavaScript 的类型推断(底层基于 Typescript )已经非常牛逼了:

但是还不够,不够霸道,不够果断,不然我们还要 Typescript 干嘛:

不能将 string 赋给 number 呀,我们希望这里有提示,但是没有
💡 本文编辑器基于 VSCode
💡本文需要一定的 Typescript 基础
💡推荐查看原文,更好的排版
为 JavaScript 开启类型检查
第一步,首先要确保你的 VScode 开启了 JavaScript 验证:

第二步,在文件顶部添加 // @ts-check

Bingo! VSCode 已经有类型报错了
第三步, 类型检查程序
如果你想通过程序来验证代码是否有类型错误,可以安装 Typescript CLI:
$ yarn add typescript -D
接着添加一个 jsconfig.json
文件,这个配置文件类似于 tsconfig.json
, 配置参数(详见这里)也差不多,只不过 jsconfig
专注于 Javascript 。我们的配置如下:
{
"compilerOptions": {
"target": "esnext",
"noEmit": true, // 🔴关闭输出,因为我们只进行类型检查
"skipLibCheck": true,
// "checkJs": true, // 开启对所有 JS 文件的类型检查,不需要 @ts-check
"strict": true, // 🔴严格的类型检查
"moduleResolution": "node", // 🔴按照 Node 方式查找模块
"jsx": "react", // 🔴开启 JSX
"module": "esnext",
"rootDir": "src", // 🔴源文件目录
"resolveJsonModule": true
},
"exclude": ["node_modules", "tests/**/*"]
}
💡 当然你直接用
tsconfig.json
也是没问题。tsconfig.json
中可以使用allowJs
允许 JavaScript 进行处理,另外可以通过checkJs
全局(相当于对所有JavaScript 文件都添加 @ts-check)开启对 JavaScript 的类型检查。如果你要渐进式地展开对 JavaScript 的类型检查和迁移,还是建议使用 @ts-check
接下来在 package.json
添加 run script
,方便我们执行:
{
"scripts": {
"type-check": "tsc -p jsconfig.json",
"type-check:watch": "tsc -p jsconfig.json --watch"
},
}
Run it!
$ yarn type-check
tsc -p jsconfig.json
src/index.js:12:1 - error TS2322: Type '"str"' is not assignable to type 'number'.
12 ins.foo = 'str';
~~~~~~~
Found 1 error.
error Command failed with exit code 1.
渐进式类型声明
单纯依赖于类型推断,远远不够,Typescript 还没那么聪明,很多情况下我们需要显式告诉 Typescript 对应实体是什么类型。
在 JavaScript 中,我们可以通过 [JSDoc 注解](https://jsdoc.app/)
或者 .d.ts
来进行类型声明。
下面尝试将 Typescript 中类型声明的习惯用法迁移到 JavaScript 中。
1. 变量
1⃣ 变量注解
比较简单,只是将类型注解放进 @type {X}
中, 类型注解的语法和 Typescript 保持一致:
const str = 'string' // 自动推断
let count: number
const member: string[] = []
const users: Array<{id: string, name: string, avatar?: string}> = []
let maybe: string | undefined
const str = 'string'; // 自动推断
/** @type {number} */
let count;
/** @type number ❤️ 括号可以省略,省得更简洁*/
let count;
/** @type {string[]} */
const member = [];
/** @type {Array<{ id: string, name: string, avatar?: string }>} */
const users = [];
/** @type string | undefined */
let maybe
2⃣ 类型断言
var numberOrString: number | string = Math.random() < 0.5 ? "hello" : 100;
var typeAssertedNumber = numberOrString as number
/**
* @type {number | string}
*/
var numberOrString = Math.random() < 0.5 ? "hello" : 100;
var typeAssertedNumber = /** @type {number} */ (numberOrString); // 注意括号是必须的
2. 类型定义
有些类型会在多个地方用到,我们通常通过 interface
或 type
定义类型,然后复用
1️⃣ 常规 interface
interface User {
id: string;
name: string;
age: number;
avatar?: string;
}
/**
* JSDoc 注解通常是单行的
* @typedef {{ id: string name: string age: number avatar?: string }} User 用户
*/
/** @type {User} */
var a
// 🔴不过多行也是可以被识别的
/**
* @typedef {{
* id: string
* name: string
* age: number
* avatar?: string
* }} User
*/
还有另外一种更 ‘JSDoc' 的类型定义方式, 它的好处是你可以针对每个字段进行文字说明,对编辑器智能提示或文档生成有用:
/**
* @typedef {Object} User
* @property {string} id 主键
* @property {string} name 名称
* @property {number} age 年龄
* @property {string} [avatar] 头像
*/
2️⃣泛型
// 泛型
interface CommonFormProps<T> {
value?: T;
onChange(value?: T): void;
}
// 多个泛型
interface Component<Props, State> {
props: Props;
state: State;
}
// 泛型
/**
* @template T
* @typedef {{
* value?: T
* onChange(value?: T): void
* }} CommonFormProps
*/
/** @type {CommonFormProps<string>} */
var b
// 多个泛型
/**
* @template Props, State
* @typedef {{
* props: Props;
* state: State;
* }} Component
*/
/** @type {Component<number, string>} */
var d
3️⃣组合已存在类型
interface MyProps extends CommonFormProps<string> {
title?: string
}
/**
* @typedef {{title?: string} & CommonFormProps<string>} MyProps
*/
/** @type {MyProps}*/
var e
4️⃣类型别名
type Predicate = (data: string, index?: number) => boolean;
type Maybe<T> = T | null | undefined;
// 类型别名
/**
* @typedef {(data: string, index?: number) => boolean} Predicate
*/
/** @type {Predicate} */
var f
/**
* @template T
* @typedef {(T | null | undefined)} Maybe
*/
/** @type {Maybe<string>} */
var g
💡 如果没有泛型变量
,多个 @typedef
可以写在一个注释中:
/**
* @typedef {(data: string, index?: number) => boolean} Predicate
* @typedef {{
* id: string
* name: string
* age: number
* avatar?: string
* }} User
*/
// 有泛型变量需要单独存在
/**
* @template T
* @typedef {(T | null | undefined)} Maybe
*/
如上,除了 @typedef 和 @template 一些特殊的语法,其他的基本和 Typescript 保持一致。
🙋🏻♂️那么问题来了!
- 怎么将类型共享给其他文件?
- 你嫌写法有点啰嗦
- 代码格式化工具不支持对注释进行格式化
- ...
5️⃣ 声明文件
实际上,我们可以通过 import 导入另一个模块中的类型定义:
// @file user.js
/**
* @typedef {{
* id: string
* name: string
* age: number
* avatar?: string
* }} User
*/
// @file index.js
/** @type {import('./user').User} */
一个更好的方式是,创建一个独立的 *.d.ts
(Typescript 纯类型声明文件)来存放类型声明,比如 types.d.ts
, 定义并导出类型:
// @file types.d.ts
export interface User {
id: string;
name: string;
age: number;
avatar?: string;
}
export interface CommonFormProps<T> {
value?: T;
onChange(value?: T): void;
}
export interface Component<Props, State> {
props: Props;
state: State;
}
export interface MyProps extends CommonFormProps<string> {
title?: string
}
export type Predicate = (data: string, index?: number) => boolean;
export type Maybe<T> = T | null | undefined;
/** @type {import('./types').User} */
const a = {};
/** @type {import('./types').CommonFormProps<string>} */
var b;
/** @type {import('./types').Component<number, string>} */
var e;
/** @type {import('./types').MyProps}*/
var f;
/** @type {import('./types').Predicate} */
var g;
/** @type {import('./types').Maybe<string>} */
var h;
💡 如果某个类型被多次引用,重复 import 也比较啰嗦,可以在文件顶部一次性导入它们:
/**
* @typedef {import('./types').User} User
* @typedef {import('./types').Predicate} Predicate
*/
/** @type {User} */
var a;
/** @type {Predicate} */
var g;
6️⃣第三方声明文件
没错,import('module').type 也可以导入第三方库的类型。这些库需要带类型声明,有些库 npm 包中自带了声明文件(比如 Vue), 有些则你需要下载对应的 @types/* 声明文件(比如 React,需要装 @types/react)。
以 React 为例,需要安装 @types/react
:
/**
* @typedef {{
* style: import('react').CSSProperties
* }} MyProps
*/
// 因为 @types/* 默认会暴露到全局,比如 @types/react 暴露了 React 命名空间,因此下面这样写也是可以的:
/**
* @typedef {{
* style: React.CSSProperties
* }} MyProps
*/
7️⃣全局声明文件
除此之外,我们也可以全局声明类型,在项目的所有地方都可以用到这些类型定义。按照习惯,我们在项目根目录(jsconfig.json 所在目录)创建一个 global.d.ts
// @file global.d.ts
/**
* 全局类型定义
*/
interface GlobalType {
foo: string;
bar: number;
}
/**
* 扩展已有的全局对象
*/
interface Window {
__TESTS__: boolean;
// 暴露 jquery 到 window, 需要安装 @types/jquery
$: JQueryStatic;
}
/** @type {GlobalType} */
var hh // ✅
window.__TESTS__ // ✅
const $elm = window.$('#id') // ✅
3. 函数
接下来看看怎么给函数进行类型声明:
1️⃣可选参数
function buildName(firstName: string, lastName?: string) {
if (lastName) return firstName + ' ' + lastName;
else return firstName;
}
function delay(time = 1000): Promise<void> {
return new Promise((res) => setTimeout(res, time));
}
// 🔴 JSDoc 注释风格
/**
* @param {string} firstName 名
* @param {string} [lastName] 姓,方括号是 JSDoc 可选参数的写法
* @returns string 可选,Typescript 可以推断出来,如果无法推断,可以显示声明
*/
function buildName(firstName, lastName) {
if (lastName) return firstName + ' ' + lastName;
else return firstName;
}
buildName('ivan') // ✅
/**
* @param {number} [time=1000] 延迟时间, 单位为 ms
* @returns {Promise<void>}
*/
function delay(time = 1000) {
return new Promise((res) => setTimeout(res, time));
}
// 🔴你也可以使用 Typescript 风格, **不过不推荐!**它有以下问题:
// - 不能添加参数注释说明, 或者说工具不会识别
// - 对可选参数的处理有点问题, 和 Typescript 行为不一致
/** @type {(firstName: string, lastName?: string) => string} */
function buildName(firstName, lastName) {
if (lastName) return firstName + ' ' + lastName;
else return firstName;
}
// ❌ 因为TS 将buildName 声明为了 (firstName: string, lastName: string | undefined) => string
buildName('1')
// 🔴另一个可选参数的声明方法是 -- 显式给可选参数设置默认值(ES6的标准),TS 会推断为可选
/**
* @param {string} firstName 名
* @param {string} [lastName=''] 姓,方括号是 JSDoc 可选参数的写法
* @returns string 可选,Typescript 可以推断出来,如果无法推断,可以显示声明
*/
function buildName(firstName, lastName = '' ) {
if (lastName) return firstName + ' ' + lastName;
else return firstName;
}
/** @type {(firstName: string, lastName?: string) => string} */
function buildName(firstName, lastName = '') {
if (lastName) return firstName + ' ' + lastName;
else return firstName;
}
buildName('1') // ✅
2️⃣ 剩余参数
function sum(...args: number[]): number {
return args.reduce((p, c) => p + c, 0);
}
/**
* @param {...number} args
*/
function sum(...args) {
return args.reduce((p, c) => p + c, 0);
}
3️⃣ 泛型与泛型约束
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
/**
* @template T
* @template {keyof T} K // 可以在 {} 中约束泛型变量的类型
* @param {T} obj
* @param {K} key
*/
function getProperty(obj, key) {
return obj[key];
}
4️⃣ this 参数声明
interface User {
name: string;
lastName: string;
}
const user = {
name: 'ivan',
lastName: 'lee',
say(this: User) {
return `Hi, I'm ${this.name} ${this.lastName}`;
},
};
/**
* @typedef {{
* name: string;
* lastName: string;
* }} User
*/
const user = {
name: 'ivan',
lastName: 'lee',
/** @this {User} */
say() {
return `Hi, I'm ${this.name} ${this.lastName}`;
},
};
这里基本覆盖了JavaScript 函数的基本使用场景,其余的以此类推,就不展开了。
4. 类
1️⃣ 常规用法
class Animal {
private name: string;
constructor(theName: string) {
this.name = theName;
}
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
可以使用 @public、@private、@protected 来声明字段或方法的访问性。
// 🔴 如果目标环境支持 ES6 class
class Animal {
/**
* @param {string} theName
*/
constructor(theName) {
// 属性声明
/**
* @type {string}
* @private 声明为私有, 对应的也有 @protected, 默认都是 @public
* 当然也可以使用 ES6 的 private field 语言特性
*/
this.name = theName;
}
/**
* @param {number} [distanceInMeters]
*/
move(distanceInMeters = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
const a = new Animal('foo');
a.name; // ❌ 不能访问私有字段
// 🔴 如果目标环境不支持,只能使用 函数形式了
/**
* @constructor
* @param {string} theName
*/
function Animal1(theName) {
/**
* @type {string} 这里不能使用 private 指令
*/
this.name = theName;
}
/**
* @param {number} [distanceInMeters = 0]
*/
Animal1.prototype.move = function (distanceInMeters) {
console.log(this.name + ' moved ' + (distanceInMeters || 0) + 'm.');
};
const a = Animal1('bird') // ❌ 使用 @constructor 后,只能 new 调用
2️⃣ 泛型
import React from 'react';
export interface ListProps<T> {
datasource: T[];
idKey?: string;
}
export default class List<T> extends React.Component<ListProps<T>> {
render() {
const { idKey = 'id', datasource } = this.props;
return datasource.map(
(i) => React.createElement('div', { key: i[idKey] }) /*...*/,
);
}
}
// @ts-check
import React from 'react';
/**
* @template T
* @typedef {{
* datasource: T[];
* idKey?: string;
* }} ListProps
*/
/**
* @template T
* @extends {React.Component<ListProps<T>>} 使用 extends 声明继承类型
*/
export default class List extends React.Component {
render() {
const { idKey = 'id', datasource } = this.props;
return datasource.map(
// @ts-expect-error JavaScript 中也可以使用 @ts-ignore 等注释指令
(i) => React.createElement('div', { key: i[idKey] }) /*...*/,
);
}
}
// 显式定义类型
/** @type {List<{id: string}>} */
var list1;
// 自动推断类型
var list2 = <List datasource={[{ id: 1 }]}></List>;
⚠️ 不支持泛型变量默认值
3️⃣接口实现
interface Writable {
write(data: string): void
}
class Stdout implements Writable {
// @ts-expect-error ❌ 这里会报错,data 应该为 string
write(data: number) {}
}
/**
* @typedef {{
* write(data: string): void
* }} Writable
*/
/**
* @implements {Writable}
*/
class Output {
/**
* @param {number} data
*/
write(data) { // ❌ data 应该为 string
}
}
6. 其他
枚举
@enum 只能用于约束对象的成员类型,作为类型时没什么卵用
/** @enum {number} Mode*/
const Mode = {
Dark: 1,
Light: 2,
Mixed: 'mix' // ❌
};
/**
* @type {Mode}
*/
var mode = 3; // ✅,作为类型时,作用不大
@deprecated
class Foo {
/** @deprecated */
bar() {
}
}
(new Foo).bar // ❌ 在 Typescript 4.0 会报错
总结
"无 Typescript 编程" 有点标题党,其实这里不是不用 Typescript,而是换一种无侵入的方式用 Typescript。
使用 JSDoc 注解进行类型声明的方式,基本上可以满足 JavaScript 的各种类型检查要求,当然还有很多 Typescript 特性不支持。
这样,利用 Typescript 带来的各种红利, 顺便培养自己写注释的习惯, JSDoc 注解也方便进行文档生成。何乐而不为
本文提及了以下 JSDoc 注解:
- @type 声明实体类型、例如变量、类成员
- @typedef 定义类型
- @template 声明泛型变量
- @param 定义函数参数类型
- @returns 定义函数返回值类型
- @constructor 或 @class 声明一个函数是构造函数
- @extends 声明类继承
- @implements 声明类实现哪些接口
- @public、@private、@protected 声明类成员可访问性
- @enum 约束对象成员类型
- @deprecated 废弃某个实体
- @property 和 @typedef 配合,声明某个字段的类型
除本文以及 Typescript 官方文档提及的 JSDoc 注解之外的其他注解,暂时不被支持。
接下来,可以看看 Typescript 的官方文档说明,继续探索 JSDoc,以及以下使用案例(欢迎补充):
如果你探索出更多玩法,欢迎评论告诉我。
扩展
- Typescript: Type Checking JavaScript Files
- Typescript: JSDoc Reference
- Typescript: TSConfig Reference
- VSCode: Working with JavaScript
- VSCode: jsconfig.json
- JSDoc
转载自:https://juejin.cn/post/6862981984801521672