👯‍♀️

JSDocで型を定義してTypeScriptの恩恵を受ける

6 min read 1

普段からTypeScriptで開発されている方ならば、TypeScriptの恩恵を十分に堪能されていることと思います。

特に私はVS Codeのインテリセンスには頼りっぱなしでこれがなければまともにコーディングができない体になってしまいました。

しかし時には、TypeScriptという概念が存在しない退屈な世界で開発を行わなけらばならない状況はあるでしょう。私はもはやキーボードでタイピングすることすらままなりません。

そんなJavaScriptしか利用できない状況で代替手段となるのが、JSDocです。JSDocのアノテーションによって型を付与することによって最低限の支援を受けることができます。

@Type

@Typeタグを使用すると、TypeScriptで型をつけるときと同じように型を宣言することができます。

/** @type {number}  年齢*/
let age;

マウスオーバーをするとJSDocに記載した情報を表示してくれますし、もちろんインテリセンスもしっかりと効いてくれます。

基本的な型

TypeScriptに用意されている基本的な型はほとんどサポートされています。

/** @type {string} */
let string

/** @type {number} */
let number

/** @type {boolean} */
let boolean

/** @type {string[]} */
let array

/** @type {[number, number]} */
let tuple

/** @type {null} */
let __null__

/** @type {undefined} */
let __undefined__

/** @type {any} */
let any

/** @type {(num1: number, num2: number) => number)} */
const fn = (num1, num2) => {
  return num1 + num2
}

/** @type {{ name: string, age: number}} */
let obj

Union型

Union型も定義することができます。その場合、型ガードによって絞り込みをすることもできます。


/** @type {string|number} */
let strOrNum

if (typeof strOrNum === 'string') {
  strOrNum // string
} else if (typeof strOrNum === 'number') {
  strOrNum // number
} 
/** @type {'red'|'green'} */
let color

Enum

@enumでEnum(列挙型)を表現することができますが、TypeScriptの提供するEnumとは異なります。
まず、JSDocのEnumはとてもシンプルです。

/** @enum {number} */
const color = {
  red: 0,
  green: 1,
  blue: 2
}

color.blue


また、任意の型を持つこともできます。

/** @enum {(n1: number, n2: number) => number)} */
const MathFuncs = {
  add: (n1, n2) => n1 + n2,
  sub: (n1, n2) => n1 - n2,
};
 
MathFuncs.add

インデックスシグネチャ

インデックスシグネチャ({ [x: string]: number })ど同等の型も定義できます。

/**
 *
 * @type {Object.<string, number>}
 */
var stringToNumber;

stringToNumber['a'] = 'h'

this

@thisによって、コンテキスト型thisの型を推測できない場合にも明示的に型を指定することができます。

const button = document.getElementById('button')

button.addEventListener('click', onClick)

/**
 * @this {HTMLElement}
 */
function onClick() {
  this.innerHTML = 'clicked!'
}

キャスト

丸括弧で囲まれた式の前に@typeタグを追加することで、型を他の型にキャストすることができます。

/**
 * @type {number | string}
 */
 let numberOrString = Math.random() < 0.5 ? "hello" : 100;
 let typeAssertedNumber = /** @type {number} */ (numberOrString);

インポート型

他ファイルの型宣言から、インポートして使うことができます。これはTypeScript固有のもので、JSDocの標準ではありません。

// User.ts
export interface User {
  name: string
  age: number
}
// index.js
/** @type { import("./User").User;} */
let user

もちろんライブラリの型定義をインポートすることもできます。

/** @type {import('axios').AxiosInstance} */
let instance

@paramと@returns

@typeと同じ構文で、関数の引数(@param)と返り値(@returns)に型をつけることができます。

/**
 * 
 * @param {number} a パラメータ1
 * @param {number} b パラメータ2
 * @param {number} [c] 任意のパラメータ
 * @param {number} [d=1] デフォルト値をもつパラメータ 
 * @returns {number} 返り値
 */
const calc = (a, b, c, d = 1) => {
  return a + b + c + d
}

スクリーンショット 2021-08-11 8.17.05.png

型を定義する

@typedef

Object型は複雑になりやすく、再利用するときに同じような記述をするのは冗長です。

TypeScriptでinterfacetypeを定義するのと同じように、@typedefタグで型を定義することができます。

/**
 * @typedef User
 * @property {string} name ユーザー名
 * @property {number} age ユーザーの年齢
 * 
 */

/**
 * @type {User}
 */
let user1

/**
 * @type {User}
 */
let user2

@param

@paramを使うと、一度限りの方を指定することができます。プロパティはネストした表現をします。

/**
 * 
 * @param {Object} user 
 * @param {string} user.name 
 * @param {number} user.age 
 */
const fn = ({ name, age }) => {
  console.log({ name, age })
}

@callback

@callbackは、オブジェクトではなく関数の型を定義します。

/**
 * @callback Calc
 * @param {number} num1
 * @param {number} num2
 * @returns {number}
 */

/** @type {Calc} */
const add = (num1, num2) => {
  return num1 + num2
}

/** @type {Calc} */
const sub = (num1, num2) => {
  return num1 - num2
} 

ジェネリクス

@templateを使うと、ジェネリクスを表現することができます。

/**
 * @template T
 * @param {T} x
 * @return {T}
 */
const test = (x) => {
  return x;
}
 
const a = test("string");
const b = test(123);
const c = test({});

しかしながら、ジェネリクスを使えるのは関数のみで、クラスや型宣言にはサポートされていません。

@ts-check

JSDocによって型を手に入れ快適な生活を送ることができましたが、一つだけ重大な問題点が存在します。

JSDocによって得られたものは所詮型ヒントによる推測にすぎず、実際はただのJavaScriptファイルであることを忘れてはいけません。

以下のように、JSDocで宣言した型以外を代入してもなにも警告をしてくれません。

/** @type {number} */
let num;

num = 1
num = '10'
num = [1, 2, 3]

num.toFixed() // TypeError: num.toFixed is not a function

JavaScriptファイルにおいて警告を有効にしたい場合には、最初の行に@ts-checkを使用します。

// @ts-check
/** @type {number} */
let num;

num = 1
num = '10' // Type 'string' is not assignable to type 'number'.
num = [1, 2, 3] // type 'number[]' is not assignable to type 'number'

もし多くのファイルに対して@ts-checkを適用させたい場合には、代わりにtsconfig.jsonを配置するようにしましょう。

クラスのアクセス修飾子

JSDocによって、publicprivateprotectedreadonlyのようなアクセス修飾子を付与することができます。

// @ts-check
class User {
  /**
   * 
   * @param {string} name 
   * @param {number} age 
   */
  constructor(name, age) {
    /** @readonly */
    this.id = 1
  
    /** @public */
    this.name = name
  
    /** @private */
    this.age = age
  }
}

const user = new User('user1', 24)

user.name // ok
user.age // Property 'age' is private and only accessible within class 'User'.
user.id = 2 // Cannot assign to 'id' because it is a read-only property.

参考

GitHubで編集を提案

Discussion

おお…
@paramぐらいしか使っていなかったので勉強になりました。
JS-Docで型チェックが発生できるとは…

これは訳アリでjsしか使えないときに非常に便利…

ログインするとコメントできます