解放开发者?也许并不需要使用 TypeScript

原文地址:you-might-not-need-typescript
本文为原文翻译,补充了一些个人看法和自己的理解

每个人都喜欢类型,每个人都喜欢自动补全,每个人都喜欢在问题出现之前得到警告。

但没有人喜欢浪费时间去编译东西。希望这能帮助说服你或你的公司,你实际上不需要TypeScript风格的语法。因此,我嵌入了与VSCode中使用的相同编辑器,向你展示你可以在原生JavaScript中获得类型安全,并且同时拥有最好的两个世界,并可以轻松玩弄它

VSCODE 类型安全

VSCode内建了一些隐藏功能,不是每个人都知道

它们非常强大,以至于它们默认被禁用,因为这几乎看起来像是Microsoft希望你使用TypeScript而不是使用ESM或原生JavaScript

下面是一些应该默认启用的功能:

json 复制代码
// settings.json
// 全局添加,或者可以添加到 <root>/.vscode/settings.json 中。
{
// 启用JavaScript文件的语义检查。
// 现有的jsconfig.Json或tsconfig.Json文件将覆盖此设置。
  "js/ts.implicitProjectConfig.checkJs": true,
  // Enable suggestion to complete JSDoc comments.
  "javascript.suggest.completeJSDocs": true,
  // 以扩展名结尾的首选路径 (good for ESM & cjs).
  "javascript.preferences.importModuleSpecifierEnding": "js",
  // 用参数签名完成函数
  "javascript.suggest.completeFunctionCalls": true,
  // 自动导入提示
  "javascript.suggest.autoImports": true
}
JavaScript 复制代码
const username = 'bob'
username = 10
// Cannot assign to 'username' because it is a constant.(2588)

let age = 22

// @ts-ignore next line
age = '22'

你还可以在项目中添加一个jsconfig.json文件。jsconfig.json是tsconfig.json的后裔,而tsconfig.json是TypeScript的配置文件。jsconfig.json实际上就是将 "allowJs"属性设置为true的tsconfig.json
拥有这个文件可以帮助你生成 .d.ts 文件

类型守卫

类型守卫(Type Guard)是一种用于在运行时检查变量类型的技术。这种机制允许开发者在特定的代码块内,以类型安全的方式访问对象的属性或方法,使得在代码中能够获得更好的智能感知,提供了更准确的代码提示

TypeScript

TypeScript本身提供了更丰富的类型检查和智能感知

typescript 复制代码
interface Robot { model: string }
interface Person { name: string }

/**
 * @param obj Object to check
 * @return Returns true if `obj` is a Person
 */
function isPerson (obj: object): obj is Person {
	return 'name' in obj
}

/**
 * Say hello!
 * @param personOrRobot To who you want to say hello to
 */
function greet(personOrRobot: Robot | Person) {
	if (isPerson(personOrRobot)) {
        // 智能提示,会提示`.name`,而不是`.model`
        // 因为上面已经使用的if语句进行判断
		console.log(`Hello ${personOrRobot.name}!`);
		// Below will throw type error
		console.log(personOrRobot.model);
	} else {
        // 会提示`.model`,而不是`.name`
		console.log(`GREETINGS ${personOrRobot.model}.`);
	}
}

JavaScript

JavaScript中,我们需要使用JSDoc注释来实现相似TypeScript的效果

JavaScript 复制代码
/** @typedef {{model: string}} Robot */
/** @typedef {{name: string}} Person */

/**
 * @param {object} obj Object to check
 * @return {obj is Person}
 */
function isPerson (obj) {
	return 'name' in obj
}

/**
 * Say hello!
 * @param {Robot | Person} personOrRobot To who you want to say hello to
 */
function greet(personOrRobot) {
	if (isPerson(personOrRobot)) {
		// 智能提示,会提示`.name`,而不是`.model`
		console.log(`Hello ${personOrRobot.name}!`);
		// 这里会抛出错误,因为使用的是model属性
		console.log(personOrRobot.model);
	} else {
		// 会提示`.model`,而不是`.name`
		console.log(`GREETINGS ${personOrRobot.model}.`);
	}
}

公开/私有字段

