TypeScript Authoring Skill
Write strictly-typed TypeScript extending javascript-author patterns.
Core Principles
| Principle |
Description |
| Strict Mode |
Always use strict TypeScript configuration |
| Explicit Types |
Prefer explicit over inferred types at boundaries |
| Narrow Types |
Use unions, discriminated unions, and type guards |
| Functional Core |
Same as JavaScript: pure functions, no side effects |
| Named Exports |
Same as JavaScript: no default exports |
Configuration
Strict tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Key Strict Options
| Option |
Purpose |
strict |
Enables all strict checks |
noUncheckedIndexedAccess |
Array/object access returns T | undefined |
noImplicitOverride |
Requires override keyword |
exactOptionalPropertyTypes |
?: means missing, not undefined |
Type Patterns
Interface vs Type
// Use interface for object shapes (extendable)
interface User {
id: string;
name: string;
email: string;
}
// Use type for unions, primitives, computed types
type UserId = string;
type Status = 'pending' | 'active' | 'inactive';
type UserWithStatus = User & { status: Status };
Discriminated Unions
// Pattern: Use 'type' or 'kind' discriminator
type Result<T> =
| { success: true; data: T }
| { success: false; error: Error };
type ApiResponse =
| { type: 'loading' }
| { type: 'success'; data: unknown }
| { type: 'error'; message: string };
function handleResponse(response: ApiResponse): void {
switch (response.type) {
case 'loading':
showSpinner();
break;
case 'success':
render(response.data); // TypeScript knows data exists
break;
case 'error':
showError(response.message); // TypeScript knows message exists
break;
}
}
Type Guards
// User-defined type guard
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value &&
'email' in value
);
}
// Assertion function
function assertUser(value: unknown): asserts value is User {
if (!isUser(value)) {
throw new TypeError('Expected User object');
}
}
// Usage
function processInput(input: unknown): User {
assertUser(input);
return input; // Type narrowed to User
}
Generics
// Basic generic function
function first<T>(array: T[]): T | undefined {
return array[0];
}
// Constrained generic
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// Generic with default
function createStore<T = Record<string, unknown>>(
initial: T
): { get(): T; set(value: T): void } {
let state = initial;
return {
get: () => state,
set: (value) => { state = value; }
};
}
Web Component Types
Typed Custom Element
// types.ts
interface MyComponentProps {
value: string;
disabled?: boolean;
}
interface MyComponentEvents {
'value-change': CustomEvent<{ oldValue: string; newValue: string }>;
'submit': CustomEvent<void>;
}
// my-component.ts
class MyComponent extends HTMLElement {
static observedAttributes = ['value', 'disabled'] as const;
// Private state
#value = '';
// Typed getter/setter
get value(): string {
return this.#value;
}
set value(newValue: string) {
const oldValue = this.#value;
this.#value = newValue;
this.dispatchEvent(
new CustomEvent('value-change', {
detail: { oldValue, newValue },
bubbles: true
})
);
}
get disabled(): boolean {
return this.hasAttribute('disabled');
}
set disabled(value: boolean) {
this.toggleAttribute('disabled', value);
}
// Typed attribute callback
attributeChangedCallback(
name: typeof MyComponent.observedAttributes[number],
oldValue: string | null,
newValue: string | null
): void {
if (oldValue === newValue) return;
switch (name) {
case 'value':
this.#value = newValue ?? '';
break;
case 'disabled':
// Handle disabled state
break;
}
}
// Typed event emission
#emit<K extends keyof MyComponentEvents>(
type: K,
detail: MyComponentEvents[K]['detail']
): void {
this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true }));
}
}
customElements.define('my-component', MyComponent);
export { MyComponent };
export type { MyComponentProps, MyComponentEvents };
Augmenting HTMLElementTagNameMap
// global.d.ts
declare global {
interface HTMLElementTagNameMap {
'my-component': MyComponent;
'user-card': UserCard;
}
}
export {};
// Usage - now type-safe
const element = document.querySelector('my-component');
// element is MyComponent | null, not Element | null
API Types
Request/Response Types
// api-types.ts
interface ApiError {
code: string;
message: string;
details?: Record<string, string[]>;
}
type ApiResult<T> =
| { ok: true; data: T }
| { ok: false; error: ApiError };
interface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
pageSize: number;
hasMore: boolean;
}
// Typed fetch wrapper
async function apiFetch<T>(
url: string,
options?: RequestInit
): Promise<ApiResult<T>> {
try {
const response = await fetch(url, options);
if (!response.ok) {
const error = await response.json() as ApiError;
return { ok: false, error };
}
const data = await response.json() as T;
return { ok: true, data };
} catch (cause) {
return {
ok: false,
error: {
code: 'NETWORK_ERROR',
message: cause instanceof Error ? cause.message : 'Unknown error'
}
};
}
}
Zod for Runtime Validation
import { z } from 'zod';
// Define schema
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1).max(100),
email: z.string().email(),
role: z.enum(['admin', 'user', 'guest']),
createdAt: z.coerce.date()
});
// Infer type from schema
type User = z.infer<typeof UserSchema>;
// Parse with runtime validation
function parseUser(input: unknown): User {
return UserSchema.parse(input);
}
// Safe parse (returns result object)
function safeParseUser(input: unknown): z.SafeParseReturnType<unknown, User> {
return UserSchema.safeParse(input);
}
Utility Types
Common Built-in Types
// Partial - all properties optional
type PartialUser = Partial<User>;
// Required - all properties required
type RequiredUser = Required<User>;
// Pick - select properties
type UserName = Pick<User, 'id' | 'name'>;
// Omit - exclude properties
type UserWithoutEmail = Omit<User, 'email'>;
// Record - object type
type UserMap = Record<string, User>;
// ReturnType - function return type
type FetchResult = ReturnType<typeof fetch>;
// Parameters - function parameters
type FetchParams = Parameters<typeof fetch>;
// Awaited - unwrap Promise
type ResolvedData = Awaited<Promise<User>>;
Custom Utility Types
// Make specific properties optional
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
// Make specific properties required
type RequiredBy<T, K extends keyof T> = T & Required<Pick<T, K>>;
// Deep partial
type DeepPartial<T> = T extends object
? { [P in keyof T]?: DeepPartial<T[P]> }
: T;
// Non-nullable
type NonNullableProps<T> = {
[P in keyof T]: NonNullable<T[P]>;
};
// Extract keys by value type
type KeysOfType<T, V> = {
[K in keyof T]: T[K] extends V ? K : never;
}[keyof T];
File Organization
src/
├── types/
│ ├── index.ts # Re-exports all types
│ ├── api.ts # API request/response types
│ ├── domain.ts # Business domain types
│ └── components.ts # Component prop/event types
├── components/
│ └── my-component/
│ ├── my-component.ts
│ ├── my-component.types.ts # Component-specific types
│ └── my-component.test.ts
├── utils/
│ ├── type-guards.ts # isX and assertX functions
│ └── validators.ts # Zod schemas
└── index.ts
Best Practices
Do
// Explicit return types on public functions
function calculateTotal(items: LineItem[]): number {
return items.reduce((sum, item) => sum + item.price, 0);
}
// Use const assertions for literals
const STATUSES = ['pending', 'active', 'closed'] as const;
type Status = typeof STATUSES[number];
// Prefer unknown over any
function parseJSON(text: string): unknown {
return JSON.parse(text);
}
// Use satisfies for type checking without widening
const config = {
port: 3000,
host: 'localhost'
} satisfies Record<string, string | number>;
Don't
// Don't use any
function bad(data: any) { } // Avoid
// Don't use non-null assertion without cause
const element = document.querySelector('#app')!; // Dangerous
// Don't use type assertions carelessly
const user = response as User; // Prefer type guards
// Don't ignore errors in catch
try {
// ...
} catch (e) { } // Always handle or log
Checklist
When writing TypeScript:
Related Skills
- javascript-author - Base patterns for functional core
- unit-testing - Testing typed code
- api-client - Typed API interactions
- custom-elements - Web Component patterns