TypescriptのType Guardはまあまあ優秀ですが、動かない時もある、というお話です。
うまく動くコード
interface StringValue {
isString: true;
value: string;
}
interface NumberValue {
isString: false;
value: number;
}
type ValueObject = StringValue | NumberValue;
function getString(obj: ValueObject): string {
if (obj.isString) {
return obj.value; // obj.valueの型はstring
}
return (obj.value + 10).toString(); // obj.valueの型はnumber
}
うまく動かないコード
分割代入します
interface StringValue {
isString: true;
value: string;
}
interface NumberValue {
isString: false;
value: number;
}
type ValueObject = StringValue | NumberValue;
function getString(obj: ValueObject): string {
const { isString, value } = obj;
if (isString) {
return value; // obj.valueの型はstring | number
// Type 'number' is not assignable to type 'string'.
}
return (value + 10).toString(); // obj.valueの型はstring | number
// Operator '+' cannot be applied to types 'string | number' and 'number'.
}
分割代入は大変便利ですが、Union型を利用する場合は気をつけましょうということでした。
なぜ?
この件に関するIssue (3年以上前ですが)に詳細が有りました。これはバグではなく仕様です。
once an object is destructed, the compiler can no longer make any assumptions about the relationships between the parts. Doing so requires data-flow analysis and alias tracking which is not trivial tasks.
一度オブジェクトが分割されると、コンパイラーはその分割された部分の関係についていかなる仮定もすることができなくなる。そのようにするには、データフローの分析とaliasの追跡が必要となり、それは簡単なタスクでは無い。
(おまけ)ESLint Plugin Reactのreact/destructuring-assignmentについて
私が使っていたESLintの設定の一つで、Reactのpropsは利用前に必ず分割代入されなければならないというものでした。
const Comp: React.FC<{value: string}> = ({
value
}) => (
<>
{value}
</>
);
const Comp1: React.FC<{ value: string }> = ({ value }) => <>{value}</>; // OK
const Comp2: React.FC<{ value: string }> = props => <>{props.value}</>;
// Must use destructuring props assignment
普段使う分にはこのルールは問題無いのですが、propsの型がUnion型の時に上記のような問題が発生する場合があります。したがって、個人的にはOFFにすることをお勧めします。(plugin:react/recommendedを利用している方は含まれていないので心配無用です)