この記事の要点
- 居住用財産の3000万円特別控除は「住まなくなった日」を起算点として厳格に期限を判定するロジックを実装します。
- 賃貸転用・売却・リバースモーゲージの各選択肢を状態遷移モデルとして定義し、排他的な分岐をTypeScriptで型安全に表現します。
- 日付境界値テストを網羅的に記述することで、税制上の適用要件判定におけるエッジケースのバグを機械的に防ぎます。
環境・前提条件
- Node.js: v20.11.0
- TypeScript: v5.3.3
- date-fns: v3.3.1
- Vitest: v1.3.1
居住用財産特例の期限計算をTypeScriptで正確に実装するには?
住まなくなった日の翌年以降3年目の12月31日という税法上の期日を、起算点をもとに加算と年末補正を組み合わせて算出します。
「居住用財産を譲渡した場合の3,000万円の特別控除の特例」は、住まなくなった日から3年を経過する日の属する年の12月31日までに譲渡する必要があります(参照: 国税庁 No.3302 マイホームを売ったときの特例)。この計算は単純な日数加算では満たすことができないため、日付ライブラリを用いて「年」の加算と年末日付の固定化を行います。
import { endOfYear, addYears, isBefore, isEqual, parseISO } from 'date-fns';
export interface PropertyTransferEligibility {
moveOutDate: string; // ISO形式 (YYYY-MM-DD)
transferDate: string; // ISO形式 (YYYY-MM-DD)
}
/**
* 居住用財産の特例適用期限(住まなくなった日から3年を経過する日の属する年の12月31日)を算出
*/
export function calculateExemptionDeadline(moveOutDateStr: string): Date {
const moveOutDate = parseISO(moveOutDateStr);
const targetYearDate = addYears(moveOutDate, 3);
return endOfYear(targetYearDate);
}
/**
* 譲渡日が特例期限内であるかを判定
*/
export function isEligibleForSpecialExemption(params: PropertyTransferEligibility): boolean {
const transferDate = parseISO(params.transferDate);
const deadline = calculateExemptionDeadline(params.moveOutDate);
return isBefore(transferDate, deadline) || isEqual(transferDate, deadline);
}
実家の活用分岐(売却・賃貸・担保化)を排他的な状態遷移で管理するには?
直和型(Discriminated Unions)を用いて各選択肢に応じた必須パラメータと不変条件を型定義します。
実家の活用方針には「売却」「賃貸転用」「リバースモーゲージ」などがあり、それぞれ必要な入力項目(修繕予算、想定賃料、契約者年齢など)が異なります。これらを単一のフラットなオブジェクトで表現すると未定義値の混入リスクが生じるため、TypeScriptの判別共用体で型安全にモデル化します。
export type PropertyOption =
| {
type: 'SALE';
expectedSalePrice: number;
moveOutDate: string;
transferDate: string;
}
| {
type: 'RENT';
initialRenovationCost: number;
monthlyRent: number;
managementFeeRate: number; // 例: 0.05
}
| {
type: 'REVERSE_MORTGAGE';
ownerAge: number; // 契約時の満年齢
propertyAssessedValue: number; // 担保評価額
monthlyBorrowingLimit: number;
};
export function validateOptionConditions(option: PropertyOption): boolean {
switch (option.type) {
case 'SALE':
return isEligibleForSpecialExemption({
moveOutDate: option.moveOutDate,
transferDate: option.transferDate,
});
case 'RENT':
return option.initialRenovationCost >= 0 && option.monthlyRent > 0;
case 'REVERSE_MORTGAGE':
// 一般的な金融機関のリバースモーゲージ基準(例: 満55歳以上または満60歳以上)のバリデーション
return option.ownerAge >= 60 && option.propertyAssessedValue > 0;
}
}
起算日と境界条件のテストケースをどのように網羅すべきか?
年の変わり目である12月31日と翌年1月1日の境界値をテストケースに設定して確実に検証します。
税務関連のシステムロジックでは、1日ずれるだけで特例適用の可否判定が逆転します。Vitest等のテストフレームワークを利用し、起算日(住まなくなった日)から3年後の12月31日23:59:59までの判定精度をテストコードで担保します。
import { describe, it, expect } from 'vitest';
import { calculateExemptionDeadline, isEligibleForSpecialExemption } from './propertyAdvisor';
describe('calculateExemptionDeadline', () => {
it('2021年3月15日に転出した場合、2024年12月31日が適用期限となること', () => {
const deadline = calculateExemptionDeadline('2021-03-15');
expect(deadline.getFullYear()).toBe(2024);
expect(deadline.getMonth()).toBe(11); // 0-indexed で12月
expect(deadline.getDate()).toBe(31);
});
it('2021年12月31日に転出した場合、2024年12月31日が適用期限となること', () => {
const deadline = calculateExemptionDeadline('2021-12-31');
expect(deadline.getFullYear()).toBe(2024);
expect(deadline.getDate()).toBe(31);
});
});
describe('isEligibleForSpecialExemption', () => {
it('期限内の譲渡日はtrueを返し、期限翌日(翌年1月1日)はfalseを返すこと', () => {
const moveOutDate = '2021-05-01';
// 期限内(2024-12-31)
expect(isEligibleForSpecialExemption({ moveOutDate, transferDate: '2024-12-31' })).toBe(true);
// 期限超過(2025-01-01)
expect(isEligibleForSpecialExemption({ moveOutDate, transferDate: '2025-01-01' })).toBe(false);
});
});
このように、業務ドメインにおける法的要件や期限ルールをコードに落とし込む際は、起算点を明示し、型システムと自動テストを組み合わせることで、判定ミスのない堅牢な設計が可能になります。
この記事は株式会社HY(横浜・湘南/不動産・相続)の社内システム開発の記録です。