JSDoc으로 타입 정보 표현하기
JSDoc으로 타입 정보 표현하기 (JSDoc Reference)
JavaScript 파일에 타입 정보를 담아 쓰는 방법을 정리해 둔 문서예요. TypeScript 컴파일러가 JSDoc 주석에 적힌 타입 힌트를 읽어서 타입 검사를 해 줘요. 이 페이지는 그중에서 "어떤 태그가 현재 지원되는지"를 한눈에 보여주고, 각 태그가 TypeScript에서 어떻게 동작하는지 예시와 함께 설명해 줍니다.
출처: TypeScript 공식문서
본문
JavaScript 파일에 JSDoc 주석을 써서 타입 정보를 제공할 때, 현재 어떤 구성 요소(construct)가 지원되는지 목록으로 정리한 문서예요.
참고 사항:
- 아래 목록에 명시적으로 나오지 않은 태그(가령
@async같은 건)는 아직 지원되지 않아요.- TypeScript 파일에서는 문서화용 태그(documentation tags)만 지원돼요. 그 외 태그들은 전부 JavaScript 파일에서만 동작해요.
Types
@type@import@param(혹은@arg,@argument)@returns(혹은@return)@typedef@callback@template@satisfies
Classes
- Property Modifiers —
@public,@private,@protected,@readonly @override@extends(혹은@augments)@implements@class(혹은@constructor)@this
Documentation
문서화 태그는 TypeScript와 JavaScript 양쪽에서 모두 동작해요.
Other
전반적으로 의미는 jsdoc.app에 나온 태그 의미와 같거나, 그보다 조금 더 넓은 상위 집합(superset)이라고 보면 돼요. 아래 코드는 각 태그가 어떻게 다르고, 어떤 식으로 쓰이는지 예시를 들어 설명해 줍니다.
참고: 플레이그라운드에서 JSDoc 지원을 직접 실험해 볼 수 있어요.
Types
@type
@type 태그로 타입을 지정할 수 있어요. 그 타입은 다음 중 하나일 수 있어요.
- 원시 타입(primitive), 예를 들면
string이나number. - 전역이든 import든, TypeScript 선언에 정의된 타입.
- JSDoc
@typedef태그로 선언한 타입.
JSDoc 타입 문법 대부분과 TypeScript 문법 전부를 쓸 수 있답니다. 가장 기본적인 string부터 조건부 타입 같은 최신 기능까지요.
/** @type {string} */
var s;
/** @type {Window} */
var win;
/** @type {PromiseLike<string>} */
var promisedString;
// You can specify an HTML Element with DOM properties
/** @type {HTMLElement} */
var myElement = document.querySelector(selector);
element.dataset.myData = "";
@type에 유니언 타입도 줄 수 있어요. 예를 들어 값이 string이 될 수도, boolean이 될 수도 있다면 이렇게 쓰면 돼요.
/** @type {string | boolean} */
var sb;
배열 타입은 여러 문법으로 지정할 수 있어요.
/** @type {number[]} */
var ns;
/** @type {Array.<number>} */
var jsdoc;
/** @type {Array<number>} */
var nas;
객체 리터럴 타입도 지정할 수 있어요. 'a'(string)와 'b'(number) 프로퍼티를 가진 객체라면 이렇게 쓰죠.
/** @type {{ a: string, b: number }} */
var var9;
맵(map)처럼 생긴 객체나 배열(array)처럼 생긴 객체는 string·number 인덱스 시그니처로 지정할 수 있어요. 표준 JSDoc 문법이든 TypeScript 문법이든 상관없어요.
/**
* A map-like object that maps arbitrary `string` properties to `number`s.
*
* @type {Object.<string, number>}
*/
var stringToNumber;
/** @type {Object.<number, object>} */
var arrayLike;
위 두 타입은 TypeScript 타입 { [x: string]: number }와 { [x: number]: any }와 동일해요. 컴파일러는 두 문법을 모두 이해합니다.
함수 타입도 TypeScript 문법이나 Google Closure 문법으로 지정할 수 있어요.
/** @type {function(string, boolean): number} Closure syntax */
var sbn;
/** @type {(s: string, b: boolean) => number} TypeScript syntax */
var sbn2;
아니면 그냥 타입을 정하지 않은 Function 타입을 써도 되고요.
/** @type {Function} */
var fn7;
/** @type {function} */
var fn6;
Closure에서 온 다른 타입들도 동작해요.
/**
* @type {*} - can be 'any' type
*/
var star;
/**
* @type {?} - unknown type (same as 'any')
*/
var question;
Casts
TypeScript는 캐스트(cast) 문법을 Google Closure에서 가져왔어요. 괄호로 감싼 어떤 표현식 앞에 @type 태그를 달아서 타입을 다른 타입으로 캐스팅할 수 있죠.
/**
* @type {number | string}
*/
var numberOrString = Math.random() < 0.5 ? "hello" : 100;
var typeAssertedNumber = /** @type {number} */ (numberOrString);
TypeScript에서처럼 const로 캐스팅하는 것도 가능해요.
let one = /** @type {const} */(1);
Import types
import types로 다른 파일에서 선언을 가져올 수 있어요. 이 문법은 TypeScript 전용이고, JSDoc 표준과는 조금 달라요.
// @filename: types.d.ts
export type Pet = {
name: string,
};
// @filename: main.js
/**
* @param {import("./types").Pet} p
*/
function walk(p) {
console.log(`Walking ${p.name}...`);
}
import types는 어떤 모듈에서 값의 타입을 모를 때, 혹은 그 타입이 커서 일일이 쓰기 번거로울 때 값의 타입을 가져오는 데 쓸 수 있어요.
/**
* @type {typeof import("./accounts").userAccount}
*/
var x = require("./accounts").userAccount;
@import
@import 태그는 다른 파일의 export를 참조하게 해 줘요.
/**
* @import {Pet} from "./types"
*/
/**
* @type {Pet}
*/
var myPet;
myPet.name;
이 태그들은 런타임에 실제로 파일을 import 하지는 않아요. 이 태그로 스코프에 들어온 심볼은 타입 검사를 위해서 JSDoc 주석 안에서만 쓸 수 있답니다.
// @filename: dog.js
export class Dog {
woof() {
console.log("Woof!");
}
}
// @filename: main.js
/** @import { Dog } from "./dog.js" */
const d = new Dog(); // error!
@param and @returns
@param은 @type과 같은 타입 문법을 쓰되, 파라미터 이름이 하나 더 붙어요. 파라미터 이름을 대괄호로 감싸면 선택적(optional) 파라미터로 선언할 수도 있어요.
// Parameters may be declared in a variety of syntactic forms
/**
* @param {string} p1 - A string param.
* @param {string=} p2 - An optional param (Google Closure syntax)
* @param {string} [p3] - Another optional param (JSDoc syntax).
* @param {string} [p4="test"] - An optional param with a default value
* @returns {string} This is the result
*/
function stringsStringStrings(p1, p2, p3, p4) {
// TODO
}
함수의 반환 타입도 마찬가지예요.
/**
* @return {PromiseLike<string>}
*/
function ps() {}
/**
* @returns {{ a: string, b: number }} - May use '@returns' as well as '@return'
*/
function ab() {}
@typedef, @callback, and @param
@typedef로 복잡한 타입을 정의할 수 있어요. @param도 비슷한 문법을 써요.
/**
* @typedef {Object} SpecialType - creates a new type named 'SpecialType'
* @property {string} prop1 - a string property of SpecialType
* @property {number} prop2 - a number property of SpecialType
* @property {number=} prop3 - an optional number property of SpecialType
* @prop {number} [prop4] - an optional number property of SpecialType
* @prop {number} [prop5=42] - an optional number property of SpecialType with default
*/
/** @type {SpecialType} */
var specialTypeObject;
specialTypeObject.prop3;
첫 줄에는 object든 Object든 둘 다 쓸 수 있어요.
/**
* @typedef {object} SpecialType1 - creates a new type named 'SpecialType1'
* @property {string} prop1 - a string property of SpecialType1
* @property {number} prop2 - a number property of SpecialType1
* @property {number=} prop3 - an optional number property of SpecialType1
*/
/** @type {SpecialType1} */
var specialTypeObject1;
@param은 일회용(one-off) 타입 지정에 비슷한 문법을 제공해요. 이때 중첩된 프로퍼티 이름 앞에는 반드시 파라미터 이름을 붙여야 한다는 점을 기억하세요.
/**
* @param {Object} options - The shape is the same as SpecialType above
* @param {string} options.prop1
* @param {number} options.prop2
* @param {number=} options.prop3
* @param {number} [options.prop4]
* @param {number} [options.prop5=42]
*/
function special(options) {
return (options.prop4 || 1001) + options.prop5;
}
@callback은 @typedef와 비슷한데, 객체 타입 대신 함수 타입을 지정해요.
/**
* @callback Predicate
* @param {string} data
* @param {number} [index]
* @returns {boolean}
*/
/** @type {Predicate} */
const ok = (s) => !(s.length % 2);
물론 이런 타입들 전부 한 줄짜리 @typedef 안에서 TypeScript 문법으로도 선언할 수 있어요.
/** @typedef {{ prop1: string, prop2: string, prop3?: number }} SpecialType */
/** @typedef {(data: string, index?: number) => boolean} Predicate */
@template
@template 태그로 타입 파라미터를 선언할 수 있어요. 이걸로 함수·클래스·타입을 **제네릭(generic)**으로 만들 수 있죠.
/**
* @template T
* @param {T} x - A generic parameter that flows through to the return type
* @returns {T}
*/
function id(x) {
return x;
}
const a = id("string");
const b = id(123);
const c = id({});
타입 파라미터를 여러 개 선언하려면 콤마로 구분하거나 태그를 여러 번 쓰면 돼요.
/**
* @template T,U,V
* @template W,X
*/
타입 파라미터 이름 앞에 타입 제약(constraint)을 붙일 수도 있어요. 다만 목록에서 첫 번째 타입 파라미터만 제약을 받아요.
/**
* @template {string} K - K must be a string or string literal
* @template {{ serious(): string }} Seriousalizable - must have a serious method
* @param {K} key
* @param {Seriousalizable} object
*/
function seriousalize(key, object) {
// ????
}
마지막으로, 타입 파라미터의 기본값을 지정할 수도 있어요.
/** @template [T=object] */
class Cache {
/** @param {T} initial */
constructor(initial) {
}
}
let c = new Cache()
@satisfies
@satisfies는 TypeScript의 후치(postfix) 연산자 satisfies를 JSDoc에서 쓸 수 있게 해 줘요. satisfies는 어떤 값이 특정 타입을 구현한다는 것을 선언하되, 그 값 자체의 타입은 바꾸지 않아요.
// @ts-check
/**
* @typedef {"hello world" | "Hello, world"} WelcomeMessage
*/
/** @satisfies {WelcomeMessage} */
const message = "hello world"
/** @satisfies {WelcomeMessage} */
const failingMessage = "Hello world!" // error: Type '"Hello world!"' does not satisfy the expected type 'WelcomeMessage'.
/** @type {WelcomeMessage} */
const messageUsingType = "hello world"
Classes
클래스는 ES6 클래스로 선언할 수 있어요.
class C {
/**
* @param {number} data
*/
constructor(data) {
// property types can be inferred
this.name = "foo";
// or set explicitly
/** @type {string | null} */
this.title = null;
// or simply annotated, if they're set elsewhere
/** @type {number} */
this.size;
this.initialize(data); // Should error, initializer expects a string
}
/**
* @param {string} s
*/
initialize = function (s) {
this.size = s.length;
};
}
var c = new C(0);
// C should only be called with new, but
// because it is JavaScript, this is allowed and
// considered an 'any'.
var result = C(1);
혹은 생성자 함수(constructor function)로 선언할 수도 있어요. 그럴 땐 @constructor를 @this와 함께 쓰면 돼요.
Property Modifiers
@public, @private, @protected는 TypeScript의 public, private, protected와 똑같이 동작해요.
// @ts-check
class Car {
constructor() {
/** @private */
this.identifier = 100;
}
printIdentifier() {
console.log(this.identifier);
}
}
const c = new Car();
console.log(c.identifier); // error: Property 'identifier' is private and only accessible within class 'Car'.
@public은 항상 암시되는 접근자라 생략할 수 있어요. 어떤 곳에서든 그 프로퍼티에 접근할 수 있다는 뜻이에요.@private은 그 프로퍼티를 담고 있는 클래스 안에서만 쓸 수 있다는 뜻이에요.@protected는 담고 있는 클래스와 그 파생 서브클래스 안에서만 쓰되, 담고 있는 클래스의 다른 인스턴스에서는 쓸 수 없다는 뜻이에요.
@public, @private, @protected는 생성자 함수(constructor functions)에서는 동작하지 않아요.
@readonly
@readonly 수정자는 프로퍼티가 초기화할 때만 쓰여지도록 보장해 줘요.
// @ts-check
class Car {
constructor() {
/** @readonly */
this.identifier = 100;
}
printIdentifier() {
console.log(this.identifier);
}
}
const c = new Car();
console.log(c.identifier);
@override
@override는 TypeScript에서와 똑같이 동작해요. 기반 클래스의 메서드를 오버라이드하는 메서드에 붙여 주면 됩니다.
export class C {
m() { }
}
class D extends C {
/** @override */
m() { }
}
오버라이드 검사를 켜려면 tsconfig에 noImplicitOverride: true를 설정해 주세요.
@extends
JavaScript 클래스가 제네릭 기반 클래스를 상속할 때, 타입 인자를 넘겨줄 JavaScript 문법은 따로 없어요. @extends 태그가 그 역할을 해 줘요.
/**
* @template T
* @extends {Set<T>}
*/
class SortableSet extends Set {
// ...
}
@extends는 클래스에서만 동작한다는 점을 기억하세요. 지금은 생성자 함수가 클래스를 상속할 방법이 없어요.
@implements
마찬가지로, TypeScript 인터페이스를 구현할 JavaScript 문법도 따로 없어요. @implements 태그가 TypeScript에서처럼 동작합니다.
/** @implements {Print} */
class TextBook {
print() {
// TODO
}
}
@constructor
컴파일러는 this 프로퍼티 할당을 보고 생성자 함수를 추론할 수 있어요. 하지만 @constructor 태그를 추가하면 검사를 더 엄격하게, 제안(suggestion)을 더 정확하게 만들 수 있어요.
/**
* @constructor
* @param {number} data
*/
function C(data) {
// property types can be inferred
this.name = "foo";
// or set explicitly
/** @type {string | null} */
this.title = null;
// or simply annotated, if they're set elsewhere
/** @type {number} */
this.size;
this.initialize(data); // error: Argument of type 'number' is not assignable to parameter of type 'string'.
}
/**
* @param {string} s
*/
C.prototype.initialize = function (s) {
this.size = s.length;
};
var c = new C(0);
c.size;
var result = C(1); // error: Value of type 'typeof C' is not callable. Did you mean to include 'new'?
@constructor를 쓰면 생성자 함수 C 안에서 this를 검사해 줘요. 그래서 initialize 메서드에 대한 제안을 받고, 숫자를 넘기면 오류가 나요. 또 C를 호출(생성) 대신 사용하면 에디터가 경고를 보여줄 수도 있어요.
다만 아쉽게도, 이 때문에 호출도 가능한 생성자 함수(callable인 constructor)는 @constructor를 쓸 수 없어요.
@this
컴파일러는 문맥(context)이 있으면 보통 this의 타입을 알아내요. 문맥이 없을 때는 @this로 this의 타입을 직접 지정해 주면 됩니다.
/**
* @this {HTMLElement}
* @param {*} e
*/
function callbackForLater(e) {
this.clientHeight = parseInt(e); // should be fine!
}
Documentation
@deprecated
함수·메서드·프로퍼티가 deprecated(더 이상 권장하지 않음)될 때, /** @deprecated */ JSDoc 주석을 달아서 사용자에게 알릴 수 있어요. 이 정보는 자동완성 목록과 제안 진단(suggestion diagnostic)으로 표시되고, 에디터가 특별하게 처리할 수 있어요. VS Code 같은 에디터에서는 보통 취소선 스타일로 표시돼요.
/** @deprecated */
const apiV1 = {};
const apiV2 = {};
apiV;
@see
@see는 프로그램 안의 다른 이름으로 링크를 걸어 줘요.
/** @see Box for implementation details */
type Boxify<T> = { [K in keyof T]: Box<T> };
일부 에디터는 Box를 링크로 바꿔서, 그쪽으로 점프했다가 다시 돌아오기 쉽게 만들어 줘요.
@link
@link는 @see와 비슷한데, 다른 태그 안에서도 쓸 수 있다는 점이 달라요.
/** @returns A {@link Box} containing the parameter. */
function box<U>(u: U): Box<U> {
return { t: u };
}
프로퍼티도 링크할 수 있어요.
/**
* Note: you should implement the {@link Pet.hello} method of Pet.
*/
function hello(p: Pet) {
p.hello()
}
선택 이름(optional name)을 붙일 수도 있어요.
/**
* Note: you should implement the {@link Pet.hello | hello} method of Pet.
*/
function hello(p: Pet) {
p.hello()
}
Other
@enum
@enum 태그는 모든 멤버가 지정된 타입인 객체 리터럴을 만들 수 있게 해 줘요. JavaScript의 대부분의 객체 리터럴과 달리, 다른 멤버는 허용하지 않아요. @enum은 Google Closure의 @enum 태그와 호환되도록 만들어진 거예요.
/** @enum {number} */
const JSDocState = {
BeginningOfLine: 0,
SawAsterisk: 1,
SavingComments: 2,
};
JSDocState.SawAsterisk;
@enum은 TypeScript의 enum과는 꽤 다르고 훨씬 단순해요. 그리고 TypeScript enum과 달리 @enum은 어떤 타입이든 가질 수 있어요.
/** @enum {function(number): number} */
const MathFuncs = {
add1: (n) => n + 1,
id: (n) => -n,
sub1: (n) => n - 1,
};
MathFuncs.add1;
@author
@author로 아이템의 작성자를 지정할 수 있어요.
/**
* Welcome to awesome.ts
* @author Ian Awesome <[email protected]>
*/
이메일 주소는 반드시 꺾쇠 괄호(< >)로 감싸는 걸 잊지 마세요. 그렇지 않으면 @example이 새 태그로 파싱되어 버립니다.
Other supported patterns
var someObj = {
/**
* @param {string} param1 - JSDocs on property assignments work
*/
x: function (param1) {},
};
/**
* As do jsdocs on variable assignments
* @return {Window}
*/
let someFunc = function () {};
/**
* And class methods
* @param {string} greeting The greeting to use
*/
Foo.prototype.sayHi = (greeting) => console.log("Hi!");
/**
* And arrow function expressions
* @param {number} x - A multiplier
*/
let myArrow = (x) => x * x;
/**
* Which means it works for function components in JSX too
* @param {{a: string, b: number}} props - Some param
*/
var fc = (props) => <div>{props.a.charAt(0)}</div>;
/**
* A parameter can be a class constructor, using Google Closure syntax.
*
* @param {{new(...args: any[]): object}} C - The class to register
*/
function registerClass(C) {}
/**
* @param {...string} p1 - A 'rest' arg (array) of strings. (treated as 'any')
*/
function fn10(p1) {}
/**
* @param {...string} p1 - A 'rest' arg (array) of strings. (treated as 'any')
*/
function fn9(p1) {
return p1.join();
}
Unsupported patterns
객체 리터럴 타입 안의 프로퍼티 타입에 **후치 등호(postfix equals)**를 붙여도 선택적 프로퍼티를 지정하지 못해요.
/**
* @type {{ a: string, b: number= }}
*/
var wrong;
대신 이렇게 프로퍼티 이름 뒤에 물음표를 붙여 주세요.
/**
* Use postfix question on the property name instead:
* @type {{ a: string, b?: number }}
*/
var right;
널러블(nullable) 타입은 strictNullChecks가 켜져 있을 때만 의미가 있어요.
/**
* @type {?number}
* With strictNullChecks: true -- number | null
* With strictNullChecks: false -- number
*/
var nullable;
TypeScript 고유 문법은 유니언 타입이에요.
/**
* @type {number | null}
* With strictNullChecks: true -- number | null
* With strictNullChecks: false -- number
*/
var unionNullable;
널러블이 아닌(non-nullable) 타입은 아무 의미가 없고, 그냥 원래 타입으로 취급돼요.
/**
* @type {!number}
* Just has type number
*/
var normal;
JSDoc의 타입 시스템과 달리, TypeScript는 타입이 null을 포함하는지 아닌지만 표시할 수 있어요. 명시적인 "non-nullable"이라는 개념은 없어요. strictNullChecks가 켜져 있으면 number는 널러블이 아니고, 꺼져 있으면 number는 널러블이에요.
Unsupported tags
TypeScript는 지원되지 않는 JSDoc 태그를 그냥 무시해요. 다음 태그들은 지원을 요청하는 열린 이슈가 있는 태그들이에요.
@memberof(issue #7237)@yields(issue #23857)@member(issue #56674)
Legacy type synonyms
오래된 JavaScript 코드와 호환되도록, 흔한 타입 몇 가지에 별칭(alias)이 지정되어 있어요. 별칭 중 일부는 기존 타입과 같은 것들인데, 대부분은 거의 쓰이지 않아요. 예를 들어 String은 string의 별칭으로 취급돼요. String이 TypeScript에서 실제 타입이긴 하지만, 옛 JSDoc에서는 string을 뜻할 때 흔히 쓰곤 했어요. 게다가 TypeScript에서 **첫 글자를 대문자로 쓴 원시 타입은 래퍼 타입(wrapper type)**이라, 거의 항상 실수로 쓰인 거예요. 그래서 컴파일러는 옛 JSDoc에서의 사용 방식에 따라 이 타입들을 동의어(synonym)로 취급해요.
String -> stringNumber -> numberBoolean -> booleanVoid -> voidUndefined -> undefinedNull -> nullfunction -> Functionarray -> Array<any>promise -> Promise<any>Object -> anyobject -> any
마지막 네 개의 별칭은 noImplicitAny: true일 때 꺼져요.
object와Object는 내장 타입인데,Object는 거의 쓰이지 않아요.array와promise는 내장 타입이 아니지만, 프로그램 어딘가에 선언되어 있을 수도 있어요.