Instruction file imported from JDoro/github-copilot-examples (
.github/instructions/typescript.instructions.md). Copyright stays with the author.
TypeScript Copilot Instructions
This instruction set is tailored to help GitHub Copilot generate TypeScript code following best practices and modern TypeScript patterns.
This is an extension of the .github/copilot-instructions.md file.
Coding Standards
Naming Conventions
- Use PascalCase for type aliases, interfaces, classes, and enums (e.g.,
UserProfile,ApiResponse,HttpClient). - Use camelCase for variables, functions, and methods (e.g.,
userName,fetchData,calculateTotal). - Use UPPER_SNAKE_CASE for constants and enum values (e.g.,
MAX_RETRY_COUNT,API_BASE_URL). - Prefix interface names with "I" only when necessary to distinguish from classes; generally avoid the prefix in modern TypeScript.
- Use descriptive names for generic type parameters: prefer
TData,TError,TContextover single letters likeT,U,Vwhen the context is clear. - Name type guard functions with "is" prefix (e.g.,
isString,isUser,isApiError). - Suffix discriminated union types with their discriminant field (e.g.,
type Shape = Circle | Square | Triangle).
Type Annotations
- Always use explicit return types for public functions and methods:
// Good export function calculateSum(a: number, b: number): number { return a + b; } // Avoid export function calculateSum(a: number, b: number) { return a + b; } - Prefer type inference for local variables when the type is obvious from the
initializer:
// Good const userName = 'John Doe'; const count = users.length; // Avoid unnecessary annotations const userName: string = 'John Doe'; - Always annotate function parameters; never rely on implicit
any:// Good function greet(name: string): void { console.log(`Hello, ${name}`); } // Bad function greet(name) { console.log(`Hello, ${name}`); } - Use type annotations for empty arrays and objects to prevent
any[]or{}:const users: User[] = []; const config: Record<string, string> = {};
Interfaces vs Type Aliases
- Prefer interfaces for object shapes that may be extended or implemented:
interface User { id: string; name: string; email: string; } interface AdminUser extends User { permissions: string[]; } - Use type aliases for unions, intersections, primitives, and tuples:
type Status = 'pending' | 'approved' | 'rejected'; type Point = [number, number]; type ApiResponse<T> = { data: T } | { error: string }; - Use type aliases for mapped types and conditional types:
// Custom mapped type example type DeepReadonly<T> = { readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K]; }; // Custom conditional type example type NoUndefined<T> = T extends undefined ? never : T; - For simple object types, prefer interfaces for better error messages and performance.
Type Safety
- Enable strict mode in
tsconfig.jsonand never disable strict checks:{ "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true } } - Never use
anytype; useunknownif the type is truly dynamic and narrow it appropriately:// Good function processValue(value: unknown): string { if (typeof value === 'string') { return value.toUpperCase(); } return String(value); } // Avoid function processValue(value: any): string { return value.toUpperCase(); } - Avoid type assertions (
as) unless absolutely necessary; prefer type guards:// Good function isUser(obj: unknown): obj is User { return ( typeof obj === 'object' && obj !== null && 'id' in obj && 'name' in obj ); } if (isUser(data)) { console.log(data.name); } // Avoid const user = data as User; - Use const assertions for literal types and readonly arrays:
const config = { apiUrl: 'https://api.example.com', timeout: 5000, } as const; const colors = ['red', 'green', 'blue'] as const; type Color = typeof colors[number]; // 'red' | 'green' | 'blue' - Prefer
strictNullChecksto catch null and undefined errors at compile time.
Generics
- Use generics to create reusable, type-safe functions and components:
function identity<T>(value: T): T { return value; } function mapArray<T, U>(arr: T[], fn: (item: T) => U): U[] { return arr.map(fn); } - Add constraints to generics when necessary:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } function merge<T extends object, U extends object>(obj1: T, obj2: U): T & U { return { ...obj1, ...obj2 }; } - Use default type parameters when appropriate:
interface ApiResponse<T = unknown> { data: T; status: number; } - Avoid over-constraining generics; keep them as general as possible while still being type-safe.
Utility Types
- Leverage built-in utility types instead of creating custom ones:
Partial<T>- makes all properties optionalRequired<T>- makes all properties requiredReadonly<T>- makes all properties readonlyPick<T, K>- picks specific propertiesOmit<T, K>- omits specific propertiesRecord<K, T>- creates an object type with specific keysExclude<T, U>- excludes types from a unionExtract<T, U>- extracts types from a unionNonNullable<T>- removes null and undefinedReturnType<T>- gets function return typeParameters<T>- gets function parameter types
type UserUpdate = Partial<User>; type UserKeys = keyof User; type UserName = Pick<User, 'name' | 'email'>; type UserWithoutId = Omit<User, 'id'>; - Create custom utility types for project-specific patterns:
type DeepPartial<T> = { [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K]; }; type Nullable<T> = T | null; // Use Awaited<T> to unwrap Promise types type UnwrapPromise<T> = Awaited<T>;
Enums vs Union Types
- Prefer const string unions over enums for better type safety and tree-
shaking:
// Good type Status = 'pending' | 'approved' | 'rejected'; // Use enums only when you need reverse mapping or specific use cases enum Direction { Up = 'UP', Down = 'DOWN', Left = 'LEFT', Right = 'RIGHT', } - Use const enums sparingly and only when performance is critical:
const enum LogLevel { Debug = 0, Info = 1, Warning = 2, Error = 3, } - When using enums, always use string enums for better debugging:
enum HttpMethod { Get = 'GET', Post = 'POST', Put = 'PUT', Delete = 'DELETE', }
Type Guards
- Always create type guards for runtime type checking:
function isString(value: unknown): value is string { return typeof value === 'string'; } function isUser(obj: unknown): obj is User { return ( typeof obj === 'object' && obj !== null && 'id' in obj && 'name' in obj && 'email' in obj ); } function isArrayOf<T>( arr: unknown, guard: (item: unknown) => item is T ): arr is T[] { return Array.isArray(arr) && arr.every(guard); } - Use the
inoperator for discriminated unions:type Success = { success: true; data: string }; type Failure = { success: false; error: string }; type Result = Success | Failure; function handleResult(result: Result): void { if ('data' in result) { console.log(result.data); } else { console.error(result.error); } } - Prefer discriminated unions with a common property:
type Shape = | { kind: 'circle'; radius: number } | { kind: 'square'; sideLength: number } | { kind: 'rectangle'; width: number; height: number }; function getArea(shape: Shape): number { switch (shape.kind) { case 'circle': return Math.PI * shape.radius ** 2; case 'square': return shape.sideLength ** 2; case 'rectangle': return shape.width * shape.height; } }
Async/Await and Promises
- Always type Promise return values explicitly:
async function fetchUser(id: string): Promise<User> { const response = await fetch(`/api/users/${id}`); return response.json(); } - Use generic Promise types for better type inference:
function delay<T>(ms: number, value: T): Promise<T> { return new Promise((resolve) => setTimeout(() => resolve(value), ms)); } - Handle errors with proper typing:
async function fetchData(): Promise<Data> { try { const response = await fetch('/api/data'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); } catch (error) { if (error instanceof Error) { console.error('Fetch error:', error.message); } throw error; } }
Error Handling
- Create custom error types for better error handling:
class ApiError extends Error { constructor( message: string, public statusCode: number, public response?: unknown ) { super(message); this.name = 'ApiError'; } } class ValidationError extends Error { constructor(message: string, public fields: Record<string, string>) { super(message); this.name = 'ValidationError'; } } - Use type guards for error handling:
function isApiError(error: unknown): error is ApiError { return error instanceof ApiError; } try { await fetchData(); } catch (error) { if (isApiError(error)) { console.error(`API Error ${error.statusCode}: ${error.message}`); } else if (error instanceof Error) { console.error(error.message); } else { console.error('Unknown error:', error); } } - Never use empty catch blocks; always handle or log errors:
// Good try { await riskyOperation(); } catch (error) { console.error('Operation failed:', error); throw error; // Re-throw if needed } // Bad try { await riskyOperation(); } catch { // Silent failure }
Function Overloads
- Use function overloads for better type inference with multiple signatures:
function createElement(tag: 'div'): HTMLDivElement; function createElement(tag: 'span'): HTMLSpanElement; function createElement(tag: string): HTMLElement; function createElement(tag: string): HTMLElement { return document.createElement(tag); } const div = createElement('div'); // Type: HTMLDivElement - Keep overload signatures before the implementation signature.
- Ensure the implementation signature is compatible with all overload signatures.
Readonly and Immutability
- Use
readonlymodifier for properties that should not be modified:interface Config { readonly apiUrl: string; readonly timeout: number; } class User { constructor( public readonly id: string, public name: string ) {} } - Use
ReadonlyArray<T>orreadonly T[]for arrays that should not be modified:function sum(numbers: readonly number[]): number { return numbers.reduce((acc, n) => acc + n, 0); } - Use
Readonly<T>utility type for readonly objects:type ReadonlyUser = Readonly<User>; - Prefer immutable data structures and operations:
// Good const newArray = [...oldArray, newItem]; const newObject = { ...oldObject, newProp: value }; // Avoid oldArray.push(newItem); oldObject.newProp = value;
Index Signatures and Mapped Types
- Use index signatures for dynamic object keys:
interface StringMap { [key: string]: string; } interface NumberDictionary { [index: string]: number; length: number; // OK, length is a number } - Prefer
Record<K, V>utility type over index signatures:type UserMap = Record<string, User>; - Use mapped types for transforming object types:
type Optional<T> = { [K in keyof T]?: T[K]; }; - Enable
noUncheckedIndexedAccessto make index signatures returnT | undefined:const map: Record<string, string> = {}; const value = map['key']; // Type: string | undefined
Conditional Types
- Use conditional types for advanced type transformations:
// Conditional type returning specific literal types type IsString<T> = T extends string ? 'yes' : 'no'; type Check1 = IsString<string>; // 'yes' type Check2 = IsString<number>; // 'no' type StringOrNumber<T> = T extends string ? string : number; type Flatten<T> = T extends Array<infer U> ? U : T; type ElementType = Flatten<string[]>; // string - Leverage
inferkeyword for extracting types:// Custom types using the infer pattern type PromiseType<T> = T extends Promise<infer U> ? U : T; type ArrayElement<T> = T extends (infer U)[] ? U : never; // Note: TypeScript provides built-in ReturnType<T> and Parameters<T> // to extract function return types and parameters - Use conditional types with mapped types:
type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? never : K; }[keyof T]; type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;
Template Literal Types
- Use template literal types for string manipulation:
type EventName<T extends string> = `on${Capitalize<T>}`; type ClickEvent = EventName<'click'>; // 'onClick' type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'; type ApiEndpoint<T extends string> = `/api/${T}`; type UserEndpoint = ApiEndpoint<'users'>; // '/api/users' - Combine with mapped types for powerful transformations:
type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]; }; type Setters<T> = { [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void; };
Type Narrowing
- Use control flow analysis for type narrowing:
function processValue(value: string | number): void { if (typeof value === 'string') { // value is string here console.log(value.toUpperCase()); } else { // value is number here console.log(value.toFixed(2)); } } - Use truthiness narrowing carefully:
function printLength(str: string | null): void { if (str) { console.log(str.length); } } - Use equality narrowing for literal types:
type Status = 'success' | 'error' | 'pending'; function handleStatus(status: Status): void { if (status === 'success') { // status is 'success' here } } - Use
instanceoffor class narrowing:if (error instanceof ApiError) { console.log(error.statusCode); }
Module Organization
- Use ES modules (
import/export) instead of namespaces:// Good export interface User { id: string; name: string; } export function getUser(id: string): User { // ... } // Avoid namespace UserModule { export interface User { id: string; name: string; } } - Export types and values separately when needed:
export type { User, UserProfile } from './types'; export { getUser, updateUser } from './api'; - Use barrel exports (
index.ts) to simplify imports:// types/index.ts export * from './user'; export * from './product'; export * from './order'; - Prefer named exports over default exports for better refactoring and
tooling support:
// Good export function fetchUser(id: string): Promise<User> { } // Avoid export default function fetchUser(id: string): Promise<User> { }
Declaration Merging
- Use declaration merging to extend interfaces:
interface User { id: string; name: string; } interface User { email: string; } // Merged: User has id, name, and email - Use module augmentation to extend third-party types:
declare module 'express' { interface Request { user?: User; } } - Avoid overusing declaration merging; prefer composition when possible.
Type Assertions vs Type Casting
- Avoid using type assertions (
as) when possible:// Bad const input = document.getElementById('input') as HTMLInputElement; // Good const input = document.getElementById('input'); if (input instanceof HTMLInputElement) { console.log(input.value); } - Use non-null assertion operator (
!) only when you're certain:// Use sparingly const element = document.getElementById('root')!; // Better const element = document.getElementById('root'); if (!element) throw new Error('Root element not found'); - Use
as constfor literal types and readonly assertions:const config = { api: 'https://api.example.com', timeout: 5000, } as const;
Comments and Documentation
- Use JSDoc comments for public APIs and exported functions:
/** * Fetches a user by their ID. * * @param id - The unique identifier of the user * @returns A promise that resolves to the user object * @throws {ApiError} When the API request fails * * @example * ```typescript * const user = await fetchUser('123'); * console.log(user.name); * ``` */ export async function fetchUser(id: string): Promise<User> { // Implementation } - Use
@deprecatedJSDoc tag for deprecated APIs:/** * @deprecated Use `fetchUser` instead. */ export function getUser(id: string): User { // Old implementation } - Document complex types with JSDoc:
/** * Represents a user in the system. */ export interface User { /** Unique identifier for the user */ id: string; /** Full name of the user */ name: string; /** Email address of the user */ email: string; }
Testing
- Use type-safe testing utilities and assertions:
import { expect, test } from 'vitest'; test('fetchUser returns a user', async () => { const user = await fetchUser('123'); expect(user).toMatchObject({ id: expect.any(String), name: expect.any(String), email: expect.any(String), }); }); - Create test-specific types when needed:
type MockUser = Pick<User, 'id' | 'name'>; function createMockUser(overrides?: Partial<MockUser>): MockUser { return { id: '123', name: 'Test User', ...overrides, }; } - Use type guards in tests to verify runtime types:
test('isUser type guard works correctly', () => { expect(isUser({ id: '1', name: 'John', email: 'john@example.com' })).toBe(true); expect(isUser({ id: '1' })).toBe(false); expect(isUser(null)).toBe(false); });
Configuration
- Always use a
tsconfig.jsonfile with strict settings:{ "compilerOptions": { "target": "ES2022", "module": "ESNext", "lib": ["ES2022", "DOM", "DOM.Iterable"], "moduleResolution": "bundler", "strict": true, "noUncheckedIndexedAccess": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "isolatedModules": true, "declaration": true, "declarationMap": true, "sourceMap": true, "outDir": "./dist", "rootDir": "./src" }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } - Use project references for monorepos:
{ "references": [ { "path": "./packages/core" }, { "path": "./packages/utils" } ] }
Performance Considerations
- Avoid complex type computations that slow down the compiler.
- Use type aliases to cache complex type computations:
// Good type ComplexType = SomeComplexComputation<T>; type Result = ComplexType & OtherType; // Avoid recomputing the same type multiple times - Use
skipLibCheckin tsconfig.json to speed up compilation in large projects. - Prefer interfaces over type intersections for object types (better performance).
Best Practices Summary
- Always enable strict mode and recommended strict compiler options.
- Never use
any; useunknownand narrow the type appropriately. - Prefer interfaces for object shapes; use type aliases for unions and complex types.
- Use const assertions for literal types and readonly values.
- Create type guards for runtime type checking.
- Leverage utility types instead of creating custom ones when possible.
- Use generics for reusable, type-safe functions and components.
- Prefer discriminated unions over simple unions for better type narrowing.
- Document public APIs with JSDoc comments.
- Use explicit return types for public functions.
- Prefer readonly and immutable patterns.
- Use template literal types and mapped types for advanced type transformations.
- Organize code with ES modules and barrel exports.
- Test types and type guards to ensure runtime safety.