Skip to main content

JavaScript Guidelines

Attribution

These guidelines are adapted from spatie.be/guidelines/javascript and tailored for the Saucebase stack (Vue 3, TypeScript, ESLint, Prettier).

Enforced automatically

Many of these rules are already enforced by the project's ESLint and Prettier configuration. Run npm run lint and npm run format to auto-fix issues before committing.

Prettier Configuration​

The project's .prettierrc enforces:

{
"singleQuote": true,
"tabWidth": 4,
"useTabs": false,
"semi": true,
"trailingComma": "all",
"arrowParens": "always",
"bracketSpacing": true,
"endOfLine": "lf"
}

Key points:

  • Single quotes for strings
  • 4-space indentation (not 2, not tabs)
  • 120 character line width — gives room for verbose TypeScript and Vue templates
  • Trailing commas everywhere (including function parameters)
  • Semicolons required

Variable Assignment​

Prefer const over let. Only use let when you know the value will need to change. Never use var.

// Good
const greeting = 'Hello';

// Good — value changes
let count = 0;
count++;

// Bad
var greeting = 'Hello';
let message = 'Static message'; // Should be const

Variable Names​

Variable names should be camelCase. For booleans, prefix with is, has, or can to make intent clear.

// Good
const isLoggedIn = true;
const hasSubscription = false;
const canEditPost = computed(() => user.value.isAdmin);

// Bad
const loggedin = true;
const subscription = false;
const editPost = computed(() => user.value.isAdmin);

Comparisons​

Always use === instead of ==. When checking truthiness for strings, numbers, or arrays, use the explicit comparison rather than relying on type coercion.

// Good
if (value === '') { ... }
if (count === 0) { ... }
if (items.length === 0) { ... }

// Bad
if (value == '') { ... }
if (!count) { ... }
if (!items.length) { ... }

Function Keyword vs Arrow Functions​

Prefer arrow functions for callbacks and composables. Use named function declarations for top-level functions or methods that need to be hoisted.

// Good — arrow for callbacks
const doubled = items.map((item) => item * 2);

// Good — named function for composable
function useCounter() {
const count = ref(0);
const increment = () => count.value++;
return { count, increment };
}

// Bad — unnecessary function keyword in callback
const doubled = items.map(function (item) {
return item * 2;
});

Always include parentheses around arrow function parameters, even for single parameters (enforced by arrowParens: "always").

// Good
const double = (n) => n * 2;

// Bad (will be auto-fixed by Prettier)
const double = n => n * 2;

Object and Array Destructuring​

Use destructuring when you need multiple properties from an object or specific elements from an array.

// Good
const { name, email } = user;
const [first, ...rest] = items;

// Good — destructure in function parameters
function greet({ name, role }) {
return `Hello, ${name} (${role})`;
}

// Acceptable — single property access is fine without destructuring
const name = user.name;

Object Method Shorthand​

Use the shorthand syntax for object methods.

// Good
const obj = {
name: 'Saucebase',
greet() {
return `Hello from ${this.name}`;
},
};

// Bad
const obj = {
name: 'Saucebase',
greet: function () {
return `Hello from ${this.name}`;
},
};

Template Literals​

Use template literals instead of string concatenation when embedding variables.

// Good
const message = `Hello, ${user.name}! You have ${count} notifications.`;

// Bad
const message = 'Hello, ' + user.name + '! You have ' + count + ' notifications.';

TypeScript​

Type Inference​

Don't annotate a type that TypeScript can infer. Add explicit types only where they add clarity or where inference fails.

// Good — TypeScript infers string[]
const names = ['Alice', 'Bob'];

// Good — explicit return type adds clarity on public API
function getUser(id: number): Promise<User> { ... }

// Bad — redundant annotation
const names: string[] = ['Alice', 'Bob'];

Interfaces vs Types​

Prefer interface for object shapes and type for unions, intersections, or aliases.

// Good
interface User {
id: number;
name: string;
email: string;
}

type Status = 'active' | 'inactive' | 'pending';
type UserWithStatus = User & { status: Status };

Enums​

Avoid TypeScript enum. Prefer const objects with as const or string union types for better tree-shaking and compatibility.

// Good
const Role = {
Admin: 'admin',
User: 'user',
} as const;
type Role = (typeof Role)[keyof typeof Role];

// Also good for simple cases
type Role = 'admin' | 'user';

// Avoid
enum Role {
Admin = 'admin',
User = 'user',
}

Vue Components​

Composition API​

Always use <script setup> with the Composition API. Do not use Options API for new components.

<!-- Good -->
<script setup lang="ts">
const props = defineProps<{ title: string }>();
const emit = defineEmits<{ close: [] }>();
</script>

<!-- Bad -->
<script lang="ts">
export default {
props: { title: String },
methods: { ... },
};
</script>

Component Names​

Component file names should use PascalCase. Single-instance components (used once per page) should be prefixed with The: TheHeader.vue, TheSidebar.vue.

Props and Emits​

Always type props and emits using TypeScript generics in defineProps and defineEmits.

<script setup lang="ts">
interface Props {
title: string;
count?: number;
items: string[];
}

const props = defineProps<Props>();
const emit = defineEmits<{
update: [value: string];
close: [];
}>();
</script>