- TypeScript
TypeScript型アサーションasの使い方|!とas unknown as
型アサーション(as)とは、「この値はこの型です」とコンパイラに伝えて、型の解釈を上書きする書き方です。 値そのものは何も変わらず、型チェックの見方だけが変わります。そのため、伝えた内容が実際の値と食い違っていると、コンパイルは通るのに実行時に TypeError で止まることがあります。
この記事では、as・非nullアサーション(!)・as unknown as・as const・satisfies を 「何をするか/コンパイルは通るか/実行時に止まりうるか/代わりの書き方」 の一覧表で比べます。さらに、コンパイルを通ったコードが実行時に止まる例を出力つきで再現し、型ガードや satisfies、オプショナルチェーンへの書き換えをビフォーアフターで並べました。エラー文は TypeScript 7.0.2(strict: true)の tsc、実行結果は Node.js 22.22 と Chromium 141 で実際に得たものです。
型アサーションの早見表
| 書き方 | 何をするか | 実際と違っていてもコンパイルは通るか | 実行時に止まりうるか | 代わりの書き方 |
|---|---|---|---|---|
値 as 型 |
値の型を指定した型として扱う | 通る(型同士に重なりがあれば) | 止まりうる | 型注釈、型ガード、satisfies |
値! |
型から null と undefined を取り除く | 通る | 止まりうる | ?.、??、if での確認 |
値 as unknown as 型 |
重なりのない型へも変換する | 必ず通る | 止まりうる | 型ガード、アサーション関数 |
値 as any |
型チェックの対象から外す | 必ず通る | 止まりうる | unknown で受けて絞り込む |
値 as const |
リテラル型・readonly として固定する | 実際の値と食い違わない | 止まらない | そのまま使ってよい |
値 satisfies 型 |
型に合うかを検査する(型は変えない) | 通らない(エラーで知らせる) | 止まらない | そのまま使ってよい |
プロパティ!: 型 |
クラスのプロパティを「あとで代入する」と伝える | 通る | 止まりうる | 初期値を書く、コンストラクタで代入 |
上の4つと最後の1つは「コンパイラに自分の判断を伝える」書き方で、as const と satisfies は「コンパイラに確かめてもらう」書き方です。 名前に as が付いていても、as const は性質が異なります。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「TypeScript型アサーションasの使い方|!とas unknown as」
検証環境と試し方
mkdir ts-assert && cd ts-assert
npm init -y
npm i typescript
npx tsc -v
Version 7.0.2
tsconfig.json は次のとおりです。型チェックだけ行うときは npx tsc --noEmit --pretty false、実行するときは noEmit を外して JavaScript を出力し、node out/ファイル名.js で動かしました。
{
"compilerOptions": {
"target": "es2022",
"module": "nodenext",
"strict": true,
"noEmit": true,
"lib": ["es2023", "dom"],
"types": []
},
"include": ["*.ts"]
}
asの基本:型の解釈を上書きする
書き方
値のうしろに as 型 を付けます。
type User = { id: number; name: string };
const input = document.querySelector("#keyword") as HTMLInputElement;
const data = JSON.parse('{"id":1,"name":"sato"}') as User;
const method = "GET" as "GET" | "POST";
山かっこを前に置く書き方(<User>値)もあり、.ts ファイルでは as と同じ意味で通りました。ただし JSX を書く .tsx ファイルではタグと見分けがつかないため、as で統一しておくとどのファイルでも同じ書き方ができます。
asは実行時には消える
as は型チェックのためだけの記法です。コンパイル後の JavaScript には何も残りません。 次の TypeScript をコンパイルした結果を見てみます。
const value = 123 as unknown as string; // コンパイルは通る
console.log(typeof value); // number のまま
console.log(value.toUpperCase()); // 実行時に TypeError
出力された JavaScript(一部):
const value = 123; // コンパイルは通る
console.log(typeof value); // number のまま
console.log(value.toUpperCase()); // 実行時に TypeError
as unknown as string がそのまま消えて、const value = 123; になっています。数値を文字列に変換する処理は入りません。 値を変換したいときは String(value) や Number(value) を使います。
| やりたいこと | 書き方 | 実行時の値 |
|---|---|---|
| 型の見方だけ変える | value as string |
変わらない |
| 値を文字列に変換する | String(value) |
文字列になる |
| 値を数値に変換する | Number(value) |
数値になる |
型注釈との違い
同じオブジェクトを、型注釈(変数: 型)と as のそれぞれで書いた結果です。
type User = { id: number; name: string };
const a: User = { id: 1 }; // エラー
const b = { id: 1 } as User; // 通る
const c = { id: 1, name: "sato", extra: true } as User; // 通る
const d: User = { id: 1, name: "sato", extra: true }; // エラー
x2.ts(3,7): error TS2741: Property 'name' is missing in type '{ id: number; }' but required in type 'User'.
x2.ts(6,40): error TS2353: Object literal may only specify known properties, and 'extra' does not exist in type 'User'.
| 比べる点 | 型注釈 const a: User = ... |
型アサーション ... as User |
|---|---|---|
| プロパティが足りない | エラー(TS2741) | 通る |
| 余分なプロパティがある | エラー(TS2353) | 通る |
| コンパイラの役割 | 値が型に合うかを検査する | 書いた人の判断を受け入れる |
| 向いている場面 | 自分で値を書くとき(基本はこちら) | コンパイラが知りえない情報を補うとき |
オブジェクトを自分で書くときは型注釈を使うと、プロパティの書き漏れや書き間違いをその場で知らせてもらえます。
asが通る条件とTS2352
as は何にでも変換できるわけではありません。2つの型のあいだに重なりがないと、TS2352 のエラーになります。
const n = 123;
const s = n as string;
type User = { id: number; name: string };
type Product = { code: string; price: number };
const u: User = { id: 1, name: "sato" };
const p = u as Product;
エラー全文:
e2352.ts(2,11): error TS2352: Conversion of type 'number' to type 'string' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
e2352.ts(7,11): error TS2352: Conversion of type 'User' to type 'Product' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
Type 'User' is missing the following properties from type 'Product': code, price
メッセージは「2つの型が十分に重なっていないので、この変換は誤りかもしれません。意図したものなら、先に unknown へ変換してください」という意味です。プロパティの型が食い違う場合も同じエラーになり、どのプロパティが合わないかが表示されます。
const e = { id: "1", name: "sato" } as User;
x2.ts(7,11): error TS2352: Conversion of type '{ id: string; name: string; }' to type 'User' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
Types of property 'id' are incompatible.
Type 'string' is not comparable to type 'number'.
通った例・通らなかった例
同じ環境で試した結果の一覧です。
| コード | 結果 | 理由 |
|---|---|---|
123 as string |
TS2352 | number と string に重なりがない |
u as Product(User から) |
TS2352 | 共通のプロパティがない |
{ id: "1", name: "sato" } as User |
TS2352 | id の型が食い違う |
{ id: 1 } as User |
通る | 足りないだけなら「広い型から狭い型へ」とみなされる |
{} as HTMLInputElement |
通る | 同上 |
u as { id: number } |
通る | 狭い型から広い型へ |
"GET" as "GET" | "POST" |
通る | リテラルをユニオン型の一員として扱う |
"GET" as "POST" |
通る | どちらも string に含まれるため重なりありと判定された |
"42" as unknown as number |
通る | unknown を挟むと何にでも変換できる |
TS2352 が出たときは、まず「本当に変換したい型は何か」を見直す合図と受け取るのがおすすめです。エラー文の案内どおりに unknown を挟めば通りますが、実際の値は変わらないままです(次の節で確かめます)。
コンパイルは通るのに実行時に止まる例
ここからは、型チェックがエラーなし(tsc の終了コード 0)で、実行すると TypeError になるコードを4つ再現します。
例1:空のオブジェクトにas
type User = { id: number; name: string; profile: { city: string } };
const user = {} as User; // コンパイルは通る
console.log(user.id); // undefined
console.log(user.profile.city); // 実行時に TypeError
実行結果:
undefined
TypeError: Cannot read properties of undefined (reading 'city')
型の上では user.profile は必ずあることになっていますが、実際の値は空のオブジェクトです。
例2:非nullアサーション(!)
type Item = { id: number; name: string };
const items: Item[] = [{ id: 1, name: "ノート" }];
const found = items.find((i) => i.id === 2)!; // 「必ずある」と主張
console.log(found.name); // 実行時に TypeError
実行結果:
TypeError: Cannot read properties of undefined (reading 'name')
find は見つからないと undefined を返します。! を付けると型から undefined が消えますが、出力された JavaScript では ! が消えるだけで、値の確認は行われません。
const found = items.find((i) => i.id === 2); // 「必ずある」と主張
console.log(found.name); // 実行時に TypeError
例3:as unknown as
const value = 123 as unknown as string; // コンパイルは通る
console.log(typeof value); // number のまま
console.log(value.toUpperCase()); // 実行時に TypeError
実行結果:
number
TypeError: value.toUpperCase is not a function
例4:JSON.parseの結果にas
type Config = { port: number; hosts: string[] };
const config = JSON.parse('{"port":"8080"}') as Config; // any から何にでもなる
console.log(config.port + 1); // 数値のつもりが文字列連結
console.log(config.hosts.length); // 実行時に TypeError
実行結果:
80801
TypeError: Cannot read properties of undefined (reading 'length')
1行目の出力に注目してください。エラーにならずに「80801」という想定外の値が出ています。 port が文字列の "8080" だったため、足し算ではなく文字列の連結になりました。TypeError で止まるよりも気づきにくい形です。
4つの例に共通すること
| 例 | コンパイラに伝えたこと | 実際の値 | 起きたこと |
|---|---|---|---|
{} as User |
User の形をしている | 空のオブジェクト | TypeError |
find(...)! |
undefined ではない | undefined | TypeError |
123 as unknown as string |
文字列である | 数値 | TypeError |
JSON.parse(...) as Config |
Config の形をしている | port が文字列、hosts なし | 想定外の値と TypeError |
どれも「伝えた型」と「実際の値」の食い違いが原因です。次の節から、確認を挟む書き方に置き換えていきます。
非nullアサーション(!)の使い方と書き換え
!が必要になる場面
strict(正確には strictNullChecks)が有効だと、null や undefined の可能性がある値をそのまま使うとエラーになります。
type Item = { id: number; name: string };
const items: Item[] = [{ id: 1, name: "ノート" }];
const found = items.find((i) => i.id === 2);
console.log(found.name);
const el = document.querySelector("#name");
el.value;
const input = document.querySelector("input");
input.value;
nobang.ts(4,13): error TS18048: 'found' is possibly 'undefined'.
nobang.ts(6,1): error TS18047: 'el' is possibly 'null'.
nobang.ts(6,4): error TS2339: Property 'value' does not exist on type 'Element'.
nobang.ts(8,1): error TS18047: 'input' is possibly 'null'.
このエラーを消すために ! を付けたくなりますが、エラーは「ここで null かもしれません」という知らせです。次のように確認を挟む書き方に置き換えられます。
ビフォーアフター:!を確認に置き換える
直す前:
const found = items.find((i) => i.id === 2)!;
console.log(found.name);
直したあと(1):オプショナルチェーンと既定値
const found = items.find((i) => i.id === 2);
console.log(found?.name ?? "(該当なし)");
直したあと(2):無いときは理由つきのエラーにする
function getItem(id: number): Item {
const item = items.find((i) => i.id === id);
if (!item) throw new Error(`id=${id} の備品がありません`);
return item; // ここでは Item
}
実行結果:
(該当なし)
id=2 の備品がありません
直したあと(2)も最終的にはエラーを投げますが、「何が見つからなかったのか」がメッセージでわかる点が、Cannot read properties of undefined との違いです。
!を使うほかの場面
! は find 以外でも同じ働きをします。試した3つの例です。
class Store {
name!: string; // 「あとで必ず代入する」と主張
show() { return this.name.toUpperCase(); }
}
try { console.log(new Store().show()); } catch (e) { console.log(String(e)); }
const stock = new Map<string, number>([["ノート", 3]]);
const n = stock.get("ペン")!;
console.log(n + 1);
const env = process.env.API_URL_T236!;
try { console.log(env.startsWith("https")); } catch (e) { console.log(String(e)); }
実行結果(環境変数 API_URL_T236 は未設定):
TypeError: Cannot read properties of undefined (reading 'toUpperCase')
NaN
TypeError: Cannot read properties of undefined (reading 'startsWith')
Map の例は TypeError にならず NaN になりました。undefined + 1 の計算結果です。
| 場面 | !を使った書き方 | 確認を挟む書き方 |
|---|---|---|
| 配列の find | items.find(f)! |
?. と ??、または if で確認 |
| Map の get | map.get(key)! |
map.get(key) ?? 0 |
| 環境変数 | process.env.X! |
起動時にまとめて確認し、無ければエラーにする |
| DOM の取得 | document.querySelector(s)! |
if または instanceof で確認 |
| クラスのプロパティ | name!: string |
初期値を書く、コンストラクタで代入する |
クラスのプロパティに ! を付けない場合は、次のエラーで初期化の漏れを知らせてくれます。
x2.ts(1,16): error TS2564: Property 'name' has no initializer and is not definitely assigned in the constructor.
!を使ってよい場面の目安
直前の行で存在を確かめていて、コンパイラがそれを読み取れないだけのときは、! が合っています。たとえば、テストコードで事前に用意したデータを取り出すときや、フレームワークが初期化を保証しているプロパティなどです。その場合は、なぜ必ずあると言えるのかを1行コメントで残しておくと、あとから読む人に伝わります。
as unknown asの使い方と注意点
as unknown as 型 は、unknown を経由することで、重なりのない型同士でも変換を通す書き方です。unknown はすべての型を受け取れ、unknown からはどの型へも as で変換できるため、2段階にするとあらゆる変換が通ります。
const n = 123;
const s1 = n as string; // TS2352
const s2 = n as unknown as string; // 通る
通るようになる代わりに、TS2352 が確かめていた「重なりがあるか」という最後の検査もなくなります。 先ほどの例3のとおり、実際の値が違えば実行時に止まります。
as unknown asとas anyの違い
| 比べる点 | as unknown as 型 |
as any |
|---|---|---|
| 変換後の型 | 指定した型 | any |
| その後の型チェック | 指定した型として続く | 以降のアクセスがすべて検査されなくなる |
| 入力補完 | 効く | 効かない |
| 検索での見つけやすさ | 文字列が長く見つけやすい | 見つけやすい |
as any で受けた値は、存在しないプロパティを読んでもコンパイルが通りました。
const anyUser = { id: 1 } as any;
console.log(anyUser.profile.city);
TypeError: Cannot read properties of undefined (reading 'city')
ビフォーアフター:as unknown asをアサーション関数に置き換える
外から来た値を特定の型として扱いたいときは、実際に中身を調べる関数を用意します。
直す前:
type Config = { port: number; hosts: string[] };
const config = JSON.parse(text) as unknown as Config;
直したあと:
type Config = { port: number; hosts: string[] };
// ユーザー定義型ガード:true を返したら v は Config
function isConfig(v: unknown): v is Config {
return (
typeof v === "object" && v !== null &&
"port" in v && typeof v.port === "number" &&
"hosts" in v && Array.isArray(v.hosts) && v.hosts.every((h) => typeof h === "string")
);
}
// アサーション関数:例外を投げなければ、以降 v は Config
function assertIsConfig(v: unknown): asserts v is Config {
if (!isConfig(v)) throw new Error("Config ではありません");
}
const raw: unknown = JSON.parse('{"port":"8080"}');
if (isConfig(raw)) {
console.log(raw.port + 1);
} else {
console.log("設定の形が想定と違います:", JSON.stringify(raw));
}
const raw2: unknown = JSON.parse('{"port":8080,"hosts":["a.example","b.example"]}');
assertIsConfig(raw2);
console.log(raw2.port + 1, raw2.hosts.length);
実行結果:
設定の形が想定と違います: {"port":"8080"}
8081 2
例4では「80801」と表示された入力が、型ガードを通すと「形が想定と違う」と判定されています。 正しい形のデータでは、8081 と正しく足し算されました。
as unknown asが合う場面
テストで一部のプロパティだけを持つ代用のオブジェクトを渡すときや、型定義が実際の値と合っていないライブラリを一時的に扱うときには、as unknown as が使われます。使うときは、1か所の関数に閉じ込めて、理由をコメントに書くと、あとから見直しやすくなります。
as constとsatisfies:コンパイラに確かめてもらう書き方
as const と satisfies は、実際の値と食い違う型を作らないため、実行時に止まる原因になりません。この2つは別の記事で詳しく扱っているので、ここでは as との違いに絞ります。
as const:値をそのままの型で固定する
const SIZES = ["S", "M", "L"] as const;
type Size = (typeof SIZES)[number]; // "S" | "M" | "L"
SIZES.push("XL");
const conf = { mode: "dark" } as const;
conf.mode = "light";
sat.ts(9,7): error TS2339: Property 'push' does not exist on type 'readonly ["S", "M", "L"]'.
sat.ts(11,6): error TS2540: Cannot assign to 'mode' because it is a read-only property.
as const は「この値は書いたとおりの内容で、変更しません」という宣言です。実際の値より都合のよい型を主張するわけではないので、as とは性質が違います。
リテラル型が必要な引数に値を渡すときにも役立ちます。
function send(method: "GET" | "POST") {}
const req = { method: "GET" };
send(req.method); // エラー(string に広がっている)
const req2 = { method: "GET" } as const;
send(req2.method); // OK
sat.ts(16,6): error TS2345: Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.
satisfies:型に合うかを検査し、型は変えない
同じ書き間違い(path を pat と書いた)を、as と satisfies で比べます。
type Routes = Record<string, { path: string; auth?: boolean }>;
const b = { home: { path: "/" }, admin: { pat: "/admin" } } satisfies Routes;
sat.ts(3,43): error TS2561: Object literal may only specify known properties, but 'pat' does not exist in type '{ path: string; auth?: boolean | undefined; }'. Did you mean to write 'path'?
satisfies は書き間違いを見つけて、正しい名前の候補まで示してくれました。同じ内容に as Routes を付けた場合も、この例では TS2352 で知らせてくれました。ただし as は、先ほどの { id: 1 } as User のように、プロパティが足りないだけの場合は通ります。 satisfies なら、その場合もエラーになります。
type User = { id: number; name: string };
const b = { id: 1 } as User; // 通る
const f = { id: 1 } satisfies User; // エラー
x2.ts(8,21): error TS2741: Property 'name' is missing in type '{ id: number; }' but required in type 'User'.
satisfies のもう1つの特徴は、検査だけを行い、変数の型は書いた値から推論されたままになることです。
const routes = {
home: { path: "/" },
admin: { path: "/admin", auth: true },
} satisfies Routes;
console.log(routes.admin.path); // /admin(キー名が補完される)
routes.nothing; // エラー(存在しないキー)
y.ts(8,8): error TS2339: Property 'nothing' does not exist on type '{ home: { path: string; }; admin: { path: string; auth: true; }; }'.
| 書き方 | 書き間違いの検査 | 変数の型 |
|---|---|---|
const x: Routes = {...} |
される | Routes(キーは string 全般) |
const x = {...} as Routes |
一部だけ(足りないプロパティは通る) | Routes |
const x = {...} satisfies Routes |
される | 書いた値のまま(キー名が残る) |
場面別の書き方:DOM取得・JSON.parse・fetch
DOM取得
querySelector の戻り値は Element | null です。入力欄の value を読むには、null の可能性と、要素の種類の両方を解決する必要があります。4つの書き方を、#keyword の入力欄が存在しないページで動かして比べました(#title は h1 要素です)。
// (A) as で決め打ち
function readA(): string {
const input = document.querySelector("#keyword") as HTMLInputElement;
return input.value;
}
// (B) ! で決め打ち
function readB(): string {
const input = document.querySelector<HTMLInputElement>("#keyword")!;
return input.value;
}
// (C) instanceof で確かめる
function readC(): string {
const input = document.querySelector("#keyword");
if (!(input instanceof HTMLInputElement)) {
return "(入力欄が見つかりません)";
}
return input.value; // ここでは HTMLInputElement
}
// (D) as で要素の種類を取り違える
function readD(): string {
const el = document.querySelector("#title") as HTMLInputElement; // 本当は h1
return el.value.trim();
}
実行結果(Chromium 141。型チェックは4つともエラーなし):
A => TypeError: Cannot read properties of null (reading 'value')
B => TypeError: Cannot read properties of null (reading 'value')
C => (入力欄が見つかりません)
D => TypeError: Cannot read properties of undefined (reading 'trim')
instanceof は「null ではないこと」と「入力欄であること」を一度に確かめられるので、DOM 取得と相性のよい書き方です。D は要素が存在していても、h1 に value が無いために止まりました。
| 書き方 | null のとき | 別の種類の要素のとき | 向いている場面 |
|---|---|---|---|
as HTMLInputElement |
実行時に止まる | 実行時に止まりうる | 要素の存在と種類が確実なとき |
querySelector<型>(s)! |
実行時に止まる | 実行時に止まりうる | 同上 |
| instanceof で確認 | 分岐で扱える | 分岐で扱える | 基本の書き方 |
タグ名のセレクタ("input") |
TS18047 で知らせる | 型が自動で決まる | タグ名だけで取れるとき |
タグ名だけのセレクタを渡すと、戻り値の型が自動で HTMLInputElement | null になります。先ほどのエラー一覧で、document.querySelector("input") には TS2339(value が無い)が出ていないのはこのためです。
イベントの対象を扱うときも同じ考え方が使えます。
document.addEventListener("click", (e) => {
// 直す前:const button = e.target as HTMLButtonElement;
if (e.target instanceof HTMLButtonElement) {
console.log(e.target.name);
}
});
JSON.parse
JSON.parse の戻り値は any なので、as を付けなくても型注釈だけでどの型にも代入できます。いったん unknown で受けると、確認なしでは使えなくなり、確認の書き忘れを防げます。
// 直す前
const config = JSON.parse(text) as Config;
// 直したあと
const raw: unknown = JSON.parse(text);
if (!isConfig(raw)) {
throw new Error(`設定の形が想定と違います: ${JSON.stringify(raw)}`);
}
const config = raw; // ここでは Config
isConfig は、前の節で作ったユーザー定義型ガードです。項目が多い型では、検証用のライブラリ(スキーマから型と検証処理の両方を作るもの)を使う方法もあります。本記事では標準機能だけで確認しました(ライブラリを使う形は未実行)。
fetchレスポンス
res.json() の戻り値も any です。ローカルに立てたサーバーに対して、as で受ける書き方と、確認を挟む書き方を比べました。サーバーは次の3通りを返します。
| URL | 応答 |
|---|---|
| /users/1 | 200、{"id":1,"name":"sato","tags":["a","b"]} |
| /users/2 | 200、{"id":2,"name":"suzuki"}(tags なし) |
| /users/9 | 404、{"message":"Not Found"} |
type User = { id: number; name: string; tags: string[] };
// (A) as で決め打ち
async function getUserA(id: number): Promise<User> {
const res = await fetch(`http://127.0.0.1:8360/users/${id}`);
return (await res.json()) as User;
}
// (B) ステータス確認 + 型ガード
function isUser(v: unknown): v is User {
return (
typeof v === "object" && v !== null &&
"id" in v && typeof v.id === "number" &&
"name" in v && typeof v.name === "string" &&
"tags" in v && Array.isArray(v.tags) && v.tags.every((t) => typeof t === "string")
);
}
async function getUserB(id: number): Promise<User> {
const res = await fetch(`http://127.0.0.1:8360/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data: unknown = await res.json();
if (!isUser(data)) throw new Error(`User の形ではありません: ${JSON.stringify(data)}`);
return data;
}
for (const fn of [getUserA, getUserB]) {
for (const id of [1, 2, 9]) {
try {
const u = await fn(id);
console.log(fn.name, id, "=>", u.name, u.tags.join("/"));
} catch (e) {
console.log(fn.name, id, "=>", String(e));
}
}
}
実行結果(Node.js 22.22):
getUserA 1 => sato a/b
getUserA 2 => TypeError: Cannot read properties of undefined (reading 'join')
getUserA 9 => TypeError: Cannot read properties of undefined (reading 'join')
getUserB 1 => sato a/b
getUserB 2 => Error: User の形ではありません: {"id":2,"name":"suzuki"}
getUserB 9 => Error: HTTP 404
A は、tags が無い応答でも 404 の応答でも、同じ Cannot read properties of undefined になりました。B は「形が違う」「HTTP 404」と原因ごとに別のメッセージになり、どこを調べればよいかがすぐわかります。エラーが起きる場所も、A はデータを使う側(join を呼んだ行)、B は受け取った直後です。
場面別のまとめ
| 場面 | 手早い書き方 | 確認を挟む書き方 |
|---|---|---|
| DOM 取得 | as HTMLInputElement |
instanceof で確認 |
| イベントの対象 | e.target as HTMLButtonElement |
e.target instanceof HTMLButtonElement |
| JSON.parse | as Config |
unknown で受けて型ガード |
| fetch レスポンス | (await res.json()) as User |
res.ok の確認と型ガード |
| 配列の検索 | find(...)! |
?. と ??、または if |
| 設定オブジェクトの定義 | {...} as Routes |
{...} satisfies Routes |
| catch した値 | (e as Error).message |
e instanceof Error で確認 |
書き換えの判断フロー
as や ! を書きたくなったときに、順番に確かめる項目です。
| 順番 | 確かめること | 当てはまるときの書き方 |
|---|---|---|
| 1 | 自分で値を書いているか | 型注釈、または satisfies |
| 2 | リテラル型のまま固定したいだけか | as const |
| 3 | null や undefined を除きたいだけか | ?.、??、if |
| 4 | 実行時に種類を調べられるか | typeof、instanceof、in |
| 5 | 外から来たデータか | unknown で受けて、ユーザー定義型ガードかアサーション関数 |
| 6 | 上のどれでも表せない、コンパイラが知りえない事実があるか | as または !(理由をコメントで残す) |
as が合うのは6番目、つまり「値は確かにその型で、コンパイラにはそれを知る方法がない」ときです。そのときも、使う場所を小さな関数にまとめておくと、あとで型ガードに差し替えやすくなります。
よくある質問
Q. 型アサーションは、ほかの言語のキャストと同じですか?
A. 実行時の値を変換しない点が違います。as は型チェックの見方を変えるだけで、コンパイル後の JavaScript には残りません。今回の検証でも、123 as unknown as string は const value = 123; として出力され、typeof の結果は number のままでした。値を変換するときは String() や Number() を使います。
Q. TS2352 が出たら、as unknown as を付ければよいですか? A. 付ければコンパイルは通りますが、値は変わらないので、実際の型が違えば実行時に止まります。まずは変換先の型が合っているかを見直し、外部から来たデータなら型ガードで中身を確かめる形にするのがおすすめです。
Q. 非nullアサーション(!)とオプショナルチェーン(?.)はどう違いますか?
A. ! は型から null と undefined を取り除くだけで、実行時には何もしません。?. は実行時に null か undefined かを調べ、そうであれば undefined を返して先へ進みます。値が無い可能性が少しでもあるなら ?. と ?? の組み合わせが向いています。
Q. as const も避けたほうがよいですか? A. as const は避ける必要はありません。値をリテラル型・readonly として固定する宣言で、実際の値と食い違う型を作らないためです。名前が似ていますが、型アサーションの as とは分けて考えて構いません。
まとめ
- as は型の見方を上書きするだけで、値は変換されない。コンパイル後の JavaScript からは消える
- as が通るのは型同士に重なりがあるとき。重なりがないと TS2352 になり、unknown を挟むと通るようになる
- 非nullアサーション(!)は null と undefined を型から取り除く。実行時の確認は行われないので、
?.・??・if に置き換えられないかを先に考える - as unknown as は最後の検査も外す書き方。外から来たデータは unknown で受けて、型ガードやアサーション関数で中身を確かめる
- as const と satisfies は、コンパイラに確かめてもらう書き方で、実行時に止まる原因にならない
- DOM 取得は instanceof、JSON.parse と fetch は型ガードにすると、原因のわかるエラーメッセージにできる
- as を使うのは、コンパイラが知りえない事実を補うとき。理由をコメントで残し、小さな関数にまとめる
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「TypeScript型アサーションasの使い方|!とas unknown as」
株式会社RJCでは、TypeScriptの型アサーションと型ガードの使い分けを仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。