这里涉及到对象属性知识点,在JavaScript中我们可以通过使用不同的属性访问修饰符来实现的属性的访问权限

TypeScript

typescript 复制代码
class Employee {
  // 存在不必要的注解
  private name: string = ''
  public alive: boolean = true

  public constructor (theName: string = 'bob') {
    this.name = theName
  }

  public getName(): string {
    // 看name属性,你觉得它是私有属性吗
    return this.name
  }
}

const employee = new Employee('Bob')
employee.name // 无法直接访问name属性

// 不是真正的私有

JavaScript

JavaScript 复制代码
class Employee {
  // 真正的私有属性
  // 所有带有 # 的都是私有的,其余的都是公有的
  // 无需多余注解
  #name
  alive = true

  constructor (theName = 'bob') {
    this.#name = theName
  }

  getName() {
    // 看name属性,你觉得它是私有属性吗
    //(带了#很容易看出来)
    return this.#name
  }
}

const employee = new Employee('Bob')
employee.name // 同样无法直接访问

你不必添加注释来说明#name的类型是字符串,或者使用JSDoc来标注它是私有的。VS Code可以通过构造函数的参数推断出name是字符串类型,这是通过默认参数值进行的推断
#{属性名} 这个是ECMAScript中的较新特性(ES12),不是所有环境都支持

Generics in JSDoc

TypeScript

typescript 复制代码
/**
 * 接受带有name属性的任意对象并将其移除
 */
function deleteName <T extends { name?: string }>(obj: T): Omit<T, 'name'> {
  const copy = JSON.parse(JSON.stringify(obj))
  delete copy.name
  return copy
}

//  IDE 将自动推断T的类型,并且还将推断nameLess的类型为 { age: number, name: string },不包含name
const nameLess = deleteName({
	age: 50,
	name: 'Gregory'
})

// 弊端:
// 过于紧凑,难以阅读
// 并没有真正提高可读性

JavaScript

JavaScript 复制代码
/**
 * 接受带有name属性的任意对象并将其移除
 * @template T
 * @param {T & {name?: string}} obj
 * @returns {Omit<T, 'name'>}
 */
const deleteName = (obj) => {
	/** @type {typeof obj} */
	const copy = JSON.parse(JSON.stringify(obj))
	delete copy.name
	return copy
}

// IDE 将自动推断T的类型,并且还将推断nameLess的类型为 { age: number, name: string },不包含 name
const nameLess = deleteName({
	age: 50,
	name: 'Gregory'
})

优势: 任何对jsdocTypeScript一无所知的人都可以通过仅查看JavaScript部分就理解正在发生的事情

导入外部/导出类型

TypeScript

typescript 复制代码
// 导入类型
import type { State } from 'node:fs'
import type { Node as AstNode } from 'unist'

JavaScript

JavaScript 复制代码
// 通过jsdoc声明类型

/** @type {import('node:fs').Stats} */
let fsStats;

// Or, via typedef for reuse at multiple places:
/** @typedef {import('node:fs').Stats} Stats */

解构参数

typescript 复制代码
// 定义类型,解构类型
interface DestructuredUser {
  userName: string,
  age: number
}


/** @type {DestructuredUser} */
const { userName, age } = getUser()

// 在函数参数上进行注解,会要求你写相同的属性两次以获得相同的函数签名,因此更加冗长
function logUser ({userName, age}: {userName: string, age: number}){
  // ...
}

// 或者这样子写
function saveUser ({ userName, age }: DestructuredUser) {
  // ...
}

// 箭头函数上的写法
const printReceipt = (obj?: {
  total: number;
  vendorName?: string | undefined;
}) => {
  // ...
}

printReceipt({
	total: 200,
	vendorName: ''
})

function getUser() {
  return {
  	userName: '',
	  age: 20
  }
}
JavaScript 复制代码
/**
 * @typedef {object} DestructuredUser
 * @property {string} userName
 * @property {number} age
 */

/** @type {DestructuredUser} */
const { userName, age } = getUser()

/**
 * 在函数参数上进行注释:
 *
 * @param {{ userName: string, age: number }} obj
 */
function logUser ({ userName, age }) {
	// ...
}





