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
型はまだ導入されて間もないので、あまり使われていない気がします。
何かいい使い方があれば教えて下さい。