- TypeScript
TypeScriptの!と?と??の違い|undefinedエラー対処
TypeScriptの「!」は「この値は null でも undefined でもない」とコンパイラーに伝える記法、「?」は「無いかもしれない」を表す記法、「??」は「無かったときの既定値」を決める演算子です。 「!」はコンパイル後に消えて実行時には何もしませんが、「?.」と「??」は JavaScript の機能としてそのまま残り、実行時に null と undefined を調べます。
この記事では、?(省略可能)・?.・??・??=・!・明確な代入アサーションの6つを1枚の比較表にまとめ、それぞれのコンパイル後の JavaScript と実行結果を並べます。さらに、Object is possibly 'undefined'(TS2532 / TS18048 / TS2533)を TypeScript 7.0.2 で再現し、型ガード → 早期 return → ?? での既定値 → ! の順に、コピペできる直し方を示します。|| と ?? で 0 や空文字の結果が変わる例も、実行結果つきで確認します。
!・?・?.・??・??= の比較早見表
| 記法 | 名前 | 書く場所 | 意味 | コンパイル後(ES2022) | 値が無いときの実行結果 |
|---|---|---|---|---|---|
name?: string |
省略可能なプロパティ・引数 | 型定義、引数 | 無くてもよい。型に undefined が加わる | 消える(引数名だけ残る) | undefined が入る |
a?.b |
オプショナルチェーン | 値の後ろ | a が null か undefined なら、そこで止めて undefined を返す | a?.b のまま |
undefined(エラーにならない) |
a ?? b |
Null合体演算子 | 2つの値の間 | a が null か undefined のときだけ b を使う | a ?? b のまま |
b |
a ??= b |
Null合体代入 | 代入 | a が null か undefined のときだけ b を代入する | a ??= b のまま |
a に b が入る |
a! |
非nullアサーション | 値の後ろ | 「null でも undefined でもない」とコンパイラーに伝える | ! が消えて a だけ残る |
そのまま進み、TypeError になることがある |
x!: number |
明確な代入アサーション | プロパティ・変数の宣言 | 「あとで必ず代入する」とコンパイラーに伝える | ! が消える |
代入前に読むと undefined |
一言でまとめると、?. と ?? は「実行時に調べる」記法、! は「調べずに通す」記法です。エラーを直すときは、調べる記法を先に検討します。
| 出ているエラー | まず試す直し方 |
|---|---|
| TS2532 / TS18048 / TS2533(possibly 'undefined') | if での型ガード、早期 return |
| 値が無いときの代わりの値が決まっている | ?. と ?? で既定値 |
| 直前の処理で値があると確かめてある | !(理由をコメントに残す) |
| TS2564(has no initializer) | 初期値を書く、コンストラクターで代入する、? を付ける。それでも合わないときに ! |
検証環境と、登場するエラーコード
npm i typescript # 2026-10-03 時点の latest は 7.0.2
npx tsc -v # Version 7.0.2
npx tsc --noEmit --strict --target ES2022 --pretty false ファイル名.ts
node -v # v22.22.2(コンパイル後の JavaScript を実行)
これらのエラーは strict(その中の strictNullChecks)が有効なときに出ます。TypeScript 7.0.2 の tsc --init で作られる tsconfig.json にも "strict": true が入っていました。
| コード | エラー文 | 出る場面 |
|---|---|---|
| TS2532 | Object is possibly 'undefined'. | 式の結果(関数の戻り値、配列の要素など)が undefined かもしれない |
| TS18048 | 'found' is possibly 'undefined'. | 変数やプロパティが undefined かもしれない(名前つきで表示) |
| TS2533 | Object is possibly 'null' or 'undefined'. | 式の結果が null か undefined かもしれない |
| TS18047 | 'el' is possibly 'null'. | 変数が null かもしれない(getElementById など) |
| TS18049 | 'maybe' is possibly 'null' or 'undefined'. | 変数が null か undefined かもしれない |
| TS2322 | Type 'string | undefined' is not assignable to type 'string'. | undefined を含む値を、含まない型の変数に入れた |
| TS2345 | Argument of type 'string | undefined' is not assignable to parameter of type 'string'. | undefined を含む値を、含まない型の引数に渡した |
| TS2564 | Property 'count' has no initializer and is not definitely assigned in the constructor. | クラスのプロパティに初期値が無い |
| TS2454 | Variable 't2' is used before being assigned. | 変数を代入の前に使った |
| TS5076 | '||' and '??' operations cannot be mixed without parentheses. | OR演算子と ?? をかっこなしで混ぜた |
TS2532 と TS18048 は同じ内容で、対象に名前があるかどうかで表示が変わります。変数なら TS18048(名前つき)、式の結果なら TS2532(Object is ...)です。直し方は共通です。
Object is possibly 'undefined' を再現する
まず、エラーが出るコードをまとめて確認します。
// err.ts
type User = { name: string; nickname?: string; address?: { city: string } };
const users: User[] = [{ name: "Sato" }];
// TS2532: Object is possibly 'undefined'.
const found = users.find((u) => u.name === "Kato");
console.log(users.find((u) => u.name === "Kato").name);
// TS18048: 'xxx' is possibly 'undefined'.
console.log(found.name);
function len(s?: string) { return s.length; }
const u0 = users[0];
console.log(u0.address.city);
// TS2533: Object is possibly 'null' or 'undefined'.
declare function load(): User | null | undefined;
console.log(load().name);
// TS18047 / TS18049
const el = document.getElementById("app");
el.textContent = "hi";
declare const maybe: User | null | undefined;
console.log(maybe.name);
// TS2322 / TS2345
const nick: string = u0.nickname;
function greet(n: string) { return "Hi " + n; }
greet(u0.nickname);
// TS2564
class Counter {
count: number;
label!: string;
}
実行結果(npx tsc --noEmit --strict --target ES2022 --lib ES2022,DOM --pretty false err.ts):
err.ts(6,13): error TS2532: Object is possibly 'undefined'.
err.ts(9,13): error TS18048: 'found' is possibly 'undefined'.
err.ts(10,35): error TS18048: 's' is possibly 'undefined'.
err.ts(12,13): error TS18048: 'u0.address' is possibly 'undefined'.
err.ts(16,13): error TS2533: Object is possibly 'null' or 'undefined'.
err.ts(20,1): error TS18047: 'el' is possibly 'null'.
err.ts(22,13): error TS18049: 'maybe' is possibly 'null' or 'undefined'.
err.ts(25,7): error TS2322: Type 'string | undefined' is not assignable to type 'string'.
Type 'undefined' is not assignable to type 'string'.
err.ts(27,7): error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'.
Type 'undefined' is not assignable to type 'string'.
err.ts(31,3): error TS2564: Property 'count' has no initializer and is not definitely assigned in the constructor.
undefined が紛れ込む入り口は、主に次の5つです。
| 入り口 | 例 | 型 |
|---|---|---|
| 見つからないことがあるメソッド | array.find()、map.get() |
T | undefined |
| 省略可能なプロパティ | nickname?: string |
string | undefined |
| 省略可能な引数 | function len(s?: string) |
string | undefined |
| DOM の取得 | document.getElementById() |
HTMLElement | null |
| 自分で null や undefined を返す関数 | load(): User | null | undefined |
宣言どおり |
strict を外して tsc --strict false でコンパイルすると、users.find(...).name のようなコードもエラーになりませんでした。エラーが消えるだけで、実行時に値が無ければ TypeError になる点は変わりません。エラー文は「ここで値が無い場合の処理を決めてください」という案内だと捉えると、直し方を選びやすくなります。
直し方を4つの順で試す
同じ「利用者の住所の市区名を返す」処理を、4通り+1で書き比べます。データは次の3パターンです。
- Kato:住所あり
- Sato:住所なし
- Abe:利用者自体が見つからない
// fix.ts
type User = { name: string; nickname?: string; address?: { city: string } };
const users: User[] = [{ name: "Sato", nickname: "" }, { name: "Kato", address: { city: "Osaka" } }];
const find = (n: string) => users.find((u) => u.name === n);
// 1) 型ガード(if で絞り込む)
function cityOf1(name: string): string {
const u = find(name);
if (u !== undefined && u.address !== undefined) {
return u.address.city;
}
return "(未登録)";
}
// 2) 早期 return
function cityOf2(name: string): string {
const u = find(name);
if (!u) return "(該当なし)";
if (!u.address) return "(未登録)";
return u.address.city;
}
// 3) ?. と ?? で既定値
function cityOf3(name: string): string {
return find(name)?.address?.city ?? "(未登録)";
}
// 4) ! (値があると言い切れる場合だけ)
function cityOf4(name: string): string {
return find(name)!.address!.city;
}
// 5) 自作の型ガード関数 / アサーション関数
function isDefined<T>(v: T | null | undefined): v is T { return v !== null && v !== undefined; }
function assertDefined<T>(v: T | null | undefined, msg = "値がありません"): asserts v is T {
if (v === null || v === undefined) throw new Error(msg);
}
function cityOf5(name: string): string {
const u = find(name);
assertDefined(u, `${name} が見つかりません`);
return u.address?.city ?? "(未登録)";
}
for (const f of [cityOf1, cityOf2, cityOf3, cityOf4, cityOf5]) {
for (const n of ["Kato", "Sato", "Abe"]) {
try { console.log(f.name, n, "->", f(n)); }
catch (e) { console.log(f.name, n, "->", (e as Error).name + ": " + (e as Error).message); }
}
}
const cities: string[] = users.map((u) => u.address?.city).filter(isDefined);
console.log(cities);
tsc --noEmit --strict はエラーなしで通りました。実行結果(Node.js 22.22.2):
cityOf1 Kato -> Osaka
cityOf1 Sato -> (未登録)
cityOf1 Abe -> (未登録)
cityOf2 Kato -> Osaka
cityOf2 Sato -> (未登録)
cityOf2 Abe -> (該当なし)
cityOf3 Kato -> Osaka
cityOf3 Sato -> (未登録)
cityOf3 Abe -> (未登録)
cityOf4 Kato -> Osaka
cityOf4 Sato -> TypeError: Cannot read properties of undefined (reading 'city')
cityOf4 Abe -> TypeError: Cannot read properties of undefined (reading 'address')
cityOf5 Kato -> Osaka
cityOf5 Sato -> (未登録)
cityOf5 Abe -> Error: Abe が見つかりません
[ 'Osaka' ]
5つともコンパイルは通りますが、値が無いときの動きが違います。
| 直し方 | 値が無いとき | 向いている場面 |
|---|---|---|
| 1. 型ガード(if) | else 側の処理へ進む | 値がある場合と無い場合で処理を分けたい |
| 2. 早期 return | その場で関数を抜ける。理由ごとに結果を変えられる | 条件が複数あり、ネストを浅く保ちたい |
3. ?. と ?? |
既定値になる | 「無ければこの値」と1行で決められる |
4. ! |
そのまま進み、実行時に TypeError | 直前の処理で値があると確かめてある |
| 5. アサーション関数 | 自分で決めたメッセージのエラーになる | 値が無いのは想定外で、原因を分かりやすく伝えたい |
1. 型ガード:if で絞り込む
if (u !== undefined) のように調べると、その if の中では u の型から undefined が外れます。これを型の絞り込みと呼びます。
| 書き方 | 外れるもの | 補足 |
|---|---|---|
if (u !== undefined) |
undefined | もっとも明示的 |
if (u != null) |
null と undefined の両方 | !=(イコール1つ)は null と undefined をまとめて調べられる |
if (u) |
null、undefined に加えて 0、空文字、false、NaN | 数値や文字列では、0 や空文字も外れる点に注意 |
if (typeof v === "string") |
string 以外 | 型ごとに処理を分けたいとき |
2. 早期 return:ネストを増やさずに絞り込む
「値が無ければ先に抜ける」と書くと、それ以降の行ではずっと絞り込まれた型になります。cityOf2 では、見つからない場合と住所が無い場合で別の結果を返せている点が、3 の書き方との違いです。
3. ?. と ?? で既定値を決める
find(name)?.address?.city ?? "(未登録)" は、途中のどこかが undefined なら全体が undefined になり、?? で既定値に置き換わります。1行で書ける反面、「見つからない」と「住所が無い」が同じ結果にまとまります(Abe も Sato も「(未登録)」)。区別が必要なら 2 の書き方が合います。
4. ! は「調べずに通す」
cityOf4 はコンパイルを通りましたが、Sato と Abe で TypeError になりました。! はエラー表示を消すだけで、値を用意してくれるわけではありません。 使ってよい場面は後の章で整理します。
5. アサーション関数で「ここから先は値がある」と示す
asserts v is T という戻り値の型を持つ関数は、例外が出なければ、それ以降の行で型が絞り込まれます。 ! と同じく1行で書けて、値が無いときは自分で決めたメッセージで止まります。isDefined のような型ガード関数は filter と組み合わせると便利で、(string | undefined)[] から string[] を取り出せました。
記法ごとの詳しい動きとコンパイル結果
6つの記法をまとめて使ったコードを、2つのターゲットでコンパイルします。
// ops.ts
type Opt = { title?: string; size?: number; meta?: { tags?: string[] }; run?: () => string };
function demo(o: Opt | undefined, cache: { v?: number }) {
const a = o?.title;
const b = o?.meta?.tags?.[0];
const c = o?.run?.();
const d = o?.size ?? 10;
cache.v ??= 100;
const e = o!.title!.length;
return [a, b, c, d, cache.v, e];
}
class Box {
value!: number;
init() { this.value = 1; }
}
tsc --strict --target ES2022 の出力:
"use strict";
function demo(o, cache) {
const a = o?.title;
const b = o?.meta?.tags?.[0];
const c = o?.run?.();
const d = o?.size ?? 10;
cache.v ??= 100;
const e = o.title.length;
return [a, b, c, d, cache.v, e];
}
class Box {
value;
init() { this.value = 1; }
}
tsc --strict --target ES2019 の出力:
"use strict";
function demo(o, cache) {
var _a, _b, _c;
var _d, _e;
const a = o === null || o === void 0 ? void 0 : o.title;
const b = (_b = (_a = o === null || o === void 0 ? void 0 : o.meta) === null || _a === void 0 ? void 0 : _a.tags) === null || _b === void 0 ? void 0 : _b[0];
const c = (_c = o === null || o === void 0 ? void 0 : o.run) === null || _c === void 0 ? void 0 : _c.call(o);
const d = (_d = o === null || o === void 0 ? void 0 : o.size) !== null && _d !== void 0 ? _d : 10;
(_e = cache.v) !== null && _e !== void 0 ? _e : (cache.v = 100);
const e = o.title.length;
return [a, b, c, d, cache.v, e];
}
class Box {
init() { this.value = 1; }
}
2つの出力から読み取れることを表にします。
| 記法 | ES2022 の出力 | ES2019 の出力 | 分かること |
|---|---|---|---|
o?.title |
そのまま | o === null || o === void 0 ? void 0 : o.title |
null と undefined の2つだけを調べている |
o?.size ?? 10 |
そのまま | !== null && !== void 0 ? 値 : 10 |
?? も null と undefined の2つだけを調べる |
cache.v ??= 100 |
そのまま | 値が無いときだけ cache.v = 100 を実行 |
値があれば代入自体を行わない |
o!.title!.length |
o.title.length |
o.title.length |
! はどちらでも跡形なく消える |
value!: number |
value; |
何も残らない | 型の情報なので消える |
title?: string |
何も残らない | 何も残らない | 型の情報なので消える |
?. と ?? は ES2020、??= は ES2021 で JavaScript に加わった構文です。ターゲットをそれより前にすると、TypeScript が同じ意味の条件式に書き換えます。void 0 は undefined のことです。
?(省略可能なプロパティ・引数)
型定義や引数の名前の後ろに ? を付けると、「無くてもよい」という意味になり、型に undefined が加わります。
// opt.ts(前半)
type Member = { name: string; nickname?: string };
function label(m: Member, suffix?: string, sep = "/") {
return m.name + sep + (m.nickname ?? "-") + (suffix ?? "");
}
console.log(label({ name: "Sato" }));
console.log(label({ name: "Kato", nickname: "kt" }, "!", ":"));
const m1: Member = { name: "A", nickname: undefined };
console.log("nickname" in m1, "nickname" in { name: "B" });
コンパイル後(ES2022)は function label(m, suffix, sep = "/") となり、? と型が消えました。実行結果:
Sato/-
Kato:kt!
true false
| 書き方 | 省略 | undefined を渡す | 関数の中での型 |
|---|---|---|---|
suffix?: string |
できる | できる | string | undefined |
suffix: string | undefined |
省略すると TS2554(Expected 1 arguments, but got 0.) | できる | string | undefined |
sep = "/"(既定値つき引数) |
できる | できる(既定値が使われる) | string(undefined が入らない) |
既定値が決まっている引数は、? より既定値つき引数にすると、関数の中で undefined を考えなくて済みます。
3行目の出力 true false は、「プロパティに undefined を入れた」オブジェクトと「プロパティ自体が無い」オブジェクトが、実行時には別物であることを示しています。この2つを型でも区別したいときは、コンパイラーオプション exactOptionalPropertyTypes を有効にします。有効にすると、{ name: "A", nickname: undefined } は次のエラーになりました。
opt2.ts(2,7): error TS2375: Type '{ name: string; nickname: undefined; }' is not assignable to type 'Member' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.
?.(オプショナルチェーン)
?. は、左側が null か undefined なら、そこで評価を打ち切って undefined を返します。 プロパティ、添字、関数呼び出しの3つの形があります。
| 形 | 書き方 | 左側が無いとき |
|---|---|---|
| プロパティ | o?.a |
undefined |
| 添字 | o?.a?.b?.[0] |
undefined |
| 関数呼び出し | o?.f?.() |
undefined(関数は呼ばれない) |
打ち切られると、右側の式は評価されません。次のコードで、引数の next() が呼ばれないことを確かめます。
// opt.ts(後半)
let calls = 0;
const next = () => ++calls;
const none: { f?: (n: number) => number } | undefined = undefined as any;
const r1 = none?.f?.(next());
console.log(r1, calls);
undefined 0
calls は 0 のままでした。なお、?. は読み取り専用で、代入の左辺には使えません。
narrow.ts(34,1): error TS2779: The left-hand side of an assignment expression may not be an optional property access.
o.a?.b = 1 と書きたい場面では、if (o.a) o.a.b = 1; と書きます。
??(Null合体演算子)と ??=
a ?? b は、a が null か undefined のときだけ b を返します。??= はその代入版です。
const cache: { v?: number; w?: number | null; z?: number } = { w: null, z: 0 };
cache.v ??= 100; cache.w ??= 200; cache.z ??= 300;
show("??= の結果", cache); // show はラベルと JSON 形式の値を表示する自作関数
const z2 = { z: 0 }; z2.z ||= 300;
show("||= の結果", z2);
実行結果:
??= の結果 {"w":200,"z":0,"v":100}
||= の結果 {"z":300}
??= では、undefined だった v と null だった w にだけ値が入り、0 だった z はそのままです。||= では 0 が 300 に置き換わりました。
また、左側が null にも undefined にもならないと型から分かる場合、TypeScript 7.0.2 では次のエラーで知らせてくれました(0 ?? next() と書いた場合)。
opt.ts(16,11): error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
!(非nullアサーション)
値の後ろに ! を付けると、型から null と undefined が外れます。コンパイル後は消えるので、実行時の動作は ! が無いときとまったく同じです。
const o: { a?: { b?: number[] } } | undefined = undefined as any;
try { const n = (o as any)!.a; show("o!.a", n); }
catch (e) { console.log("o!.a".padEnd(28), (e as Error).name + ": " + (e as Error).message); }
o!.a TypeError: Cannot read properties of undefined (reading 'a')
! を使ってよいのは、「型では表せないが、値があることを別の方法で確かめてある」場合です。
| 場面 | ! の適否 |
より確実な書き方 |
|---|---|---|
直前の行で map.has(key) を確かめてから map.get(key)! |
使える | const v = map.get(key); if (v !== undefined) {...} |
| 自分で書いた HTML に必ずある要素を取得する | 使える(理由をコメントに) | if (!el) throw new Error("...") で早めに止める |
| テストコードで、準備したデータを取り出す | 使える | ― |
| API の応答や利用者の入力など、外から来る値 | 向かない | 型ガード、?? で既定値 |
| エラーを消したいだけで、値がある根拠が無い | 向かない | 型ガード、早期 return |
DOM の取得で比べると、次のようになります(input.value = "a" だけが TS18047 になり、他の3つは通りました)。
const input = document.querySelector<HTMLInputElement>("#q");
input.value = "a"; // TS18047: 'input' is possibly 'null'.
input!.value = "a"; // 通る。要素が無ければ実行時に TypeError
if (input) input.value = "a"; // 通る。要素が無ければ何もしない
const el2 = document.getElementById("app") as HTMLDivElement; // as でも null が外れる
この DOM のコードはコンパイルのみ確認しており、ブラウザーでの実行は未実行です。
明確な代入アサーション(プロパティ名の後ろの !)
クラスのプロパティに初期値が無いと TS2564 になります。直し方は4つあり、! は最後の選択肢です。
// cls.ts
class Repo {
private items!: string[]; // 明確な代入アサーション
init() { this.items = ["a"]; }
count() { return this.items.length; }
}
const r = new Repo();
try { console.log(r.count()); } catch (e) { console.log((e as Error).name + ": " + (e as Error).message); }
r.init(); console.log(r.count());
let token!: string;
function setup() { token = "abc"; }
setup(); console.log(token.length);
class Fixed {
count = 0; // 初期値を書く
label: string;
memo?: string; // 省略可能にする
constructor(label: string) { this.label = label; }
}
console.log(new Fixed("x"));
実行結果:
TypeError: Cannot read properties of undefined (reading 'length')
1
3
Fixed { count: 0, label: 'x', memo: undefined }
init() を呼ぶ前に count() を呼ぶと、TypeError になりました。! を付けると、「代入の前に読んでいないか」のチェックを自分で引き受けることになります。
| TS2564 の直し方 | 書き方 | 向いている場面 |
|---|---|---|
| 初期値を書く | count = 0; |
最初の値が決まっている |
| コンストラクターで代入する | constructor(label: string) { this.label = label; } |
作るときに値を受け取る |
| 省略可能にする | memo?: string; |
値が無い状態も正しい状態である |
| 明確な代入アサーション | items!: string[]; |
フレームワークやライブラリが後から値を入れる |
変数でも同じ記法が使えます。let token!: string; と書くと、関数の中で代入している場合など、コンパイラーが代入を追えない場面で TS2454(Variable is used before being assigned.)を出さないようにできます。
OR演算子と ?? で、0 や空文字の結果が変わる
|| は左側が「偽とみなされる値」のときに右側を返し、?? は左側が null か undefined のときだけ右側を返します。違いが出るのは 0、空文字、false、NaN です。
// rt.ts(抜粋)
const vals: [string, unknown][] = [["0", 0], ['""', ""], ["false", false], ["NaN", NaN],
["null", null], ["undefined", undefined], ['"a"', "a"]];
console.log("値".padEnd(12), "|| 'x'".padEnd(10), "?? 'x'");
for (const [k, v] of vals) console.log(k.padEnd(12), String(v || "x").padEnd(10), String(v ?? "x"));
type Conf = { retry?: number; label?: string; verbose?: boolean };
const conf: Conf = { retry: 0, label: "", verbose: false };
// conf.retry || 3 などを順に表示
実行結果:
値 || 'x' ?? 'x'
0 x 0
"" x
false x false
NaN x NaN
null x x
undefined x x
"a" a a
retry || 3 3
retry ?? 3 0
label || "(なし)" "(なし)"
label ?? "(なし)" ""
verbose || true true
verbose ?? true false
設定値の例で見ると、違いがはっきりします。
| 設定した値 | OR演算子での結果 | ?? での結果 | 意図に合うのは |
|---|---|---|---|
retry: 0(再試行しない) |
3(設定が無視される) | 0 | ?? |
label: ""(ラベルなし) |
"(なし)" | ""(空文字のまま) | 場合による。空文字も「未入力」と扱うなら OR演算子 |
verbose: false(詳細表示しない) |
true(設定が無視される) | false | ?? |
「値が設定されていないときだけ既定値にしたい」なら ??、「空文字や 0 も未入力とみなしたい」なら || と使い分けます。数値と真偽値の既定値は、ほとんどの場合 ?? が合います。
なお、2つをかっこなしで混ぜるとエラーになります。優先順位の取り違えを防ぐための仕様です。
const x = a || b ?? "z";
mix.ts(2,11): error TS5076: '||' and '??' operations cannot be mixed without parentheses.
(a || b) ?? "z" のように、かっこで順序を明示します。
つまずきやすい4つの場面
コールバックの中で絞り込みが外れる
if で調べたのに、コールバックの中でまた TS18048 が出ることがあります。
declare let current: User | undefined;
function a() {
if (current) {
setTimeout(() => console.log(current.name), 0);
}
}
function b() {
const u = current;
if (u) {
setTimeout(() => console.log(u.name), 0);
}
}
narrow.ts(7,34): error TS18048: 'current' is possibly 'undefined'.
let の変数は、コールバックが実行されるまでの間に書き換えられる可能性があるため、絞り込みが引き継がれません。const の変数に入れ直すと、関数 b のようにエラーなく書けます。! を足すより、こちらが確実です。
filter で undefined を取り除く
function c(xs: (string | undefined)[]) {
const ys: string[] = xs.filter((x) => x !== undefined); // 通る
const zs: string[] = xs.filter(Boolean); // TS2322
}
narrow.ts(22,9): error TS2322: Type '(string | undefined)[]' is not assignable to type 'string[]'.
TypeScript 7.0.2 では、(x) => x !== undefined のようなアロー関数は型ガードとして推論され、結果が string[] になりました。filter(Boolean) は型が絞り込まれないため、アロー関数で書くか、先ほどの isDefined を渡します。
配列の添字と Map の get
const arr = [1, 2, 3];
const first: number = arr[0];
const m = new Map<string, number>();
const got: number = m.get("a");
--strict だけの場合、エラーは m.get("a") の行(TS2322)だけです。配列の添字アクセスは number 型として扱われます。--noUncheckedIndexedAccess を付けると、arr[0] も number | undefined になり、同じ TS2322 が出ました。
| 取得方法 | --strict のみ |
--noUncheckedIndexedAccess あり |
|---|---|---|
arr[0] |
number |
number | undefined |
map.get(key) |
number | undefined |
number | undefined |
arr.find(...) |
number | undefined |
number | undefined |
TypeScript 7.0.2 の tsc --init が作る tsconfig.json には "noUncheckedIndexedAccess": true と "exactOptionalPropertyTypes": true も入っていました。新しく作ったプロジェクトで配列の添字に TS2532 が出るのは、この設定によるものです。直し方はこれまでと同じで、arr[0] ?? 0 や if (first !== undefined) を使います。
if (value) で 0 や空文字が外れる
if (count) と書くと、count が 0 のときも else 側に進みます。数値や文字列で「値があるか」を調べるときは、if (count !== undefined) または if (count != null) と書くと、0 や空文字を値として扱えます。|| と ?? の違いと同じ考え方です。
使い分けの判断表
| 状況 | 選ぶ記法 | 例 |
|---|---|---|
| 無くてもよい項目を型で表す | ?(省略可能) |
nickname?: string |
| 既定値が決まっている引数 | 既定値つき引数 | sep = "/" |
| 無いかもしれない値の奥を読む | ?. |
user?.address?.city |
| 無いときの代わりの値を決める | ?? |
conf.retry ?? 3 |
| 無いときだけ初期化する | ??= |
cache.v ??= 100 |
| 値の有無で処理を分ける | if の型ガード、早期 return | if (!u) return; |
| 値が無いのは想定外で、すぐ知りたい | アサーション関数、throw | assertDefined(u) |
| 値があることを別の方法で確かめてある | ! |
map.get(key)!(直前に has を確認) |
| 後から必ず代入されるプロパティ | 明確な代入アサーション | items!: string[] |
よくある質問
Q. ! と ?. はどう使い分けますか?
A. ?. は実行時に null と undefined を調べ、無ければ undefined を返して先へ進みます。! は調べずに通すので、無ければ実行時に TypeError になります。値が無い可能性が少しでもあるなら ?.(と ??)を、値があることを別の方法で確かめてあるなら ! を選びます。
Q. TS2532 と TS18048 は何が違いますか? A. 内容は同じです。対象が変数やプロパティで名前を示せるときは TS18048('found' is possibly 'undefined'.)、関数の戻り値など名前の無い式のときは TS2532(Object is possibly 'undefined'.)と表示されます。null も含む場合は TS18049 と TS2533 です。
Q. ?? と || はどちらを使えばよいですか?
A. 0、空文字、false を「設定された値」として残したいなら ?? です。今回の実行結果でも、0 || 3 は 3、0 ?? 3 は 0 でした。空文字も未入力とみなしたい場面では || が合います。
Q. エラーを消すために strict を外してもよいですか? A. 外すとエラーは表示されなくなりますが、値が無いときに実行時エラーになる点は変わりません。エラーが出た箇所ごとに「無いときにどうするか」を決めていくと、実行時のエラーを先回りして減らせます。
まとめ
?は「無くてもよい」を型で表す。型に undefined が加わり、コンパイル後は消えます。?.は null と undefined を実行時に調べ、無ければ undefined を返す。右側の式は評価されません。??と??=は null と undefined のときだけ既定値を使う。0、空文字、false はそのまま残ります。!はコンパイル後に消える。値を調べないので、無ければ実行時に TypeError になります。- possibly 'undefined' の直し方は、型ガード → 早期 return →
??で既定値 →!の順に検討します。 - TS2564 は、初期値・コンストラクター・
?を先に試し、後から代入されると分かっているときに明確な代入アサーションを使います。 - コールバックの中では
constに入れ直す、filter はアロー関数で書くと、!に頼らず型を絞り込めます。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。