import { ComputedLine, Line } from './Line';
import TaxReturn from './TaxReturn';
-import Form from './Form';
+import Form, { isFormT } from './Form';
import { InconsistencyError, NotFoundError } from './Errors';
test('add and get line', () => {
//expect(() => f.getValue(tr, 'other')).toThrow(NotFoundError);
});
+test('form types', () => {
+ class FormA extends Form<any> {
+ readonly name = 'A';
+ protected readonly _lines = {};
+ };
+ class FormB extends Form<any> {
+ readonly name = 'B';
+ protected readonly _lines = {};
+ };
+
+ expect(isFormT(new FormA(), FormA)).toBe(true);
+ expect(isFormT(new FormB(), FormA)).toBe(false);
+ expect(isFormT(new FormA(), FormB)).toBe(false);
+ expect(isFormT(new FormB(), FormB)).toBe(true);
+});
+
/*
abstract class Form2<L extends { [key: string]: Line<any> } , I> {
abstract readonly name: string;
private readonly _input?: I;
+ // Avoid using this; prefer the getLine() helpers declared below. This
+ // is only exposed for propagating line type information.
+ get lines(): L { return this._lines; }
+
constructor(input?: I) {
this._input = input;
}
return this._input[name];
}
};
+
+export type FormClass<T extends Form<any>> = new (...args: any[]) => T;
+
+export function isFormT<T extends Form<any>>(form: Form<any>,
+ formClass: FormClass<T>):
+ form is T {
+ return form.constructor === formClass;
+}
import { Line, AccumulatorLine, InputLine, ReferenceLine, ComputedLine } from './Line';
-import Form from './Form';
+import Form, { FormClass } from './Form';
import TaxReturn from './TaxReturn';
import { NotFoundError } from './Errors';
class TestForm extends Form<TestForm['_lines']> {
readonly name = 'Form 1';
protected readonly _lines = {
- '6b': new ConstantLine(12.34)
+ '6b': new ConstantLine(12.34),
+ 's': new ConstantLine('abc'),
};
};
const tr = new TaxReturn(2019);
tr.addForm(new TestForm());
- const l1 = new ReferenceLine<number>('Form 1', '6b');
- expect(l1.value(tr)).toBe(12.34);
+ const l1 = new ReferenceLine(TestForm, '6b');
+ let n: number = l1.value(tr);
+ expect(n).toBe(12.34);
- const l2 = new ReferenceLine<number>('Form 2', '6b');
- expect(() => l2.value(tr)).toThrow(NotFoundError);
+ const l2 = new ReferenceLine(TestForm, 's');
+ let s: string = l2.value(tr);
+ expect(s).toBe('abc');
+
+ //TYPEERROR:
+ //const l3 = new ReferenceLine(TestForm, '7a');
+ //let n2: string = l1.value(tr);
+ //let s2: number = l2.value(tr);
+});
+
+test('self reference line', () => {
+ class OtherForm extends Form<OtherForm['_lines']> {
+ readonly name = 'Form A';
+ protected readonly _lines = {
+ '6c': new ConstantLine(55)
+ };
+ };
+ class TestForm extends Form<TestForm['_lines']> {
+ readonly name = 'Form 1';
+ protected readonly _lines = {
+ 'a': new ConstantLine(100.2),
+ 'b': new ReferenceLine(OtherForm, '6c'),
+ 'c': new ReferenceLine((TestForm as unknown) as FormClass<Form<any>>, 'b'),
+ 'd': new ReferenceLine(TestForm as any, 'b'),
+ };
+ };
+
+ const tr = new TaxReturn(2019);
+ const f = new TestForm();
+ tr.addForm(f);
+ tr.addForm(new OtherForm());
- const l3 = new ReferenceLine<number>('Form 1', '7a');
- expect(() => l3.value(tr)).toThrow(NotFoundError);
+ expect(f.getValue(tr, 'a')).toBe(100.2);
+ expect(f.getValue(tr, 'b')).toBe(55);
+ expect(f.getValue(tr, 'c')).toBe(55);
+ expect(f.getValue(tr, 'd')).toBe(55);
});
test('input line', () => {
readonly name = 'Z-2';
protected readonly _lines = {
'2c': new ComputedLine<number>((tr: TaxReturn, l: Line<number>): any => {
- return tr.getForm('Z').getLine('3').value(tr) * 0.2;
+ return tr.getForm(FormZ).getLine('3').value(tr) * 0.2;
})
};
};
tr.addForm(new FormZ({ 'input': 100 }));
tr.addForm(new FormZ2());
- const l = new ReferenceLine<number>('Z-2', '2c');
+ const l = new ReferenceLine(FormZ2, '2c');
expect(l.value(tr)).toBe(20);
});
tr.addForm(new TestForm());
tr.addForm(new TestForm());
- const l = new AccumulatorLine('Form B', 'g');
+ const l = new AccumulatorLine(TestForm, 'g');
expect(l.value(tr)).toBe(300.75);
});
import TaxReturn from './TaxReturn';
-import Form from './Form';
+import Form, { FormClass } from './Form';
export abstract class Line<T> {
private _description?: string;
}
};
-export class ReferenceLine<T> extends Line<T> {
- private _form: string;
- private _line: string;
+export class ReferenceLine<F extends Form<any>,
+ L extends keyof F['lines'],
+ T extends ReturnType<F['lines'][L]['value']>>
+ extends Line<T> {
+ private _form: FormClass<F>;
+ private _line: L;
private _fallback?: T;
- constructor(form: string, line: string, description?: string, fallback?: T) {
+ // If creating a ReferenceLine and F is the same class as the
+ // the one the Line is in, erase |form|'s type with |as any| to
+ // keep TypeScript happy.
+ constructor(form: FormClass<F>, line: L, description?: string, fallback?: T) {
super(description || `Reference F${form}.L${line}`);
this._form = form;
this._line = line;
}
value(tr: TaxReturn): T {
- if (this._fallback !== undefined && !tr.maybeGetForm(this._form))
+ const form: F = tr.findForm(this._form);
+ if (this._fallback !== undefined && !form)
return this._fallback;
- return tr.getForm(this._form).getLine(this._line).value(tr);
+ const value: T = form.getValue(tr, this._line);
+ return value;
}
};
}
};
-export class AccumulatorLine extends Line<number> {
- private _form: string;
- private _line: string;
+export class AccumulatorLine<F extends Form<any>,
+ L extends keyof F['lines']>
+ extends Line<number> {
+ private _form: FormClass<F>;
+ private _line: L;
- constructor(form: string, line: string, description?: string) {
+ constructor(form: FormClass<F>, line: L, description?: string) {
super(description || `Accumulator F${form}.L${line}`);
this._form = form;
this._line = line;
}
value(tr: TaxReturn): number {
- const forms = tr.getForms(this._form);
- const reducer = (acc: number, curr: Form<any>) => acc + (curr.getValue(tr, this._line) as number);
+ const forms: F[] = tr.findForms(this._form);
+ const reducer = (acc: number, curr: F) => acc + curr.getValue(tr, this._line);
return forms.reduce(reducer, 0);
}
};
const f = new TestForm();
tr.addForm(f);
expect(() => tr.addForm(new TestForm)).toThrow(InconsistencyError);
- expect(tr.getForm(f.name)).toBe(f);
+ expect(tr.getForm(TestForm)).toBe(f);
+ expect(tr.findForm(TestForm)).toBe(f);
});
test('multiple-copy forms', () => {
tr.addForm(f1);
tr.addForm(f2);
- expect(() => tr.getForm(f1.name)).toThrow(InconsistencyError);
+ expect(() => tr.getForm(TestForm)).toThrow(InconsistencyError);
+ expect(() => tr.findForm(TestForm)).toThrow(InconsistencyError);
- const forms = tr.getForms(f1.name);
+ const forms = tr.findForms(TestForm);
expect(forms.length).toBe(2);
expect(forms).toContain(f1);
expect(forms).toContain(f2);
});
test('get non-existent form', () => {
+ class TestForm extends Form<null> {
+ readonly name = 'Test Form';
+ protected readonly _lines = null;
+ }
const tr = new TaxReturn(2019);
- expect(() => tr.getForm('form')).toThrow(NotFoundError);
- expect(tr.getForms('form')).toEqual([]);
+ expect(() => tr.getForm(TestForm)).toThrow(NotFoundError);
+ expect(tr.findForm(TestForm)).toBeNull();
+ expect(tr.findForms(TestForm)).toEqual([]);
+});
+
+type FormClass<T extends Form<any>> = Function & { prototype: T };
+
+class TR {
+ private _forms: Form<any>[] = [];
+
+ add(form: Form<any>) {
+ this._forms.push(form);
+ }
+
+ find(name: string): Form<any> {
+ const forms = this._forms.filter(f => f.name == name);
+ if (forms.length > 0)
+ return forms[0];
+ return null;
+ }
+
+ find2<T extends Form<any>>(cls: FormClass<T>): T[] {
+ let forms: T[] = [];
+ const isT = (form: Form<any>): form is T => form.constructor === cls;
+ for (let form of this._forms) {
+ if (isT(form))
+ forms.push(form);
+ }
+ return forms;
+ }
+};
+
+test('type test', () => {
+ class FormA extends Form<FormA['_lines']> {
+ readonly name = 'Form A';
+ protected readonly _lines = {};
+ };
+ class FormB extends Form<FormB['_lines']> {
+ readonly name = 'Form B';
+ readonly supportsMultipleCopies = true;
+ protected readonly _lines = {};
+ };
+
+ const tr = new TR();
+
+ tr.add(new FormA());
+ tr.add(new FormB());
+
+ expect(tr.find('Form A')).not.toBeNull();
+
+ expect(tr.find2(FormB).length).toBe(1);
+
+ tr.add(new FormB());
+ expect(tr.find2(FormB).length).toBe(2);
+
});
-import Form from './Form';
+import Form, { FormClass, isFormT } from './Form';
import Person, { Relation } from './Person';
import { NotFoundError, InconsistencyError, UnsupportedFeatureError } from './Errors';
addForm(form: Form<any>) {
if (!form.supportsMultipleCopies) {
- const other = this.getForms(form.name);
+ const other = this.findForms(form.constructor as FormClass<Form<any>>);
if (other.length > 0) {
throw new InconsistencyError(`Cannot have more than one type of form ${form.name}`);
}
this._forms.push(form);
}
- maybeGetForm<T extends Form<any>>(name: string): T | null {
- const forms = this.getForms<T>(name);
- if (forms.length == 0) {
+ findForm<T extends Form<any>>(cls: FormClass<T>): T | null {
+ const forms = this.findForms(cls);
+ if (forms.length == 0)
return null;
- }
- if (forms.length > 1) {
- throw new InconsistencyError(`More than 1 form named ${name}`);
- }
+ if (forms.length > 1)
+ throw new InconsistencyError(`Form ${forms[0].name} has multiple copies`);
return forms[0];
}
- getForm<T extends Form<any>>(name: string): T {
- const form = this.maybeGetForm<T>(name);
- if (!form)
- throw new NotFoundError(`No form named ${name}`);
- return form;
+ findForms<T extends Form<any>>(cls: FormClass<T>): T[] {
+ const forms: T[] = this._forms.filter((form: Form<any>): form is T => isFormT(form, cls));
+ return forms;
}
- getForms<T extends Form<any>>(name: string): T[] {
- return this._forms.filter(f => f.name == name) as T[];
+ getForm<T extends Form<any>>(cls: FormClass<T>): T {
+ const form = this.findForm(cls);
+ if (!form)
+ throw new NotFoundError(`No form ${cls}`);
+ return form;
}
};
-import Form from '../Form';
+import Form, { FormClass } from '../Form';
import TaxReturn from '../TaxReturn';
import { Line, AccumulatorLine, ComputedLine, ReferenceLine } from '../Line';
import { UnsupportedFeatureError } from '../Errors';
import Form8959 from './Form8959';
+import Form1099INT from './Form1099INT';
+import Form1099DIV from './Form1099DIV';
+import FormW2 from './FormW2';
export enum FilingStatus {
Single,
readonly name = '1040';
protected readonly _lines = {
- '1': new AccumulatorLine('W-2', '1', 'Wages, salaries, tips, etc.'),
- '2a': new AccumulatorLine('1099-INT', '8', 'Tax-exempt interest'),
- '2b': new AccumulatorLine('1099-INT', '1', 'Taxable interest'),
- '3a': new AccumulatorLine('1099-DIV', '1b', 'Qualified dividends'),
- '3b': new AccumulatorLine('1099-DIV', '1a', 'Ordinary dividends'),
+ '1': new AccumulatorLine(FormW2, '1', 'Wages, salaries, tips, etc.'),
+ '2a': new AccumulatorLine(Form1099INT, '8', 'Tax-exempt interest'),
+ '2b': new AccumulatorLine(Form1099INT, '1', 'Taxable interest'),
+ '3a': new AccumulatorLine(Form1099DIV, '1b', 'Qualified dividends'),
+ '3b': new AccumulatorLine(Form1099DIV, '1a', 'Ordinary dividends'),
// 4a and 4b are complex
'4b': new ComputedLine(() => 0),
'4d': new ComputedLine(() => 0),
// 4c and 4d are not supported
// 5a and 5b are not supported
- '6': new ReferenceLine<number>('Schedule D', '21', 'Capital gain/loss', 0),
- '7a': new ReferenceLine<number>('Schedule 1', '9', 'Other income from Schedule 1', 0),
+ '6': new ReferenceLine(/*'Schedule D'*/ undefined, '21', 'Capital gain/loss', 0),
+ '7a': new ReferenceLine(/*'Schedule 1'*/ undefined, '9', 'Other income from Schedule 1', 0),
'7b': new ComputedLine((tr: TaxReturn): number => {
- const lineIds = ['1', '2b', '3b', '4b', '4d', /*'5b',*/ '6', '7a'];
- const lines: number[] = lineIds.map(l => this.getValue(tr, l as keyof Form1040['_lines']));
- return reduceBySum(lines);
+ let income = 0;
+ income += this.getValue(tr, '1');
+ income += this.getValue(tr, '2b');
+ income += this.getValue(tr, '3b');
+ income += this.getValue(tr, '4b');
+ income += this.getValue(tr, '4d');
+ //income += this.getValue(tr, '5b');
+ income += this.getValue(tr, '6');
+ income += this.getValue(tr, '7a');
+ return income;
}, 'Total income'),
- '8a': new ReferenceLine<number>('Schedule 1', '22', 'Adjustments to income', 0),
+ '8a': new ReferenceLine(undefined /*'Schedule 1'*/, '22', 'Adjustments to income', 0),
'8b': new ComputedLine((tr: TaxReturn): number => {
return this.getValue(tr, '7b') - this.getValue(tr, '8a');
}, 'Tax'),
'12b': new ComputedLine((tr: TaxReturn): number => {
- return this.getValue(tr, '12a') + tr.getForm<Schedule2>('Schedule 2').getValue(tr, '3');
+ return this.getValue(tr, '12a') + tr.getForm(Schedule2).getValue(tr, '3');
}, 'Additional tax'),
// Not supported: 13a - child tax credit
return value < 0 ? 0 : value;
}),
- '15': new ReferenceLine<number>('Schedule 2', '10', undefined, 0),
+ '15': new ReferenceLine(undefined /*'Schedule 2'*/, '10', undefined, 0),
'16': new ComputedLine((tr: TaxReturn): number => {
return this.getValue(tr, '14') + this.getValue(tr, '15');
'17': new ComputedLine((tr: TaxReturn): number => {
const fedTaxWithheldBoxes = [
- ['W-2', '2'], ['1099-R', '4'], ['1099-DIV', '4'], ['1099-INT', '4']
+ new AccumulatorLine(FormW2, '2'),
+ //new AccumulatorLine(Form1099R, '4'),
+ new AccumulatorLine(Form1099DIV, '4'),
+ new AccumulatorLine(Form1099INT, '4'),
];
- const withholding = fedTaxWithheldBoxes.map(b => (new AccumulatorLine(b[0], b[1])).value(tr));
+ const withholding: number[] = fedTaxWithheldBoxes.map(b => b.value(tr));
let additionalMedicare = 0;
- const f8959 = tr.maybeGetForm('8595')
+ const f8959 = tr.findForm(Form8959)
if (f8959) {
additionalMedicare = f8959.getValue(tr, '24');
}
// 18 not supported
- '19': new ReferenceLine<number>('1040', '17', 'Total payments'),
+ '19': new ReferenceLine(Form1040 as any, '17', 'Total payments'),
'20': new ComputedLine((tr: TaxReturn): number => {
- const l16 = this.getValue(tr, '16');
- const l19 = this.getValue(tr, '19');
+ const l16: number = this.getValue(tr, '16');
+ const l19: number = this.getValue(tr, '19');
if (l19 > l16)
return l19 - l16;
return 0;
'1': new ComputedLine((tr: TaxReturn): number => {
// TODO - this is just using Taxable Income, rather than AMT-limited
// income
- const taxableIncome = tr.getForm('1040').getValue(tr, '11b');
- switch (tr.getForm<Form1040>('1040').getInput('filingStatus')) {
+ const f1040 = tr.getForm(Form1040);
+ const taxableIncome = f1040.getValue(tr, '11b');
+ switch (f1040.getInput('filingStatus')) {
case FilingStatus.Single:
if (taxableIncome < 510300)
return 0;
// 6 is not supported (Additional tax on IRAs, other qualified retirement plans, and other tax-favored accounts)
// 7 is not supported (Household employment taxes.)
'8': new ComputedLine((tr: TaxReturn): number => {
- const f1040 = tr.getForm<Form1040>('1040');
+ const f1040 = tr.getForm(Form1040);
const wages = f1040.getLine('1').value(tr);
const agi = f1040.getLine('8b').value(tr);
let value = 0;
if (additionalMedicare) {
- const f8959 = tr.getForm('8959');
+ const f8959 = tr.getForm(Form8959);
value += f8959.getValue(tr, '18');
}
if (niit) {
- const f8960 = tr.getForm('8960');
+ //const f8960 = tr.getForm('8960');
}
return value;
import { Line, AccumulatorLine, ComputedLine, ReferenceLine } from '../Line';
import Form1040, { FilingStatus } from './Form1040';
+import FormW2 from './FormW2';
export default class Form8959 extends Form<Form8959['_lines']> {
readonly name = '8959';
protected readonly _lines = {
- '1': new AccumulatorLine('W-2', '5', 'Medicare wages'),
+ '1': new AccumulatorLine(FormW2, '5', 'Medicare wages'),
// 2 is not supported (Unreported tips from Form 4137)
// 3 is not supported (Wages from Form 8919)
'4': new ComputedLine((tr: TaxReturn): number => {
return this.getValue(tr, '1');
}),
'5': new ComputedLine((tr: TaxReturn): number => {
- return Form8959.filingStatusLimit(tr.getForm<Form1040>('1040').getInput('filingStatus'));
+ return Form8959.filingStatusLimit(tr.getForm(Form1040).getInput('filingStatus'));
}),
'6': new ComputedLine((tr: TaxReturn): number => {
const value = this.getValue(tr, '5') - this.getValue(tr, '4');
return this.getValue(tr, '7');
}),
- '19': new AccumulatorLine('W-2', '6', 'Medicare tax withheld'),
- '20': new ReferenceLine<number>('8595', '1'),
+ '19': new AccumulatorLine(FormW2, '6', 'Medicare tax withheld'),
+ '20': new ReferenceLine(Form8959 as any, '1'),
'21': new ComputedLine((tr: TaxReturn): number => {
return this.getValue(tr, '20') * 0.0145;
}, 'Regular Medicare withholding on Medicare wages'),