3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

TypeScriptAdvent Calendar 2020

Day 7

assertsでassert関数

Last updated at Posted at 2020-12-06

TypeScript3.7からasserts x is T型が導入されました。

それまでにもあったユーザー定義型ガードと同じようなものですが、assert関数を書くのに便利そうなので、簡単に書いてみました。

TL;DR

assert関数はこんな感じになりました。

function assert(condition: false, message?: string): never;
function assert(
  condition: boolean,
  message?: string
): asserts condition is true;
function assert<T extends [] | {}>(
  condition: T | null | undefined,
  message?: string
): asserts condition is T;
function assert<T>(
  condition: unknown,
  typeguard: (o: unknown) => o is T,
  message?: string
): asserts condition is T;
function assert(condition: unknown, typeguardOrmessage?: string | ((o: unknown) => boolean), message?: string) {
  if (typeof typeguardOrmessage === 'function') {
    condition = typeguardOrmessage(condition);
  } else {
    message = typeguardOrmessage;
  }
  if (!condition) {
    throw new Error(message || 'Assertion failed');
  }
}

使い方はこんな感じ。

function proc(property: {type: 'string' | 'number'; value: string | number}) {
  // 文字列型の場合
  if (property.type === 'string') {
    // **型ガードその1**
    if (typeof property.value === 'string') {
      // never型としてのassert
      assert(false, 'typeがstringなのだからvalueはstring型のはず');
    }
    property.value; // ここでproperty.valueはstring
    //...
    return;
  }
}

function index(node: unknown): number {
  // **型ガードその2**
  assert(node, isNode, 'nodeはINodeのはず');
  if (!node.parent) return 0;
  // **nullチェック**
  assert(node.parent.children, 'node.parent は node を子に持つので、node.parent.children は非 null のはず');
  return node.parent.children.indexOf(node);
}

// INodeはこんな想定
interface INode {
  parent?: INode;
  children?: INode[];
}
declare function isNode(obj: unknown): obj is INode;

typeofなどでの型ガードがtrueになる場合でも使えたり、カンマ演算子でも有効になってくれたら、もっと使い勝手がよくなるのに…

まずは基本

typeofで単純なassert。

関数の返値の型のところにasserts 引数 is 型のように書くことで、その関数から帰ってきたときにその引数が指定した型であると見なされるようになります。

function assertIsString(target: unknown): asserts target is string {
  if (typeof target !== 'string') {
    throw new Error('target is not string');
  }
}

function test1(value: unknown) {
  value; // ここではvalueはunknownと見なされる
  assertIsString(value);
  value; // ここではvalueはstringと見なされる
}

asserts x is string型の関数を呼んだ後でなら、その引数はstring型と見なされます。

実際にその型かどうかは関数の実装次第なのは、ユーザー定義型ガードと一緒です。
まあそこは、関数の実装を信頼する、ということで目をそらします。

参考: TypeScript 3.7のasserts x is T型はどのように危険なのか

でも、これじゃ型ごとに用意しないといけなくて面倒ですね。

Genericで色々使えるassert関数に

ごくごく普通な、asserts x is string型を使ってすらいない、assert関数が
Genericとオーバーロードを使うことで色々と使えるassert関数になります。

function assert(condition: unknown, message?: string) {
  if (!condition) {
    throw new Error(message || 'Assertion failed')
  }
}

nullチェック

以下のようなオーバーロードを用意しておくと

function assert<T extends [] | {}>(condition: T | null | undefined, message?: string): asserts condition is T;

一行追加するだけでnullチェックをインデントを深くすることなしに行えます。

function index(node: INode): number {
  if (!node.parent) return 0;
  assert(node.parent.children);
  return node.parent.children.indexOf(node);
}

できれば三項演算子とカンマ演算子で以下のように書きたいところですが、

function index(node: INode): number {
  return node.parent ? (
    assert(node.parent.children),
    node.parent.children.indexOf(node) // ここでnode.parent.childrenがundefinedの可能性がある、というエラーになる
  ) : 0;
}

