[Office]TypescriptとWebpackを使ってWSHを書いてOfficeを操作する

Office製品では自動化やバッチ処理でVBAやWSH(JScript)を使うことができるのだが、最近流行りのREST APIやAdd-inを書くのに使えるTypescriptと比べると書きにくい印象を受ける(きっとアレルギー?)。それでも組織のセキュリティの都合でVBAやWSHしか選択肢が無い場合がある。(産業スパイのお手伝いをするわけにはいかないから…)

とはいえVBAは今どきやりたくないし、Powershellは独特だし、JScriptは型チェックが無くて何気に書きにくいし…何かないかと思っていたら、WSHをTypescriptで書くことができるという大分古い記事を見つけた([TypeScriptでWSHを書いちゃおう](https://takanosho.wordpress.com/2012/12/24/visual-studio-advent-calendar-2012/))。

調べてみると、Node.jsを普段使っている人であれば普段と変わらない環境でTypescriptとWebpackを活用してWSHを書くことができることが分かった。これにより、`Node.js`を入れられない一般ユーザ向けにスクリプトを書く時でも、言語を変えなくて済むようになる。(以下はOutlookでメールを作成する例)

----
(追記)色々がんばってみたがWSHはやっぱり貧弱だった...残念。
とりあえず単純な処理だけやらせられればOKだけど。
(追記2)Powershellをやってみたら意外といけた。以下に、Powershellで定期的にOutlookの予定を収集させ、ConfluenceのContent-Propertyに投稿するという、とてもニッチなスクリプトを載せたので、今度記事に書く予定。
https://github.com/stm32p103/jira-confluence/tree/master/ps


----

```typescript:index.ts
let outlook = new ActiveXObject( 'Outlook.Application' );

const message = outlook.CreateItem(Outlook.OlItemType.olMailItem);
message.Subject = 'Hello, world!';
message.Save();
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/7c09cf09-2968-884b-c5c9-f510ea43f2d5.png)
ここでは、TypescriptとWebpackでWSHを作るときに必要なこと・気を付けることをまとめる。

#TypescriptでWSHを書く
まずはTypescriptで書き、JScriptに変換し、実行する手段を用意する。といっても、Node.jsを使っているなら特に新しいことはない。

## 必要なファイルを用意する
いつも通り`package.json`を作って、必要なパッケージをインストールして、`tsconfig.json`を書いて、`tsc`でトランスパイルすればよい。以下は、既に型情報を追加した状態の`package.json`。

```json:package.jsonの例(抜粋)
{
  "name": "wsh-sample",
  "main": "index.ts",
  "devDependencies": {
    "typescript": "^3.6.3"
  },
  "dependencies": {
    "@types/typescript": "^2.0.0",
    "@types/activex-outlook": "^14.0.5",
    "@types/activex-scripting": "^1.0.7",
    "@types/windows-script-host": "^5.8.3"
  }
}
```

`tsconfig.json`(トランスパイラtscの設定)では、2つ気を付けた方が良いことがある。それ以外はあまり重要ではなさそうだった。

* lib
    * `scripthost`は設定しない。この後インストールする`activex-scripting`などと定義が重複するため。
    * `dom`は設定しない。WSHには`console`は存在しないから。
* target
    * WSHが対応するJavascriptの言語仕様は古いため、`es3`にする必要がある。
    * es3ではファイルごとにスコープが分かれていないらしく、グローバルスコープで名前が衝突すると`Cannot redeclare block-scoped variable`というエラー出てしまうため注意が必要。

```json:tsconfig.jsonの例
{
  "compileOnSave": false,
  "compilerOptions": {
    "baseUrl": ".",
    "module": "es2015",
    "outDir": "dist",
    "declaration": true,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "target": "es3",
    "sourceMap": false,
    "moduleResolution": "node",
    "strictNullChecks": false,
    "allowSyntheticDefaultImports": true,
    "typeRoots": [
      "node_modules/@types"
    ],
    "lib": [
      "es6",
      "es2015"
    ]
  },
  "exclude": [ "node_modules", "dist" ]
}
```

## 複数ファイルに分割するならWebpackを使う
Typescriptが扱えるよう`ts-loader`をインストールし、設定ファイル`webpack.config.js`を用意する。この辺もNode.jsとは特に変わりがない。(ただし、Node.js上で動く前提のパッケージはWSH上で動かないので、あまりうれしくないかもしれない)

```bash:Webpackインストール
npm install --save-dev webpack webpack-cli ts-loader
```

```javascript:webpack.config.jsの例
module.exports = {
	mode: "development",
	entry: "./index.ts",
	output: {
		filename: "index.js"
	},
	resolve: {
		extensions: [".ts", ".tsx", ".js", "json" ]
	},
	module: {
		rules: [
			{ test: /\.tsx?$/, loader: "ts-loader" },
		]
	}
};
```
`webpack`(デフォルトで設定ファイル`webpack.config.js`を参照)コマンドで必要なファイルをトランスパイルすれば、`tsconfig.json`で設定した出力フォルダ(ここでは`dist`)に`webpack.config.js`に指定した`output`のファイル名(ここでは`index.js`)が生成される。

## 自動でトランスパイルする
特に試行錯誤している段階では、ソースを編集後`tsc`や`webpack`コマンドを実行するのが面倒な場合がある。そんな時は、`tsc --watch`や`webpack --watch`を使うことで、変更を検知して自動でトランスパイルするようにしておくと楽になる。

## 型情報を入手する
Typescriptでは@types/*に型情報が登録されていると、Eclipseなどで入力補完することができるようになる。型情報は[TypeSearch](https://microsoft.github.io/TypeSearch/)で検索することができる。

型情報をインストールした後で.tsファイルを見ると、入力補完が使えるようになる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/773c21d9-3371-b9fc-7e14-122eac366b3c.png)

### 型情報の例
`ActiveXObject("Outlook.Application")`の型情報は`activex-outlook`という風に、「activex-<ドットで区切られる先頭の部分>」と命名されている。例えば`Msxml2.DOMDocument`なら`activex-msxml2`になる。TypescriptはMicrosoftが作っているためか、型情報がちゃんと用意されているようだった。

* WSH: https://www.npmjs.com/package/@types/windows-script-host
* Scriptiong: https://www.npmjs.com/package/@types/activex-scripting
* Outlook: https://www.npmjs.com/package/@types/activex-outlook
* Excel: https://www.npmjs.com/package/@types/activex-excel
* MSXML2: https://www.npmjs.com/package/@types/activex-msxml2

```:型情報のインストール
npm install --save-dev @types/windows-script-host
npm install --save-dev @types/activex-scripting
npm install --save-dev @types/activex-outlook
npm install --save-dev @types/activex-excel
npm install --save-dev @types/activex-msxml2
```

###OLE Programmataic Identifierの調べ方
`ActiveXObject`でオブジェクトを作る際に指定する文字列(例えば`Outlook.Application`)は、OLE Programmatic Identifierという。この文字列は`activex-outlook`などの型情報の中で定義されていて、`ActiveXObject(identifier)`の戻り値の型を調べるのに使われている。

その定義は`@types/activex-*/index.d.ts`の`ActiveXObjectNameMap`インターフェースにある。このインターフェースのメンバは[OLE Programmatic Identifiers(Outlook)](https://docs.microsoft.com/ja-jp/office/vba/outlook/concepts/getting-started/ole-programmatic-identifiers-outlook)に載っているものと一致している。

例えば`activex-outlook`なら以下のようになっていて、

```typescript:@types/activex-outlook/index.d.ts
interface ActiveXObjectNameMap {
    'DOCSITE.DocSiteControl': Outlook._DocSiteControl;
    'Outlook.Application': Outlook.Application;
    'Outlook.OlkBusinessCardControl': Outlook.OlkBusinessCardControl;
    ...
```
`activex-msxml2`なら以下のようになっている。

```typescript:@types/activex-msxml2/index.d.ts
interface ActiveXObjectNameMap {
    'Msxml2.DOMDocument': MSXML2.DOMDocument60;
    'Msxml2.DOMDocument.6.0': MSXML2.DOMDocument60;
    'Msxml2.FreeThreadedDOMDocument': MSXML2.FreeThreadedDOMDocument60;
    ...
```

`ActiveXObject`の引数で引用符('')を打てば、入力補完が働くようになっている。(引用符を打たないとダメだったので、初めは補完が働かないものと誤解していた)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/fea6ab7f-7eb2-de83-ab4e-095de4524afe.png)


## 実行する
単一ファイルなら`tsc`、複数ファイルなら`webpack`を使ってトランスパイルした結果(ここでは`index.js`)を、`cscript`か`wscript`で実行する。間違ってNode.jsで実行してもエラーになってしまうので注意すること。

例えば、以下の`index.ts`をトランスパイルして実行すると、

```typescript:index.ts
WSH.Echo( 'Hello.' );
let a = 1;
let b = 2;
let c = a + b;
WSH.Echo( 'a + b = ' + c );
```
以下のように、期待した文字が表示される。

```
>cscript dist\index.js
Microsoft (R) Windows Script Host Version 5.812
Copyright (C) Microsoft Corporation. All rights reserved.

Hello.
a + b = 3
```

`cscript`の代わりに`wscript dist\index.js`を実行すると、以下のようにウィンドウが表示される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/97be2815-98cf-ff64-d773-e0efd50219ba.png)　![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/2b303d0a-e101-6802-74d4-d70541d0f4ae.png)

なお、`WSH.Echo`は`console.log`と比べると貧弱で、文字列しか出力できない。複雑なオブジェクトを表示したければ、Visual Studioのデバッガを使う。

```
cscript dist\index.js //x
```
を実行すると、デバッガを起動することができる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/118c04a7-ef0d-9696-9181-35a93f7ce8ef.png)

デバッガの画面では`index.js`のソースが表示され、ブレークポイントを設定したり、ステップ実行したりできる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/4f657085-3994-59ae-dbee-3d88e17a91cb.png) ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/08ef7cc7-bf0b-8fc9-832c-4039c2d6ce14.png)

# Officeを操作する
幾つかのお題で試してみる。

## Outlookでメッセージを作る
`ActiveXObject()`でOutlookのオブジェクトを作り、CreateItemメソッドを実行することで、新しいアイテムを作る。アイテムの種類は`OlItemType`列挙で指定する。メッセージの場合は、`Outlook.OlItemType.olMailItem`となる。(列挙名はPascalCase、メンバ名はcamelCaseの模様)

```typescript:index.ts
let outlook = new ActiveXObject( 'Outlook.Application' );

const message = outlook.CreateItem(Outlook.OlItemType.olMailItem);
message.Subject = 'Hello, world!';
message.Save();
```

* [Outlook.Applicationオブジェクト](https://docs.microsoft.com/ja-jp/office/vba/api/outlook.application)
* [CreateItemメソッド](https://docs.microsoft.com/ja-jp/office/vba/api/outlook.application.createitem)
* [OlItemType列挙](https://docs.microsoft.com/ja-jp/office/vba/api/outlook.olitemtype)

トランスパイルして実行すると、デバッガ上でメッセージを作成しているところが覗ける。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/309346cf-d4b2-7b91-5303-ddb617496a8e.png)
Outlookを開くと、下書きに以下のようなメッセージができていることが確認できる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/7c09cf09-2968-884b-c5c9-f510ea43f2d5.png)

## Outlookで予定の情報を取得する
以下のような予定が1件しかない時に、その情報を出力してみる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/dbb62124-b371-45d4-4e06-fcd4675447e5.png)

予定表は`GetDefaultFolder`から`olFolderCalendar`を指定することで参照できる。予定表に対応するフォルダのオブジェクト`calendarFolder`から、プロパティ`Items`を取得することで、フォルダ内の予定を参照できるようになる。最初の要素は`GetFirst()`メソッドで取得できる。配列だろうと思って`entries[0]`と書くと、以降の処理で「Nullまたはオブジェクトではありません」とエラーが発生してしまうので注意する。

```typescript:index.ts
let outlook = new ActiveXObject( 'Outlook.Application' );

const mapi = outlook.GetNamespace( 'MAPI' );
const calendarFolder = mapi.GetDefaultFolder( Outlook.OlDefaultFolders.olFolderCalendar );
let entries = calendarFolder.Items;
let entry = entries.GetFirst();

if( entry ) {
  WSH.Echo( `${entry.Subject}@${entry.Location}: ${entry.Start} - ${entry.End}` );
}
```

* [GetNamespaceメソッド](https://docs.microsoft.com/ja-jp/office/vba/api/outlook.application.getnamespace)
* [NameSpaceオブジェクト](https://docs.microsoft.com/ja-jp/office/vba/api/outlook.namespace)('MAPI'しか指定できない?)
* [GetDefaultFolderメソッド](https://docs.microsoft.com/ja-jp/office/vba/api/outlook.namespace.getdefaultfolder)
* [OlDefaultFolders列挙](https://docs.microsoft.com/ja-jp/office/vba/api/outlook.oldefaultfolders)
* [Folderオブジェクト](https://docs.microsoft.com/ja-jp/office/vba/api/outlook.folder)

トランスパイルして実行すると、予定の情報が取得できていることが分かる。

```
>tsc
>cscript dist\index.js
Microsoft (R) Windows Script Host Version 5.812
Copyright (C) Microsoft Corporation. All rights reserved.

サンプル@どこか: Mon Sep 23 08:00:00 UTC+0900 2019 - Mon Sep 23 08:30:00 UTC+0900 2019
```

### Itemの型情報を得る
上記のスクリプトは動くには動くのだが、実は`entries.GetFirst()`の戻り値は`any`型になっているので、メンバの入力補完はできない。マニュアルでも`Item`が何かを調べてからでないとメンバにアクセスしてはならないと書いてある。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/4f689865-17a6-c2fc-bc39-79592c353a95.png)
>既定では、予定表フォルダーには**AppointmentItem** オブジェクトと**MeetingItem** オブジェクトを含めることができ、連絡先フォルダーには**ContactItem** オブジェクトと**distlistitem** オブジェクトを含めることができます。 通常、フォルダー内のアイテムを列挙する場合は、フォルダー内のアイテムの種類を想定しないでください。アイテムに適用されるプロパティにアクセスする前に、アイテムのメッセージクラスを確認してください。

`Folder`の`Items`プロパティのサンプルコードによると、`Class`メンバを見てからメンバにアクセスしていることが分かる。判定に使っている`olContact`は`OlObjectClass`列挙のメンバなので、この種類を調べればよいということらしい。

```vb:ContactItemを取得する例
If (myItem.Class = olContact) Then 
    MsgBox myItem.FullName & ": " & myItem.LastModificationTime 
End If 
```
なので、「予定表」から「会議」を取得するなら、以下のように`Item`が`olAppointment`であることを調べた後Type Assertionすることで、以後の処理でメンバを参照する際に入力補完や型チェックができるようになる。

```typescript:index.ts
let outlook = new ActiveXObject( 'Outlook.Application' );

const mapi = outlook.GetNamespace( 'MAPI' );
const calendarFolder = mapi.GetDefaultFolder( Outlook.OlDefaultFolders.olFolderCalendar );
let entries = calendarFolder.Items;
let tmp = entries.GetFirst();

while( tmp ) {
  if( tmp.Class == Outlook.OlObjectClass.olAppointment ) {
    const entry = tmp as Outlook.AppointmentItem;
    WSH.Echo( `${entry.Subject}@${entry.Location}: ${entry.Start} - ${entry.E}` );
    tmp = entries.GetNext();
  }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/39b82a6b-6aa9-6b54-631b-9a0c694e9d64.png)

* [Folderオブジェクト](https://docs.microsoft.com/ja-jp/office/vba/api/outlook.folder): 上記説明の引用元
* [Itemsプロパティ](https://docs.microsoft.com/ja-jp/office/vba/api/outlook.folder.items):サンプルコードにメッセージクラスの確認方法が載っている
* [AppointmentItemオブジェクト](https://docs.microsoft.com/ja-jp/office/vba/api/outlook.appointmentitem): 会議
* [MeetingItemオブジェクト](https://docs.microsoft.com/ja-jp/office/vba/api/outlook.meetingitem): 会議の案内や応答(出席依頼やキャンセル連絡、承諾・欠席の回答など)
* [OlObjectClass](https://docs.microsoft.com/ja-jp/office/vba/api/outlook.olobjectclass): Itemの区別に使う。`olAppointment`なら`AppointmentItem`クラスと判断できる

### 型の互換性はあるか?
例えば日付は、`VarDate`という型で、Typescriptの`Date`と異なる。そのままでは互換性が無いものの、変換は用意されている。`activex-interop`(他のActiveXObjectの型情報から参照される)で、`varDate`⇔`Date`の様な変換があることが分かる。

```typescript:@types/activex-interop
/** Automation date (VT_DATE) */
declare class VarDate {
    private constructor();
    private VarDate_typekey: VarDate;
}
// ここで、VarDateからDateに変換する
interface DateConstructor {
    new(vd: VarDate): Date;
}
// ここで、DateからVarDateに変換する
interface Date {
    getVarDate: () => VarDate;
}
```
試してみると、差が無いことが分かる。

```typescript:index.ts
let outlook = new ActiveXObject( 'Outlook.Application' );

const mapi = outlook.GetNamespace( 'MAPI' );
const calendarFolder = mapi.GetDefaultFolder( Outlook.OlDefaultFolders.olFolderCalendar );
let entries = calendarFolder.Items;
let tmp = entries.GetFirst();

while( tmp ) {
  if( tmp.Class == Outlook.OlObjectClass.olAppointment ) {
    const entry = tmp as Outlook.AppointmentItem;
    
    const startDate: Date = new Date( entry.Start );
    const startVarDate: VarDate = startDate.getVarDate(); 
    
    WSH.Echo( `Date   : ${startDate}` );
    WSH.Echo( `varDate: ${startVarDate}` );
    tmp = entries.GetNext();
  }
}
```

```
>cscript dist\index.js実行結果
Microsoft (R) Windows Script Host Version 5.812
Copyright (C) Microsoft Corporation. All rights reserved.

Date   : Mon Sep 23 08:00:00 UTC+0900 2019
varDate: Mon Sep 23 08:00:00 UTC+0900 2019
```

# まとめ
WSHをTypescriptで書く方法と例を紹介した。おおよそ以下が分かっていれば、VBSのサンプルをそのままTypescriptに置き換えられるようになる。Officeに限らず、ActiveXが提供されたツールで定型作業を自動化する場面で活用してみようと思う。

* プログラムを書く
    * ターゲットをES3に設定してトランスパイルするだけ
    * ファイルを分割して開発し、Webpackで1つにすることができる
    * 型情報は`activex-*`(*: Outlook.Applicationなどのドットで区切られる先頭の文字列)で得られる
    * OLE Programmatic Identifierは型情報の`index.d.ts`の`ActiveXObjectNameMap`で調べられる
* 実行
    * 実行: `cscript <script>`か`wscript <script>`
    * デバッグ: `cscript <script> //x`
* Officeを操作する
    * マニュアルに書いてあることに気を付ける(例えば、メンバにアクセスする前にクラスを調べる)
        * `Class`プロパティが`OlObjectClass`のどれかを調べ、Type Assertionする
    * JavascriptとActiveXObjectの型は、変換が用意されている