Angular ve TypeScript'te Convention: Style Guide, Prettier ve ESLint'in Sorumluluk Paylaşımı
Angular projesinde biçim Prettier'ın, adlandırma ve selector kuralları ESLint'in olmalı ve ikisinin arasındaki sınırı eslint-config-prettier çizer: o olmadan kopyalanmış iki @stylistic kuralı Prettier'dan geçmiş kodda 110 hata üretti. Yazı Angular 22 Style Guide'ın kararlarını, eski projelerde tip son ekli adlandırmayı, yeni projede Prettier'ın 7 dosya bulmasını, naming-convention kuralını ve ESLint 10'un .eslintrc'yi yok saymasını gösteriyor.
Sürüm notu: Bu yazıdaki kodlar Angular 22.2 (CLI 22.2.7, TypeScript 6.0), ESLint 10.15, typescript-eslint 8.71, angular-eslint 22.6, Prettier 3.10.1 ve eslint-config-prettier 10.1.8 ile Node.js 24 üzerinde denenmiştir.
Önce Tahmin Et: Yeni Projede Prettier Kaç Dosya Bulur?
Bookstore'un web arayüzü için sıfırdan bir Angular 22 projesi açıyoruz. Angular CLI bu sürümde projeye Prettier'ı devDependency olarak ve bir .prettierrc dosyasını hazır koyuyor:
$ npx @angular/cli@22.2.7 new bookstore-web --defaults --skip-git --ssr=false --zoneless
$ cat bookstore-web/.prettierrc
{
"printWidth": 100,
"singleQuote": true,
"overrides": [
{
"files": "*.html",
"options": {
"parser": "angular"
}
}
]
}
Ardından ng add angular-eslint ile ESLint'i ekliyoruz ve tek satır kod yazmadan, aracın kendi ürettiği dosyalar üzerinde npx prettier --check . çalıştırıyoruz. Prettier'ın ayarını CLI yazdı, dosyaları da CLI yazdı. Kaç dosya "formatlanmamış" çıkar?
Angular Style Guide'ın Ana Kararları
Angular tarafında convention'ın tek resmî kaynağı angular.dev'deki Angular Style Guide. Guide kendini zorunlu değil önerilen bir set olarak tanımlıyor, genel TypeScript kuralları için Google'ın TypeScript stil rehberine yönlendiriyor ve uygulama kodunun Angular'a özgü kısmıyla ilgileniyor. Sayfada güncellenme tarihi yok; okuduğumuz hâli Angular 22 dokümanı. Bookstore'u etkileyen kararları kendi cümlelerimizle anlatalım.
Dosya adları tire ile ayrılır ve dosyanın içindeki ana TypeScript adıyla eşleşir: BookList class'ı book-list.ts'te durur. Test dosyası aynı adı alır ve .spec.ts ile biter. helpers, utils, common gibi her şeyi içine alabilen adlardan kaçınılır. Bir component'in .ts, .html ve .css dosyaları aynı temel adı paylaşır.
Klasörler dosya tipine göre değil feature'a göre kurulur: components/, services/ klasörleri yerine features/catalog/, features/cart/. Testler test ettikleri kodun yanında durur; her dosya mümkünse tek bir kavram taşır.
Bağımlılıklar constructor parametresiyle değil inject() fonksiyonuyla alınır. Angular'a özgü property'ler (inject edilenler, input'lar, output'lar, query'ler) method'lardan önce gruplanır. Component sunuma odaklanır; doğrulama ve veri dönüşümü ayrı koda gider. Template'te yalnız basit ifadeler durur, karmaşık mantık TypeScript'e, çoğunlukla bir computed'a taşınır. Yalnız template'in kullandığı üyeler protected, Angular'ın başlattığı input, output, model ve query'ler readonly olur. NgClass ve NgStyle yerine class ve style binding'leri tercih edilir. Event handler'lar tetikleyen event'e göre değil yaptıkları işe göre adlandırılır: onClick değil addToCart. Lifecycle hook'ları kısa tutulur, iyi adlandırılmış method'ları çağırır.
En görünür değişiklik dosya adlarında. Angular 20 ile style guide ve CLI, dosya adındaki tip son ekini bıraktı; Angular serisinin, signal API değişikliklerini de anlatan "Angular 20: Stable Signal API'leri, httpResource ve Yeni Dosya Adlandırma" yazısında bunu görmüştük. Angular 22 CLI'ının bugünkü çıktısı:
$ npx ng g component features/catalog/book-list
CREATE src/app/features/catalog/book-list/book-list.css (0 bytes)
CREATE src/app/features/catalog/book-list/book-list.spec.ts (547 bytes)
CREATE src/app/features/catalog/book-list/book-list.ts (196 bytes)
CREATE src/app/features/catalog/book-list/book-list.html (24 bytes)
$ npx ng g service features/catalog/book-catalog
CREATE src/app/features/catalog/book-catalog.spec.ts (346 bytes)
CREATE src/app/features/catalog/book-catalog.ts (82 bytes)
Component class'ı BookList, service class'ı BookCatalog; Component ve Service son ekleri yok. Angular 22 CLI'ı service'i bu sürümde @Service() decorator'ıyla üretti; bu decorator'ın DI (Dependency Injection; bağımlılıkların class'a dışarıdan verilmesi) davranışı bu yazının konusu değil, burada bizi ilgilendiren adlandırma. Bu kuralları uygulayan Bookstore component'i:
import { Component, computed, inject, input, output } from '@angular/core';
import { Book } from '../book';
import { BookCatalog } from '../book-catalog';
@Component({
selector: 'bs-book-list',
templateUrl: './book-list.html',
styleUrl: './book-list.css',
})
export class BookList {
private readonly catalog = inject(BookCatalog);
readonly maxPrice = input(1000);
readonly bookAdded = output<Book>();
protected readonly books = computed(() =>
this.catalog.findAll().filter((book) => book.price <= this.maxPrice()),
);
protected addToCart(book: Book): void {
this.bookAdded.emit(book);
}
}
Filtre template'te değil computed'da; template yalnız books()'u dolaşıyor. books ve addToCart yalnız template'ten kullanıldığı için protected, selector'da Bookstore'un bs öneki var.
Eski Projelerle Bir Arada Yaşamak
Angular 20 öncesinde açılmış bir projede yüzlerce *.component.ts ve *.service.ts dosyası var. Hepsini yeniden adlandırmak hem riskli hem de git blame geçmişini böler. Bu durumda yeni kuralı yalnız yeni koda uygulamak yerine, CLI'ı eski kurala göre üretmeye ayarlamak daha tutarlı olur. Component, directive ve service schematic'lerinin type, guard, pipe, interceptor, resolver ve module schematic'lerinin typeSeparator seçeneği var. angular.json'da proje altına:
"schematics": {
"@schematics/angular:component": { "type": "component" },
"@schematics/angular:service": { "type": "service" },
"@schematics/angular:guard": { "typeSeparator": "." }
}
$ npx ng g component features/cart/cart-summary --dry-run
CREATE src/app/features/cart/cart-summary/cart-summary.component.css (0 bytes)
CREATE src/app/features/cart/cart-summary/cart-summary.component.spec.ts (632 bytes)
CREATE src/app/features/cart/cart-summary/cart-summary.component.ts (236 bytes)
CREATE src/app/features/cart/cart-summary/cart-summary.component.html (27 bytes)
$ npx ng g guard features/cart/checkout --dry-run
CREATE src/app/features/cart/checkout.guard.spec.ts (476 bytes)
CREATE src/app/features/cart/checkout.guard.ts (132 bytes)
Bu ayar olmadan aynı guard checkout-guard.ts olarak üretiliyor (varsayılan ayırıcı tire). Hangi yolu seçtiğin daha az önemli; önemli olan kararın angular.json'da yazılı olması ve repo'da iki adlandırmanın rastgele karışmaması. Geçiş yapacaksan bunu tek bir PR'da, başka değişiklik olmadan yap; beşinci yazıdaki .git-blame-ignore-revs tekniği burada da işe yarar.
Prettier: Config, Ignore ve Template'ler
TypeScript, HTML (HyperText Markup Language), CSS, SCSS (Sass'ın CSS uyumlu syntax'ı) ve JSON (JavaScript Object Notation) dosyalarının biçimi Bookstore'da Prettier'ın. Prettier de CSharpier gibi opinionated: dokümanındaki option philosophy sayfası mevcut seçeneklerin bir kısmını tarihsel kalıntı olarak niteliyor, yeni seçenek taleplerini kabul etmediklerini söylüyor ve gerekçe olarak her yeni seçeneğin tartışmayı yalnızca seçeneğin değerine taşıdığını gösteriyor. "Why Prettier" sayfasının özeti de bu: aracı benimsemenin asıl sebebi stil tartışmalarını bitirmek.
CLI'ın ürettiği .prettierrc'teki overrides bölümü önemli: *.html dosyaları angular parser'ıyla okunur, böylece Prettier @for, @if ve {{ }} gibi Angular syntax'ını anlar. Bookstore'un tek satıra sıkışmış bir template'i:
<ul>@for (book of books(); track book.isbn) {<li>{{book.title}} ({{book.price}} TRY) <button type="button" (click)="addToCart(book)" [disabled]="!book.inStock">Add to cart</button></li>}</ul>
$ npx prettier --write src/app/features/catalog/book-list/book-list.html
src/app/features/catalog/book-list/book-list.html 22ms
<ul>
@for (book of books(); track book.isbn) {
<li>
{{ book.title }} ({{ book.price }} TRY)
<button type="button" (click)="addToCart(book)" [disabled]="!book.inStock">
Add to cart
</button>
</li>
}
</ul>
Prettier node_modules'u atlar ve çalıştırıldığı klasörde bir .gitignore varsa onun kurallarına da uyar. Repo'da tutulan ama formatlanmaması gereken dosyalar için .prettierignore yazılır; Bookstore'da bu bir kod üreticinin çıktısı. Ignore dosyası olmadan src/app/api/generated/payment-api.ts uyarı verdi, ekledikten sonra kontrol temiz geçti:
# Kod üretici çıktısı
src/app/api/generated/
Şimdi tahminin cevabı. Hiç kod yazmadan, ng new ve ng add angular-eslint sonrası:
$ npx prettier --check .
Checking formatting...
[warn] src/app/app.config.ts
[warn] src/app/app.html
[warn] src/app/app.spec.ts
[warn] src/index.html
[warn] src/main.ts
[warn] tsconfig.app.json
[warn] tsconfig.spec.json
[warn] Code style issues found in 7 files. Run Prettier with --write to fix.
Yedi dosya. CLI'ın template'leri Prettier'ın çıktısıyla birebir aynı değil; örneğin main.ts'teki zincir iki satıra bölünmüş, Prettier ise 100 karakter sınırına sığdığı için tek satıra alıyor, tsconfig.app.json'daki tek element'li array'leri de tek satıra topluyor. Sonuç basit: yeni projenin ilk işlerinden biri npx prettier --write . çalıştırıp ayrı bir commit atmak. Bunu yapmazsan ilk feature PR'ında bu yedi dosya da diff'e girer.
Formatting Neden ESLint'te Değil?
ESLint uzun yıllar hem kalite hem biçim kuralları taşıdı: indent, quotes, semi, max-len. Bu kurallar ESLint 8.53.0'da deprecated oldu ve ESLint 10.15'te hâlâ yerinde duruyor; kurulu paketteki kural meta verisi her biri için deprecatedSince: "8.53.0" diyor. typescript-eslint ise kendi biçim kurallarını önce deprecated yaptı, v8'de tamamen kaldırdı: kurulu 8.71 sürümünde indent, quotes, semi ve member-delimiter-style kuralları yok. Bu kurallar topluluk tarafından sürdürülen ESLint Stylistic projesine (@stylistic/eslint-plugin) taşındı.
typescript-eslint'in "What About Formatting?" sayfası gerekçeyi açıkça anlatıyor. Linter kuralları birbirinden bağımsız çalışır, bir kuralın düzeltmesi bir başkasını bozabilir ve sonuca ulaşmak için birden fazla geçiş gerekebilir; özellikle tip bilgili lint'te bu yavaşlık hissedilir. Formatter ise kodu var olan düzenden bağımsız olarak baştan yazar, daha tutarlı ve daha hızlıdır. Öneri: biçimi Prettier gibi bir formatter'a bırak, ESLint'i kod kalitesi ve convention kuralları için kullan, üçüncü parti configuration'ların açtığı biçim kurallarını da eslint-config-prettier ile kapat.
Bir isim karışıklığına dikkat: ESLint config'indeki tseslint.configs.stylistic bir formatting seti değil. Biçimle değil yazım tercihiyle ilgili kuralları açar; örneğin interface ile type alias arasında tutarlılık. Prettier ile çakışmaz. Prettier kullanmayan bir ekip için @stylistic/eslint-plugin meşru bir alternatif, ama o zaman biçim ESLint'in sahipliğine geçer ve ikisini birlikte açmamak gerekir.
Convention Niteliğindeki ESLint Kuralları
ESLint'in yüzlerce kuralının çoğu kod kalitesiyle ilgili ve ilerideki Statik Analiz serisinin konusu. Convention'a, yani adlandırma ve yapı kararlarına doğrudan dokunan iki kural ailesi ise bu serinin. Birincisi @typescript-eslint/naming-convention. Bookstore'un TypeScript tarafındaki kararları: varsayılan camelCase, tipler PascalCase, const değişkenler camelCase ya da UPPER_CASE, enum üyeleri PascalCase ve interface'ler I öneki olmadan.
Son karar C# tarafının tersi ve bilinçli. İkinci yazıda C#'ta interface'lere I önekini zorladık, çünkü .NET'in kendi convention'ı bu. TypeScript dünyasında Google'ın stil rehberi ve Angular'ın kendi kodu interface'lere önek koymuyor. Her dil kendi topluluğunun convention'ını izler; iki dili birden yazan ekip için bu fark convention dokümanına bir satır olarak girer. Bilerek bozulmuş bir model dosyasıyla ng lint çıktısı:
export interface IBook {
isbn: string;
title: string;
price: number;
in_stock: boolean;
}
export enum book_format {
HARDCOVER,
Paperback,
}
export const MAX_CART_ITEMS = 20;
export const DefaultCurrency = 'TRY';
export function isAffordable(book: IBook, Budget: number): boolean {
return book.price <= Budget;
}
$ npx ng lint
src/app/features/catalog/book.ts
1:18 error Interface name `IBook` must not match the RegExp: /^I[A-Z]/u @typescript-eslint/naming-convention
5:3 error Type Property name `in_stock` must match one of the following formats: camelCase @typescript-eslint/naming-convention
8:13 error Enum name `book_format` must match one of the following formats: PascalCase @typescript-eslint/naming-convention
14:14 error Variable name `DefaultCurrency` must match one of the following formats: camelCase, UPPER_CASE @typescript-eslint/naming-convention
16:43 error Parameter name `Budget` must match one of the following formats: camelCase @typescript-eslint/naming-convention
Bir ayrıntı: HARDCOVER enum üyesi hata vermedi. Tek kelimelik, tamamı büyük harf bir ad kuralın PascalCase kontrolünden geçiyor; HARD_COVER yazılsaydı yakalanırdı. Araç biçimi denetler ve biçimin sınırlarında boşluklar kalır; ilk yazıda dediğimiz gibi naming'in son sözü review'da.
İkinci aile angular-eslint'in selector kuralları. CLI'ın ürettiği configuration'da önek app; Bookstore'un kararı bs. angular.json'daki prefix değerini ve ESLint kurallarındaki prefix'i değiştirdikten sonra, eski önekle kalmış iki component:
$ npx ng lint
src/app/app.ts
6:13 error The selector should start with one of these prefixes: "bs" (https://angular.dev/style-guide#choosing-component-selectors) @angular-eslint/component-selector
src/app/features/catalog/book-list/book-list.ts
5:13 error The selector should start with one of these prefixes: "bs" (https://angular.dev/style-guide#choosing-component-selectors) @angular-eslint/component-selector
✖ 2 problems (2 errors, 0 warnings)
İki yer de değişmeli: angular.json'daki önek CLI'ın yeni üreteceği component'leri, ESLint kuralı ise var olanları belirliyor. Yalnız birini değiştirirsen ya CLI yanlış önek üretir ya da lint eskileri yakalamaz.
ESLint 10 Flat Config: Minimal Kurulum
ng add angular-eslint bir eslint.config.js üretiyor; Bookstore'un convention kuralları ve eslint-config-prettier eklenmiş son hâli (Prettier'dan geçmiş olarak):
// @ts-check
const eslint = require('@eslint/js');
const { defineConfig } = require('eslint/config');
const tseslint = require('typescript-eslint');
const angular = require('angular-eslint');
const eslintConfigPrettier = require('eslint-config-prettier/flat');
module.exports = defineConfig([
{
files: ['**/*.ts'],
extends: [
eslint.configs.recommended,
tseslint.configs.recommended,
tseslint.configs.stylistic,
angular.configs.tsRecommended,
],
processor: angular.processInlineTemplates,
rules: {
'@typescript-eslint/naming-convention': [
'error',
{ selector: 'default', format: ['camelCase'] },
{ selector: 'import', format: null },
{ selector: 'variable', modifiers: ['const'], format: ['camelCase', 'UPPER_CASE'] },
{ selector: 'typeLike', format: ['PascalCase'] },
{
selector: 'interface',
format: ['PascalCase'],
custom: { regex: '^I[A-Z]', match: false },
},
{ selector: 'enumMember', format: ['PascalCase'] },
],
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'bs',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'bs',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.html'],
extends: [angular.configs.templateRecommended, angular.configs.templateAccessibility],
rules: {},
},
eslintConfigPrettier,
]);
Flat config bir array; sonraki element öncekileri ezer. Bu yüzden eslintConfigPrettier en sonda: kendisinden önce açılmış bütün biçim kurallarını kapatır. Flat config için paketin eslint-config-prettier/flat girişi kullanılıyor. Kalite kurallarını hangi seviyede açacağın, tip bilgili lint ve angular.configs setlerinin içeriği Statik Analiz serisinin işi; burada yalnız convention kurallarını ve Prettier'la sınırı kurduk. Bu configuration'la ng lint "All files pass linting." ve prettier --check . "All matched files use Prettier code style!" dedi.
Sürüm notunda TypeScript 6.0 yazmasının sebebi de burada: Angular 22.2'nin derleyicisi peer dependency olarak >=6.0 <6.1 istiyor, typescript-eslint 8.71 de <6.1.0. Go ile yeniden yazılmış TypeScript 7 npm'de yayımda, ama bu araç zinciri bugün 6.0 üzerinde çalışıyor.
Sık Düşülen Tuzaklar
eslint-config-prettier olmadan biçim kuralı açmak. Eski bir projeden kopyalanan iki satırı (@stylistic/quotes double, @stylistic/indent 4) Prettier'dan geçmiş Bookstore koduna ekledik. eslint src 110 error verdi, hepsi --fix ile düzeltilebilir. --fix sonrası ESLint temizdi ama bu kez prettier --check src on dosyayı formatlanmamış buldu; git diff --shortstat: 93 ekleme, 93 silme. Üçüncü yazıdaki format çatışmasının TypeScript versiyonu. Çakışmayı paketin kendi CLI'ı da gösteriyor:
$ npx eslint-config-prettier src/app/app.ts
The following rules are unnecessary or might conflict with Prettier:
- @stylistic/indent
The following rules are enabled but cannot be automatically checked. See:
https://github.com/prettier/eslint-config-prettier#special-rules
- @stylistic/quotes
- no-unexpected-multiline
eslintConfigPrettier'ı array'in sonuna ekleyince aynı komut "No rules that are unnecessary or conflict with Prettier were found." dedi, ESLint ve Prettier ikisi de temiz döndü. Bu komutu CI pipeline'ında bir kez çalıştırmak, birinin ileride biçim kuralı eklemesine karşı ucuz bir sigorta.
Eski .eslintrc örneklerini ESLint 10'a kopyalamak. İnternetteki Angular ESLint örneklerinin büyük kısmı .eslintrc.json ve "extends": ["plugin:@angular-eslint/recommended", "prettier"] biçiminde. ESLint 10 bu dosyaya hiç bakmıyor:
$ npx eslint src
Oops! Something went wrong! :(
ESLint: 10.15.0
ESLint couldn't find an eslint.config.* file.
From ESLint v9.0.0, the default configuration file is now eslint.config.*.
If you are using a .eslintrc.* file, please follow the migration guide
to update your configuration file to the new format:
https://eslint.org/docs/latest/use/configure/migration-guide
ESLint 9'daki ESLINT_USE_FLAT_CONFIG=false ile eski config'e dönme yolunu da denedik; ESLint 10 değişkeni yok sayıp aynı hatayı verdi. ng lint ise daha kısa konuştu: "Could not find config file." Eski configuration taşınırken "prettier" girdisi eslint-config-prettier/flat import'una, overrides blokları files alanlı config object'lerine dönüşür.
Prettier sürümünü sabitlememek. CLI'ın yazdığı "prettier": "^3.8.1" caret aralığı, yeni bir minor sürüm çıktığında farklı makinelerin farklı Prettier kurmasına izin verir; lock dosyası bunu kısmen önler ama npm install ile güncellenir. Prettier'ın kurulum dokümanı tam sürümle (--save-exact) kurmayı öneriyor; CSharpier'daki manifest sabitlemesinin karşılığı bu.
Style guide'ı lint kuralı sanmak. "Handler'ı eyleme göre adlandır", "template'te karmaşık mantık olmasın", "component sunuma odaklansın" gibi kararların çoğunun bir ESLint kuralı yok. Bunlar review'da yaşar; convention dokümanında da "review" olarak işaretlenmeli.
Küçük Bir Deneme
★☆☆ Kendi Angular projende npx prettier --check . çalıştır. Hiç formatlanmamış dosya çıkmıyorsa .prettierrc'teki printWidth'i 80'e çek ve kaç dosyanın etkilendiğini say; sonra geri al.
★★☆ @typescript-eslint/naming-convention'a boolean değişkenlerin is, has, can önekleriyle başlamasını isteyen bir kural ekle (types: ['boolean'] seçeneği tip bilgili lint ister; hata mesajı sana ne yapman gerektiğini söyleyecek). Kaç ihlal çıktı, hangileri gerçekten kötü isimdi?
★★★ Projene geçici olarak bir @stylistic biçim kuralı ekle, npx eslint-config-prettier ile çakışmayı gör, sonra eslintConfigPrettier'ı array'de sona değil başa koyup aynı komutu çalıştır. Sıranın neden önemli olduğunu çıktıyla açıkla.
Artık iki tarafta da kurallar dosyalarda ve araçlar onları uyguluyor. Ama araçlar ancak çalıştırıldıklarında işe yarar. Serinin son yazısında bu kuralları otomatikleştireceğiz: pre-commit hook'ları, CI kapıları, legacy bir kod tabanına kademeli geçiş ve büyük formatlama commit'ini git blame'den saklayan .git-blame-ignore-revs.