カンマ演算子ではasserts x is string型が機能しないようです。

どうしても三項演算子と一緒に使いたいならこんな感じになります。

function index(node: INode): number {
  return node.parent ? (() => {
    assert(node.parent.children);
    return node.parent.children.indexOf(node);
  })() : 0;
}

型ガード

型ガードに使えるか、と思って試してみました。

型ガードでは真偽値を使うので、オーバーロードに以下を追加しておきます。

function assert(condition: boolean, message?: string): asserts condition is true;

assertに型ガードを指定してみます。

function proc(property: {type: 'string' | 'number', value: string | number}) {
  // 文字列型の場合
  if (property.type === 'string') {
    // **型ガード**
    assert(typeof property.value === 'string', 'type が string なのだから value は string 型のはず');
    property.value; // 型ガードは`asserts x is T`型では機能しないらしくここでは string | number
    //...
  }
}

型ガードはasserts x is T型では機能しないようです。

仕方がないのでif文で型ガードしてみます。

function proc(property: {type: 'string' | 'number', value: string | number}) {
  if (property.type === 'string') {
    // 仕方ないのでif文で型ガード
    if (typeof property.value !== 'string') {
      assert(false, 'typeがstringなのだからvalueはstring型のはず');
    }
    property.value; // assert(false)としても`asserts x is T`型のままではまだ string | number
    //...
  }
}

assert関数がasserts x is T型のままではまだ型ガードが有効になりません。

そこで、もう一つオーバーロードを追加します。

function assert(condition: false, message?: string): never;

もうasserts x is T型でも何でもなくなってますが、falseを指定しているなら常にassertに失敗するのでnever型になるわけです。

こうしておくと

function proc(property: {type: 'string' | 'number', value: string | number}) {
  if (property.type === 'string') {
    // 仕方ないのでif文で型ガード
    if (typeof property.value !== 'string') {
      assert(false, 'typeがstringなのだからvalueはstring型のはず');
    }
    property.value; // assert(false)がnever型なので string と見なされる
    //...
  }
}

のように型ガードが有効になります。まあ、当然ですね。

ユーザー定義型ガードとの併用

たいていの場合、ある型のasserts x is T型を用意するとき、同時にユーザー定義型ガードも用意することになると思います。

そんなとき、同じ条件式を二箇所に書くのも面倒なので、Genericで済ませてしまいましょう。

function assertIsType<T>(obj: unknown, typeguard: (obj: unknown) => obj is T, message?: string): asserts obj is T {
  assert(typeguard(obj), message);
}

こういう関数を用意しておくと、ユーザー定義型ガードさえあれば、型ごとにasserts x is T型を用意する必要はなくなります。

interface INode {
  parent?: INode;
  children?: INode[];
}

declare function isNode(obj: unknown): obj is INode;

function proc(obj: unknown) {
  assertIsType(obj, isNode);
  obj; // ここでobjはINode
}

どうせなのでassert関数とまとめてみます。

function assert(condition: false, message?: string): never;
function assert(
  condition: boolean,
  message?: string
): asserts condition is true;
function assert<T extends [] | {}>(
  condition: T | null | undefined,
  message?: string
): asserts condition is T;
function assert<T>(
  condition: unknown,
  typeguard: (o: unknown) => o is T,
  message?: string
): asserts condition is T;
function assert(condition: unknown, typeguardOrmessage?: string | ((o: unknown) => boolean), message?: string) {
  // 第2引数が関数なら型ガードと見なす
  if (typeof typeguardOrmessage === 'function') {
    condition = typeguardOrmessage(condition);
  } else {
    message = typeguardOrmessage;
  }
  if (!condition) {
    throw new Error(message || 'Assertion failed');
  }
}

最後に

asserts x is T型はまだ導入されて間もないので、あまり使われていない気がします。

何かいい使い方があれば教えて下さい。

3
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?