/**
 * 另外一种写法
 * @param {DestructuredUser} param
 */
function saveUser ({ userName, age }) {
  // ...
}

/**
 * 注释箭头函数:
 * @param {object} [obj] - [] 表示可选参数
 * @param {number} obj.total - 价格
 * @param {string=} obj.vendorName - 所有带有 {*=} 的表示都是可选的
 */
const printReceipt = ({ total, vendorName }) => {
  // 这样子都可以拿到正确的类型
}

printReceipt({
	total: 200,    // 使用智能感知可以解释它
	vendorName: ''
})

function getUser() {
  return {
	userName: '',
	age: 20
  }
}

函数中的可选参数

typescript 复制代码
function increase (x, amount = 1): number {
  return x + amount;
}

// Chrome devtools无法理解JSDoc/TypeScript
function decrease (x, amount?): number {
  return x - (amount ?? 1)
}

/** @param message */
function say (message?: string): void {
  if (message) alert(message)
}
JavaScript 复制代码
function increase (x, amount = 1) {
  return x + amount;
}

// 使用默认参数,告诉devtools它是可选的
function decrease (x, amount = undefined) {
  return x - (amount ?? 1)
}

/** @param {string} [message] 可选的参数 */
function say (message) {
  if (message) alert(message)
}

/** @param {string=} message 可选的参数 */
function say (message) {
  if (message) alert(message)
}

关于CJS

  • CommonJS从v12.17开始支持从commonjs中的动态import(),但TypeScript并不以这种方式看待

  • TypeScriptimport替换为require并破坏了仅在ESM中使用的导入

TypeScript

typescript 复制代码
// main.cjs
import('esm-only-pkg')

JavaScript

JavaScript 复制代码
// Hot garbage output that no longer works
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Promise.resolve().then(() => __importStar(require('esm-only-pkg')));
// loading a esm-only module using require cause a crash 👆

关于ESM支持

尽管你可以通过一些目标配置将TypeScript转译为1:1ES模块,但TypeScript在处理扩展名方面表现得相当糟糕,并且自2017年以来一直完全忽视了这个问题。

他们表示:"我们不想涉及它,也不想自动修复扩展名问题,我们依赖于require()来解决索引和没有扩展名的问题。" 简而言之,他们将其标记为"按设计工作"

因此,开发者简单地按照一些指南(例如Pure ESM package)的建议添加了.js

  • package.json中添加{"type": "module"}
  • 在导入的末尾添加.js扩展名
typescript 复制代码
// main.ts

// In before:
import { ModalBackground } from './ModalBackground'
// out before:
import { ModalBackground } from './ModalBackground'


// What ppl are doing now to support esm:
import { ModalBackground } from './ModalBackground.js'
// resulting output
import { ModalBackground } from './ModalBackground.js'
  • 没有文件 ./ModalBackground.js,只有一个./ModalBackground.ts文件
  • 如果你实际上设置了allowJs=true并且想要导入一个.js文件,怎么处理?忽略扩展名

如果在同一目录下存在ModalBackground.jsModalBackground.ts文件时,如果使用import('ModalBackground.ts')这样的导入语句,就会引发歧义。Deno或其他系统不知道应该加载哪个文件,因为它们具有相同的模块名称

因此,最好的做法是不要发布未编译的TypeScript包。TypeScript增加了编译时的开销,因此加载时间会更长。

JavaScript 复制代码
// main.js

// JavaScript则没有这个问题
import { ModalBackground } from './ModalBackground.js'

/* 
Deno能够很好地下载并解析JavaScript文件,包括对JSDoc的准确处理。如果你使用 Deno的VSCode插件而非内置的TypeScript解析器,你仍然可以享受到正常的IDE自动补全功能 
*/
import FileSystem from 'https://cdn.jsdelivr.net/npm/native-file-system-adapter/mod.js'

但对于Deno用户来说,这也是一个大问题。Deno不会将远程HTTP导入与浏览器导入有任何区别,它要求明确指定到其他文件的路径,要求你写成./ModalBackground.ts../index.ts,而不是../../index.js,即使你只有一个index.ts文件

TypeScript中存在的问题

typescript 复制代码
new (async ()=>{}).constructor('return "works"')().then(console.log)

