Help us understand the problem. What is going on with this article?

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

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

とはいえVBAは今どきやりたくないし、Powershellは独特だし、JScriptは型チェックが無くて何気に書きにくいし…何かないかと思っていたら、WSHをTypescriptで書くことができるという大分古い記事を見つけた(TypeScriptでWSHを書いちゃおう)。

調べてみると、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


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

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

image.png
ここでは、TypescriptとWebpackでWSHを作るときに必要なこと・気を付けることをまとめる。

TypescriptでWSHを書く

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

必要なファイルを用意する

いつも通りpackage.jsonを作って、必要なパッケージをインストールして、tsconfig.jsonを書いて、tscでトランスパイルすればよい。以下は、既に型情報を追加した状態のpackage.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というエラー出てしまうため注意が必要。
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上で動かないので、あまりうれしくないかもしれない)

Webpackインストール
npm install --save-dev webpack webpack-cli ts-loader
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)が生成される。

自動でトランスパイルする

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

型情報を入手する

Typescriptでは@types/*に型情報が登録されていると、Eclipseなどで入力補完することができるようになる。型情報はTypeSearchで検索することができる。

型情報をインストールした後で.tsファイルを見ると、入力補完が使えるようになる。
image.png

型情報の例

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

型情報のインストール
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.tsActiveXObjectNameMapインターフェースにある。このインターフェースのメンバはOLE Programmatic Identifiers(Outlook)に載っているものと一致している。

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

@types/activex-outlook/index.d.ts
interface ActiveXObjectNameMap {
    'DOCSITE.DocSiteControl': Outlook._DocSiteControl;
    'Outlook.Application': Outlook.Application;
    'Outlook.OlkBusinessCardControl': Outlook.OlkBusinessCardControl;
    ...

activex-msxml2なら以下のようになっている。

@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

実行する

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

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

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 image.png

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

cscript dist\index.js //x

を実行すると、デバッガを起動することができる。
image.png

デバッガの画面ではindex.jsのソースが表示され、ブレークポイントを設定したり、ステップ実行したりできる。
image.png image.png

Officeを操作する

幾つかのお題で試してみる。

Outlookでメッセージを作る

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

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

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

トランスパイルして実行すると、デバッガ上でメッセージを作成しているところが覗ける。
image.png
Outlookを開くと、下書きに以下のようなメッセージができていることが確認できる。
image.png

Outlookで予定の情報を取得する

以下のような予定が1件しかない時に、その情報を出力してみる。
image.png

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

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}` );
}

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

>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

既定では、予定表フォルダーにはAppointmentItem オブジェクトとMeetingItem オブジェクトを含めることができ、連絡先フォルダーにはContactItem オブジェクトとdistlistitem オブジェクトを含めることができます。 通常、フォルダー内のアイテムを列挙する場合は、フォルダー内のアイテムの種類を想定しないでください。アイテムに適用されるプロパティにアクセスする前に、アイテムのメッセージクラスを確認してください。

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

ContactItemを取得する例
If (myItem.Class = olContact) Then 
    MsgBox myItem.FullName & ": " & myItem.LastModificationTime 
End If 

なので、「予定表」から「会議」を取得するなら、以下のようにItemolAppointmentであることを調べた後Type Assertionすることで、以後の処理でメンバを参照する際に入力補完や型チェックができるようになる。

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

型の互換性はあるか?

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

@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;
}

試してみると、差が無いことが分かる。

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.tsActiveXObjectNameMapで調べられる
  • 実行
    • 実行: cscript <script>wscript <script>
    • デバッグ: cscript <script> //x
  • Officeを操作する
    • マニュアルに書いてあることに気を付ける(例えば、メンバにアクセスする前にクラスを調べる)
      • ClassプロパティがOlObjectClassのどれかを調べ、Type Assertionする
    • JavascriptとActiveXObjectの型は、変換が用意されている
Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away