commit 1828052daf6a5ed1eb59f78af31dd120f0232755
parent 60bce6002bc55339e17e058da596529bd614a49c
Author: Asher Morgan <59518073+ashermorgan@users.noreply.github.com>
Date: Sun, 22 Jun 2025 08:27:02 -0700
Convert most components to TypeScript
Diffstat:
15 files changed, 115 insertions(+), 65 deletions(-)
diff --git a/eslint.config.js b/eslint.config.js
@@ -1,11 +1,14 @@
-import { defineConfig, globalIgnores } from 'eslint/config'
+import { globalIgnores } from 'eslint/config'
+import { configureVueProject, defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
import globals from 'globals'
import js from '@eslint/js'
import ts from 'typescript-eslint'
import pluginVue from 'eslint-plugin-vue'
import pluginPlaywright from 'eslint-plugin-playwright'
-export default defineConfig([
+configureVueProject({ scriptLangs: ['ts', 'js'] })
+
+export default defineConfigWithVueTs([
{
name: 'app/files-to-lint',
files: ['**/*.{js,mjs,jsx,vue}'],
@@ -25,6 +28,7 @@ export default defineConfig([
js.configs.recommended,
ts.configs.recommended,
...pluginVue.configs['flat/essential'],
+ vueTsConfigs.recommended,
{
...pluginPlaywright.configs['flat/recommended'],
diff --git a/package-lock.json b/package-lock.json
@@ -19,6 +19,7 @@
"@tsconfig/node22": "^22.0.2",
"@types/node": "^24.0.3",
"@vitejs/plugin-vue": "^5.2.4",
+ "@vue/eslint-config-typescript": "^14.5.1",
"@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.7.0",
"eslint": "^9.22.0",
@@ -3577,6 +3578,32 @@
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT"
},
+ "node_modules/@vue/eslint-config-typescript": {
+ "version": "14.5.1",
+ "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-14.5.1.tgz",
+ "integrity": "sha512-ys6qdYHGXS/WLt0r5vUcTiG163F4NbNpx3ABTsGITw8k5uCFiv4g9E1N9Jydlw62KzJMVKGcpXbg6LCA3fV+eA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/utils": "^8.34.1",
+ "fast-glob": "^3.3.3",
+ "typescript-eslint": "^8.34.1",
+ "vue-eslint-parser": "^10.1.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "peerDependencies": {
+ "eslint": "^9.10.0",
+ "eslint-plugin-vue": "^9.28.0 || ^10.0.0",
+ "typescript": ">=4.8.4"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@vue/language-core": {
"version": "2.2.10",
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.10.tgz",
@@ -10151,7 +10178,6 @@
"integrity": "sha512-dbCBnd2e02dYWsXoqX5yKUZlOt+ExIpq7hmHKPb5ZqKcjf++Eo0hMseFTZMLKThrUk61m+Uv6A2YSBve6ZvuDQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"debug": "^4.4.0",
"eslint-scope": "^8.2.0",
@@ -10177,7 +10203,6 @@
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true,
"license": "Apache-2.0",
- "peer": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
diff --git a/package.json b/package.json
@@ -30,6 +30,7 @@
"@tsconfig/node22": "^22.0.2",
"@types/node": "^24.0.3",
"@vitejs/plugin-vue": "^5.2.4",
+ "@vue/eslint-config-typescript": "^14.5.1",
"@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.7.0",
"eslint": "^9.22.0",
diff --git a/src/components/DecimalInput.vue b/src/components/DecimalInput.vue
@@ -2,7 +2,7 @@
<input ref="inputElement" type="number" step="any" required @blur="onblur" v-model="stringValue">
</template>
-<script setup>
+<script setup lang="ts">
import { ref, watch } from 'vue';
import { formatNumber } from '@/utils/format';
@@ -21,7 +21,7 @@ const props = defineProps({
padding: {
type: Number,
default: 0,
- validator(value) {
+ validator(value: number) {
return value >= 0;
},
},
@@ -32,7 +32,7 @@ const props = defineProps({
digits: {
type: Number,
default: 1,
- validator(value) {
+ validator(value: number) {
return value > 0;
},
},
@@ -51,7 +51,7 @@ const stringValue = ref(format(model.value));
/**
* The input element
*/
-const inputElement = ref(null);
+const inputElement = ref();
/*
* Update the internal value when the component value changes
@@ -84,10 +84,10 @@ function onblur() {
/**
* Format a decimal as a string
- * @param {Number} value The decimal
- * @returns {String} The formated string
+ * @param {number} value The decimal
+ * @returns {string} The formated string
*/
-function format(value) {
+function format(value: number): string {
return formatNumber(value, props.padding, props.digits, true);
}
</script>
diff --git a/src/components/DoubleOutputTable.vue b/src/components/DoubleOutputTable.vue
@@ -26,10 +26,12 @@
</div>
</template>
-<script setup>
+<script setup lang="ts">
import { computed } from 'vue';
+import type { PropType } from 'vue';
+
import { formatDuration, formatNumber } from '@/utils/format';
-import { DISTANCE_UNITS } from '@/utils/units';
+import { DISTANCE_UNITS, DISTANCE_UNIT_KEYS } from '@/utils/units';
const props = defineProps({
/**
@@ -52,7 +54,7 @@ const props = defineProps({
* The set of input times
*/
inputTimes: {
- type: Array,
+ type: Array<number>,
default: () => [],
},
@@ -60,7 +62,7 @@ const props = defineProps({
* The input distance
*/
inputDistance: {
- type: Object,
+ type: Object as PropType<{ distanceValue: number, distanceUnit: DISTANCE_UNIT_KEYS }>,
default: () => ({
distanceValue: 5,
distanceUnit: 'kilometers',
@@ -79,10 +81,10 @@ const results = computed(() => {
]];
props.inputTimes.forEach((input, y) => {
- let row = [formatDuration(input, 3, 2, false)];
+ const row = [formatDuration(input, 3, 2, false)];
props.targets.forEach(target => {
- let result = props.calculateResult({ ...props.inputDistance, time: input }, target);
+ const result = props.calculateResult({ ...props.inputDistance, time: input }, target);
if (y === 0) {
results[0].push(result[result.result === 'key' ? 'value' : 'key']);
diff --git a/src/components/IntegerInput.vue b/src/components/IntegerInput.vue
@@ -2,7 +2,7 @@
<input ref="inputElement" type="number" step="1" required @blur="onblur" v-model="stringValue">
</template>
-<script setup>
+<script setup lang="ts">
import { ref, watch } from 'vue';
/**
@@ -20,7 +20,7 @@ const props = defineProps({
padding: {
type: Number,
default: 0,
- validator(value) {
+ validator(value: number) {
return value >= 0;
},
},
@@ -39,7 +39,7 @@ const stringValue = ref(format(model.value));
/**
* The input element
*/
-const inputElement = ref(null);
+const inputElement = ref();
/**
* Update the internal value when the component value changes
@@ -72,10 +72,10 @@ function onblur() {
/**
* Format an integer as a string
- * @param {Number} value The integer
- * @returns {String} The formated string
+ * @param {number} value The integer
+ * @returns {string} The formated string
*/
-function format(value) {
+function format(value: number): string {
return value.toString().padStart(props.padding, '0');
}
</script>
diff --git a/src/components/PaceInput.vue b/src/components/PaceInput.vue
@@ -5,8 +5,8 @@
<decimal-input v-model="model.distanceValue"
:aria-label="label + ' distance value'" :min="0" :digits="2"/>
<select v-model="model.distanceUnit" :aria-label="label + ' distance unit'">
- <option v-for="(value, key) in DISTANCE_UNITS" :key="key" :value="key">
- {{ value.name }}
+ <option v-for="key in DISTANCE_UNIT_KEYS" :key="key" :value="key">
+ {{ DISTANCE_UNITS[key].name }}
</option>
</select>
</div>
@@ -17,13 +17,21 @@
</div>
</template>
-<script setup>
-import { DISTANCE_UNITS } from '@/utils/units';
+<script setup lang="ts">
+import type { PropType } from 'vue';
+
+import { DISTANCE_UNITS, DISTANCE_UNIT_KEYS } from '@/utils/units';
import DecimalInput from '@/components/DecimalInput.vue';
import TimeInput from '@/components/TimeInput.vue';
import useObjectModel from '@/composables/useObjectModel';
+interface Pace {
+ distanceValue: number,
+ distanceUnit: DISTANCE_UNIT_KEYS,
+ time: number,
+};
+
const props = defineProps({
/**
* The prefix for each field's aria-label
@@ -37,7 +45,7 @@ const props = defineProps({
* The component value
*/
modelValue: {
- type: Object,
+ type: Object as PropType<Pace>,
default: () => ({
distanceValue: 5,
distanceUnit: 'kilometers',
@@ -48,7 +56,7 @@ const props = defineProps({
// Generate internal ref tied to modelValue prop
const emit = defineEmits(['update:modelValue']);
-const model = useObjectModel(() => props.modelValue, emit, 'modelValue');
+const model = useObjectModel<Pace>(() => props.modelValue, (x) => emit('update:modelValue', x));
</script>
<style scoped>
diff --git a/src/components/RaceOptions.vue b/src/components/RaceOptions.vue
@@ -17,16 +17,25 @@
</div>
</template>
-<script setup>
+<script setup lang="ts">
+import type { PropType } from 'vue';
+
+import { RacePredictionModel } from '@/utils/races';
+
import DecimalInput from '@/components/DecimalInput.vue';
import useObjectModel from '@/composables/useObjectModel';
+interface RaceOptions {
+ model: RacePredictionModel,
+ riegelExponent: number,
+}
+
const props = defineProps({
/**
* The component value
*/
modelValue: {
- type: Object,
+ type: Object as PropType<RaceOptions>,
default: () => ({
model: 'AverageModel',
riegelExponent: 1.06,
@@ -36,5 +45,6 @@ const props = defineProps({
// Generate internal ref tied to modelValue prop
const emit = defineEmits(['update:modelValue']);
-const model = useObjectModel(() => props.modelValue, emit, 'modelValue');
+const model = useObjectModel<RaceOptions>(() => props.modelValue,
+ (x) => emit('update:modelValue', x));
</script>
diff --git a/src/components/SplitOutputTable.vue b/src/components/SplitOutputTable.vue
@@ -75,7 +75,7 @@ const props = defineProps({
// Generate internal ref tied to modelValue prop
const emit = defineEmits(['update:modelValue']);
-const model = useObjectModel(() => props.modelValue, emit, 'modelValue');
+const model = useObjectModel(() => props.modelValue, (x) => emit('update:modelValue', x));
/**
* The target table results
diff --git a/src/components/TargetEditor.vue b/src/components/TargetEditor.vue
@@ -145,7 +145,7 @@ const props = defineProps({
const emit = defineEmits(['close', 'revert', 'update:modelValue']);
// Generate internal ref tied to modelValue prop
-const model = useObjectModel(() => props.modelValue, emit, 'modelValue');
+const model = useObjectModel(() => props.modelValue, (x) => emit('update:modelValue', x));
/**
* Add a new distance based target
diff --git a/src/components/TargetSetSelector.vue b/src/components/TargetSetSelector.vue
@@ -74,7 +74,7 @@ const props = defineProps({
// Generate internal ref tied to modelValue prop
const emit = defineEmits(['update:targetSets']);
-const targetSets = useObjectModel(() => props.targetSets, emit, 'targetSets');
+const targetSets = useObjectModel(() => props.targetSets, (x) => emit('update:targetSets', x));
/**
* The dialog element
@@ -115,10 +115,13 @@ function editTargetSet() {
* Create and select a new target
*/
function newTargetSet() {
- let key = Date.now().toString();
- targetSets.value[key] = {
- name: 'New target set',
- targets: [],
+ const key = Date.now().toString();
+ targetSets.value = {
+ ...targetSets.value,
+ [key]: {
+ name: 'New target set',
+ targets: [],
+ },
};
model.value = key;
editTargetSet();
diff --git a/src/components/TimeInput.vue b/src/components/TimeInput.vue
@@ -13,7 +13,7 @@
</div>
</template>
-<script setup>
+<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import IntegerInput from '@/components/IntegerInput.vue';
@@ -25,7 +25,7 @@ import DecimalInput from '@/components/DecimalInput.vue';
const model = defineModel({
type: Number,
default: 0,
- validator(value) {
+ validator(value: number) {
return value >= 0 && value <= 359999.99;
},
});
@@ -99,7 +99,7 @@ const seconds = computed({
/**
* Update the internal value when the component value changes
*/
-watch(model, (newValue) => {
+watch(model, (newValue: number) => {
if (newValue !== internalValue.value) {
internalValue.value = newValue;
}
@@ -108,7 +108,7 @@ watch(model, (newValue) => {
/**
* Update the component value when the internal value changes
*/
-watch(internalValue, (newValue) => {
+watch(internalValue, (newValue: number) => {
model.value = newValue;
});
@@ -116,7 +116,7 @@ watch(internalValue, (newValue) => {
* Process up and down arrow presses
* @param {Object} e The keydown event args
*/
-function onkeydown(e, step = 1) {
+function onkeydown(e: KeyboardEvent, step: number = 1) {
if (e.key === 'ArrowUp') {
if (Math.floor(internalValue.value) + step > max.value) {
internalValue.value = max.value;
diff --git a/src/composables/useObjectModel.ts b/src/composables/useObjectModel.ts
@@ -6,32 +6,29 @@ import { deepCopy, deepEqual } from '@/utils/misc';
/*
* Generate an internal ref that implements support for v-model with objects
* @param {Function} prop A function returning the prop
- * @param {Function} emit The emit function
- * @param {string} name The name of the v-model prop
+ * @param {Function} emit A function for emitting update events
* @returns {Ref<object>} The internal ref
*/
-export default function defineObjectModel(prop: () => Ref<object>,
- emit: (x: string, y: object) => void,
- name: string): Ref<object> {
+export default function defineObjectModel<T>(prop: () => T, emit: (x: T) => void): Ref<T> {
/**
* The internal value
*/
- const internalValue = ref(deepCopy(prop()));
+ const internalValue: Ref<T> = ref<T>(prop()) as Ref<T>;
/**
* Update the internal value when the component value changes
*/
- watch(prop, (newValue: object) => {
- if (!deepEqual(internalValue.value, newValue)) {
- internalValue.value = deepCopy(newValue);
+ watch(prop, (newValue: T) => {
+ if (!deepEqual<T>(internalValue.value, newValue)) {
+ internalValue.value = deepCopy<T>(newValue);
}
}, { deep: true });
/**
* Update the component value when the internal value changes
*/
- watch(internalValue, (newValue: object) => {
- emit(`update:${name}`, deepCopy(newValue));
+ watch(internalValue, (newValue: T) => {
+ emit(deepCopy<T>(newValue));
}, { deep: true });
return internalValue;
diff --git a/src/utils/misc.ts b/src/utils/misc.ts
@@ -1,18 +1,18 @@
/**
* Create a deep copy of an object
- * @param {object} value The object to copy
- * @returns {object} The copied object
+ * @param {Type} value The object to copy
+ * @returns {Type} The copied object
*/
-export function deepCopy(value: object): object {
+export function deepCopy<Type>(value: Type): Type {
return JSON.parse(JSON.stringify(value));
}
/**
* Test whether two objects are deeply equal
- * @param {object} value1 The first object
- * @param {object} value2 The second object
+ * @param {Type} value1 The first object
+ * @param {Type} value2 The second object
* @returns {boolean} Whether the two objects are equal
*/
-export function deepEqual(value1: object, value2: object): boolean {
+export function deepEqual<Type>(value1: Type, value2: Type): boolean {
return JSON.stringify(value1) === JSON.stringify(value2);
}
diff --git a/src/utils/units.ts b/src/utils/units.ts
@@ -1,21 +1,21 @@
-export const enum TIME_UNIT_KEYS {
+export enum TIME_UNIT_KEYS {
seconds = 'seconds',
minutes = 'minutes',
hours = 'hours',
}
-export const enum DISTANCE_UNIT_KEYS {
+export enum DISTANCE_UNIT_KEYS {
meters = 'meters',
yards = 'yards',
kilometers = 'kilometers',
miles = 'miles',
marathons = 'marathons',
}
-export const enum SPEED_UNIT_KEYS {
+export enum SPEED_UNIT_KEYS {
meters_per_second = 'meters_per_second',
kilometers_per_hour = 'kilometers_per_hour',
miles_per_hour = 'miles_per_hour',
}
-export const enum PACE_UNIT_KEYS {
+export enum PACE_UNIT_KEYS {
seconds_per_meter = 'seconds_per_meter',
time_per_kilometer = 'seconds_per_kilometer',
time_per_mile = 'seconds_per_mile',