学习TypeScript的TypeScript--JSDoc指南

165 阅读5分钟

TypeScript与JSDoc注解#的关系

在最好的情况下,TypeScript通过从你使用JavaScript的方式中正确推断出类型。

function addVAT(price, vat) {
  return price * (1 + vat) // Oh! You add and mulitply with numbers, so it's a number
}

在上面的例子中,我们对数值进行了mulitply。这个操作只对类型有效number 。有了这些信息,TypeScript知道addVAT 的返回值将是number 的类型。

为了确保输入值是正确的,我们可以添加默认值。

function addVAT(price, vat = 0.2) { // great, `vat`is also number!
  return price * (1 + vat)
}

但是类型推理只能到此为止了。我们可以通过添加JSDoc注释为TypeScript提供更多信息。

/**
 * Adds VAT to a price
 * 
 * @param {number} price The price without VAT
 * @param {number} vat The VAT [0-1]
 * 
 * @returns {number}
 */
function addVAT(price, vat = 0.2) {
  return price * (1 + vat)
}

Paul Lewis在这方面有一个很棒的视频。但是比起注释中的几个基本类型,还有很多很多。事实证明,用JSDoc类型工作可以让你走得很远。

激活报告#

为了确保你不仅提供类型信息,而且在你的编辑器中得到实际的错误反馈(或通过tsc ),请在你的源文件中激活@ts-check 标志。

// @ts-check

如果有一个特定的行出现了错误,但你认为你更清楚,请添加@ts-ignore 标志。

// @ts-ignore
addVAT('1200', 0.1); // would error otherwise

内联类型#

定义参数是一件事。有时你想确保一个还没有被分配的变量有正确的类型。TypeScript支持内联注解。

/** @type {number} */
let amount;
amount = '12'; // 💥 does not work

不要忘记正确的注释语法。带有// 的内联注释将无法工作。

定义对象#

基本类型是一回事,但在JavaScript中你通常要处理复杂的类型和对象。对于基于注释的类型注解来说,没有问题。

/**
 * @param {[{ price: number, vat: number, title: string, sold?: boolean }]} articles
 */
function totalAmount(articles) {
  return articles.reduce((total, article) => {
    return total + addVAT(article)
  }, 0)
}

看,我们定义了一个复杂的对象类型(就像我们在TypeScript中做的那样),作为一个参数内联。

内联注解所有的东西会很快变得很拥挤。有一种更优雅的方式,通过@typedef 来定义对象类型。

/**
 * @typedef {Object} Article
 * @property {number} price
 * @property {number} vat
 * @property {string} string
 * @property {boolean=} sold
 */

/**
 * Now we can use Article as a proper type
 * @param {[Article]} articles
 */
function totalAmount(articles) {
  return articles.reduce((total, article) => {
    return total + addVAT(article)
  }, 0)
}

更多的编写工作,但最终更可读。另外TypeScript现在可以用Article 的名字来识别Article ,在你的IDE中提供更好的信息。

请注意可选的参数sold 。它是用@property {boolean=} sold 来定义的。另一种语法是@property {boolean} [sold] 。对于函数@params 也是如此。

定义函数#

函数可以被内联定义,就像它们的对应对象一样。

/**
 * @param {string} url
 * @param {(status: number, response?: string) => void} cb
 */
function loadData(url, cb) {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', url)
  xhr.onload = () => {
    cb(xhr.status, xhr.responseText)
  }
}

同样,这也会很快变得非常混乱。有一个@callback 注释可以帮助解决这个问题。

/**
 * @callback LoadingCallback
 * @param {number} status
 * @param {string=} response
 * @returns {void}
 */

/**
 * @param {string} url
 * @param {LoadingCallback} cb
 */
function loadData(url, cb) {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', url)
  xhr.onload = () => {
    cb(xhr.status, xhr.responseText)
  }
}

@callback 与函数注解的参数相同,但工作原理是@typedef

导入类型#

@typedef 允许你从任何其他 或 文件中导入类型。有了它,你可以在TypeScript中写TypeScript类型定义并在你的源文件中导入它们。.js .ts

参见article.ts

export type Article = {
  title: string,
  price: number,
  vat: number,
  sold?: boolean,
}

和我们的main.js

// The following line imports the Article type from article.ts and makes it
// available under Article
/** @typedef { import('./article').Article } Article */

/** @type {Article} */
const article = {
  title: 'The best book in the world',
  price: 10,
  vat: 0.2
}

你也可以直接在类型注释中导入一个类型。

/** @type {import('./article').Article} */
const article = {
  title: 'The best book in the world',
  price: 10,
  vat: 0.2
}

当你在没有环境类型定义的情况下混合使用TypeScript时非常好。

使用泛型的工作#

TypeScript的泛型语法在任何有泛型的地方都是可用的。

/** @type PromiseLike<string> */
let promise;

// checks. `then` is available, and x is a string
promise.then(x => x.toUpperCase())

但是你可以用@template 注释来定义更详细的泛型(特别是带有泛型的函数)。

/**
 * @template T
 * @param {T} obj
 * @param {(keyof T)[]} params
 */
function pluck(obj, ...params) {
  return params.map(el => obj[el])
}

这很方便,但对于复杂的泛型来说有点难办。内联泛型仍然以TypeScript的方式工作。

/** @type { <T, K extends keyof T>(obj: T, params: K[]) => Array<T[K]>} */
function values(obj, ...params) {
  return params.map(el => obj[el])
}

const numbers = values(article, 'price', 'vat')
const strings = values(article, 'title')
const mixed = values(article, 'title', 'vat')

有更复杂的泛型吗?可以考虑把它们放在TypeScript文件中,然后通过import函数导入。

泛型#

将一个特殊结构的JavaScript对象变成一个枚举,并确保值是一致的。

/** @enum {number} */
const HTTPStatusCodes = {
  ok: 200,
  forbidden: 403,
  notFound: 404,
}

枚举与普通的TypeScript枚举有很大不同。他们确保这个对象中的每个键都有指定的类型。

/** @enum {number} */
const HTTPStatusCodes = {
  ok: 200,
  forbidden: 403,
  notFound: 404,
  errorsWhenChecked: 'me' // 💣
}

这就是他们的全部工作。

typeof#

我最喜欢的工具之一,typeof ,也可以使用。为你节省了大量的编辑工作。

/**
 * @param {number} status The status code as a number
 * @param {string} data The data to work with
 */
function defaultCallback(status, data) {
  if(status === 200) {
    document.body.innerHTML = data
  }
}

/**
 * @param {string} url the URL to load data from
 * @param {typeof defaultCallback} cb what to do afterwards
 */
function loadData(url, cb) {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', url)
  xhr.onload = () => {
    cb(xhr.status, xhr.responseText)
  }
}

从类中扩展和增强#

extends 注解允许你在从基本的JavaScript类扩展时指定通用参数。请看下面的例子。

/**
 * @template T
 * @extends {Set<T>}
 */
class SortableSet extends Set {
  // ...
}

@augments 另一方面允许你对通用参数进行更具体的处理。

/**
 * @augments {Set<string>}
 */
class StringSet extends Set {
  // ...
}

很方便!

底线#

TypeScript注解在普通JavaScript中的应用非常广泛。TypeScript还有更多的东西,特别是在输入泛型的时候,但是对于很多基本的任务来说,你不需要安装任何编译器就可以获得很多编辑器的超能力。