new ArrayBuffer() // Expected 1 arguments, but got 0.

fetch(new URL('/')) // Argument is not of type `RequestInfo`

new Date() + 1000 // Operator '+' cannot be applied to other types

new Blob([{}]).text() // is not assignable to type 'BlobPart'.

alert('', extraArg) // Expected 0-1 arguments, but got 2.

1 + true // Operator '+' cannot be applied to other types

'abc'.replaceAll('a', 'A') // 'replaceAll' does not exist on string

import('https://example.com/foo.js') // Error: Cannot find module

await Promise.resolve(
  "Unaware if it's a module with top-level await support or not"
)

window.addEventListener('message', () => {}, {
  once: true,
  // Don't support new unknown features
  signal: new AbortController().signal
})

class A {}
class B extends A {
  x = ''
  constructor (x, y, z) {
    if (arguments.length < 3) throw new TypeError('Meh')
    super() // A 'super' call must be the first statement...
  }
}
JavaScript 复制代码
new (async ()=>{}).constructor('return "works"')().then(console.log)

new ArrayBuffer()     // Totally okay to omit length for 0-length

fetch(new URL('/'))   // Fallback behavior is to cast to string

new Date() + 1000     // Ok to use + operator, it has Symbol.toPrimitive

new Blob([{}]).text() // Fallback behavior is to cast to string

alert('', extraArg)   // It don't hurts to do byte saving tech

1 + true              // Because you can

'abc'.replaceAll('a', 'A') // Don't need to specify target

import('https://example.com/foo.js') // Not a problem

await Promise.resolve(
  "Unaware if it's a module with top-level await support or not"
)

window.addEventListener('message', () => {}, {
  once: true,
  // Dose not complain of unknown properties in new spec
  signal: new AbortController().signal
})

class A {}
class B extends A {
  x = ''
  constructor (x, y, z) {
    if (arguments.length < 3) throw new TypeError('Meh')
    super()
  }
}

TypeScript目前应该更加注重的是Symbol.toPrimitiveJavaScript中类型转换的实际工作方式

个人观点

JSDoc作为JavaScript提供静态类型检查的工具,为代码提供额外的上下文类型信息,帮助开发者编写更可靠的代码,从而减少在运行时出现的一些错误

如果只是简单的类型推断,我觉得是可以直接使用JSDoc

个人认为在一些较为复杂的类型场景下,使用JSDoc的代码可能会变得更加复杂(从上面定义类型的例子中可以看出),相比之下,TypeScript在处理复杂类型时更为简洁,尤其是在多人协作开发的情况下

另外,TypeScript提供的功能更为强大,它在编译时能够捕获一些错误信息,包括重构、成员修饰符等功能,而这些是JSDoc所无法提供的

这可能会增加一些编译时间,但为了获得更优秀的开发体验,这是值得的

相关推荐
顶顶年华正版软件官方1 小时前
剪辑抽帧技巧有哪些 剪辑抽帧怎么做视频 剪辑抽帧补帧怎么操作 剪辑抽帧有什么用 视频剪辑哪个软件好用在哪里学
前端·音视频·视频·会声会影·视频剪辑软件·视频剪辑教程·剪辑抽帧技巧
MarkHD1 小时前
javascript 常见设计模式
开发语言·javascript·设计模式
托尼沙滩裤2 小时前
【js面试题】js的数据结构
前端·javascript·数据结构
不熬夜的臭宝2 小时前
每天10个vue面试题(一)
前端·vue.js·面试
朝阳392 小时前
vue3【实战】来回拖拽放置图片
javascript·vue.js
不如喫茶去2 小时前
VUE自定义新增、复制、删除dom元素
前端·javascript·vue.js
长而不宰2 小时前
vue3+electron项目搭建,遇到的坑
前端·vue.js·electron
阿垚啊3 小时前
vue事件参数
前端·javascript·vue.js
加仑小铁3 小时前
【区分vue2和vue3下的element UI Dialog 对话框组件,分别详细介绍属性,事件,方法如何使用,并举例】
javascript·vue.js·ui
过去式的美好4 小时前
vue前端通过sessionStorage缓存字典
前端·vue.js·缓存