Loading...
Loading...
Comprehensive TypeScript skill covering the full type system: fundamentals, generics, conditional/mapped/template literal types, utility types, strict mode, React patterns, production tsconfig, and advanced patterns from Boris Cherny (variance...
npx skill4agent add peterbamuhigire/skills-web-dev typescript-masterytypescript-masterySKILL.md| Category | Artifact | Format | Example |
|---|---|---|---|
| Correctness | Type-system test plan | Markdown doc covering generic, conditional, mapped, and template-literal type tests | |
let name: string = "Alice";
let id: string | number; // union
let val: unknown; // safe unknown — must narrow before use
let never_: never; // unreachable / exhaustive check
const ids: string[] = [];
const entry: [string, number] = ["Alice", 30]; // tuple
const named: [name: string, age: number] = ["Alice", 30]; // named tuple
const greet = (name: string, age = 30): string => `Hello ${name}`;
const concat = (first: string, last?: string) => last ? `${first} ${last}` : first;type ID = string | number; // unions, primitives → type
type Status = "active" | "inactive"; // literal unions → type
interface User { // object shapes → interface (merges)
id: string;
name: string;
email?: string;
}
interface AdminUser extends User { roles: string[] }
type AdminUser = User & { roles: string[] }; // intersection — equivalenttype Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number };
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2;
case "rectangle": return shape.width * shape.height;
default: return assertNever(shape); // exhaustive
}
}
function assertNever(x: never): never { throw new Error("Unhandled: " + x) }typeof val === "string" // typeof guard
err instanceof Error // instanceof guard
"roles" in user // in guard
val != null // nullish guard
function isAdmin(u: User | AdminUser): u is AdminUser { return "roles" in u }
function assertAdmin(u: User | AdminUser): asserts u is AdminUser {
if (!("roles" in u)) throw new Error("Not admin");
}type P = Partial<User>; // all optional
type R = Required<User>; // all required
type RO = Readonly<User>; // all readonly
type D = Omit<User, "id">; // without id
type S = Pick<User, "name" | "email">; // only these
type Rc = Record<"dev"|"prod", Config>; // keyed map
type NN = NonNullable<string | null>; // removes null/undefined
type X = Extract<"a"|"b"|"c", "a"|"c">; // "a" | "c"
type Ex = Exclude<"a"|"b"|"c", "a"|"c">; // "b"type AlbumKeys = keyof Album;
const cfg = { dev: "http://localhost", prod: "https://api.example.com" } as const;
type Env = keyof typeof cfg;
type EnvVals = typeof cfg[keyof typeof cfg];
const ROLES = ["admin", "user", "guest"] as const;
type Role = typeof ROLES[number]; // "admin" | "user" | "guest"
type Params = Parameters<typeof fn>;
type Return = ReturnType<typeof fn>;
type Res = Awaited<ReturnType<typeof asyncFn>>;type Result<T, E extends { message: string } = Error> =
| { success: true; data: T }
| { success: false; error: E };
function echo<T>(input: T): T { return input }
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key] }
function createMap<T = string>(): Map<string, T> { return new Map() }
// Conditional
type ToArray<T> = T extends any[] ? T : T[];
// Distributive (applies to each union member)
type DistributiveOmit<T, K extends PropertyKey> = T extends any ? Omit<T, K> : never;
// Mapped
type Nullable<T> = { [K in keyof T]?: T[K] | null };
// Key remapping + template literal
type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] };
// Template literals
type Route = `/${string}`;
type Colors = `${"red"|"blue"}-${100|200|300}`; // 6 combinationsconst routes = { home: "/", about: "/about" } satisfies Record<string, `/${string}`>;
routes.home; // type is "/" not string — satisfies keeps narrow type
const user = getUser() as AdminUser; // escape hatch (use sparingly)
const btn = document.getElementById("btn")!; // non-null assertion// Covariant (producer — can use subtype where supertype expected)
type Producer<T> = () => T;
declare let catProducer: Producer<Cat>;
declare let animalProducer: Producer<Animal>;
animalProducer = catProducer; // OK — Cat is a subtype of Animal
// Contravariant (consumer — can use supertype where subtype expected)
type Consumer<T> = (t: T) => void;
declare let catConsumer: Consumer<Cat>;
declare let animalConsumer: Consumer<Animal>;
catConsumer = animalConsumer; // OK — Animal consumer handles Cat too
// Function types: params are contravariant, return type is covariant
// "Be liberal in what you accept, conservative in what you return"infer// Extract wrapped type from any container
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type UnwrapArray<T> = T extends Array<infer U> ? U : T;
// Custom ReturnType
type MyReturnType<F extends (...args: any) => any> = F extends (...args: any) => infer R ? R : never;
// Extract first and last tuple elements
type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never;
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;
// Flatten nested arrays
type Flatten<T> = T extends Array<infer U> ? Flatten<U> : T;// Prevent mixing structurally identical types (e.g., user ID vs order ID)
type UserId = string & { readonly __brand: unique symbol };
type OrderId = string & { readonly __brand: unique symbol };
const toUserId = (id: string): UserId => id as UserId;
const toOrderId = (id: string): OrderId => id as OrderId;
function getUser(id: UserId) { /* ... */ }
const uid = toUserId('abc');
const oid = toOrderId('abc');
getUser(uid); // OK
getUser(oid); // Error — OrderId is not UserIdtype Option<T> = { type: 'some'; value: T } | { type: 'none' };
const some = <T>(value: T): Option<T> => ({ type: 'some', value });
const none: Option<never> = { type: 'none' };
function map<T, U>(opt: Option<T>, fn: (t: T) => U): Option<U> {
return opt.type === 'none' ? none : some(fn(opt.value));
}
function getOrElse<T>(opt: Option<T>, fallback: T): T {
return opt.type === 'none' ? fallback : opt.value;
}
// Usage — no null checks needed
const name = getOrElse(map(getUser(id), u => u.name), 'Anonymous');// Model failure without throwing — callers must handle errors
type DatabaseError = { type: 'DatabaseError'; message: string };
type NotFoundError = { type: 'NotFoundError'; id: string };
async function findUser(id: string): Promise<User | DatabaseError | NotFoundError> {
try {
const user = await db.findById(id);
if (!user) return { type: 'NotFoundError', id };
return user;
} catch (e) {
return { type: 'DatabaseError', message: String(e) };
}
}
// Caller must handle all cases
const result = await findUser('123');
if ('type' in result) {
if (result.type === 'NotFoundError') console.log('Not found:', result.id);
else console.log('DB error:', result.message);
} else {
console.log('User:', result.name); // guaranteed User
}// Pair an interface with a namespace of the same name
interface Currency { unit: string; value: number }
namespace Currency {
export const from = (value: number, unit: string): Currency => ({ value, unit });
export const add = (a: Currency, b: Currency): Currency => {
if (a.unit !== b.unit) throw new Error('Unit mismatch');
return { value: a.value + b.value, unit: a.unit };
};
export const format = (c: Currency) => `${c.value} ${c.unit}`;
}
const usd = Currency.from(100, 'USD');
const total = Currency.add(usd, Currency.from(50, 'USD'));// Map event names to their payload types
type Events = {
'user:login': { userId: string; timestamp: Date };
'user:logout': { userId: string };
'order:created': { orderId: string; amount: number };
};
class TypedEventEmitter<T extends Record<string, unknown>> {
private handlers: { [K in keyof T]?: Array<(payload: T[K]) => void> } = {};
on<K extends keyof T>(event: K, handler: (payload: T[K]) => void) {
(this.handlers[event] ??= []).push(handler);
}
emit<K extends keyof T>(event: K, payload: T[K]) {
this.handlers[event]?.forEach(h => h(payload));
}
}
const emitter = new TypedEventEmitter<Events>();
emitter.on('user:login', ({ userId, timestamp }) => console.log(userId, timestamp));
emitter.emit('user:login', { userId: '123', timestamp: new Date() });
// emitter.emit('user:login', { wrongField: true }); // Error!// What can be merged:
// interface + interface → merged properties
// namespace + namespace → merged members
// class + interface → interface adds instance members
// namespace + function → adds static properties
// namespace + enum → adds methods to enum
interface User { name: string }
interface User { age: number } // merged: { name, age }
// Augment a module's types
declare module 'express' {
interface Request { userId?: string }
}interface ButtonProps {
label: string;
onClick: () => void;
variant?: "primary" | "secondary" | "danger";
children?: React.ReactNode;
}
const Button = ({ label, onClick, variant = "primary" }: ButtonProps) => (
<button className={`btn-${variant}`} onClick={onClick}>{label}</button>
);
const [user, setUser] = useState<User | null>(null);
const inputRef = useRef<HTMLInputElement>(null);
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => setValue(e.target.value);{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"esModuleInterop": true,
"skipLibCheck": true,
"isolatedModules": true,
"declaration": true,
"sourceMap": true,
"outDir": "dist"
}
}strictnoUncheckedIndexedAccessarr[i]T | undefinedtype Values<T> = T[keyof T];
type DeepReadonly<T> = { readonly [K in keyof T]: DeepReadonly<T[K]> };
type Merge<A,B> = Omit<A, keyof B> & B;
type AsyncReturn<T extends (...args: any) => Promise<any>> = Awaited<ReturnType<T>>;
type Getter<T extends string> = `get${Capitalize<T>}`;
// Strict Omit — only accepts keys that exist in T
type StrictOmit<T, K extends keyof T> = Omit<T, K>;
// Make specific keys required, keep rest as-is
type RequireKeys<T, K extends keyof T> = Required<Pick<T,K>> & Omit<T,K>;// BAD: any
function fn(x: any) { return x.val }
// GOOD: unknown + narrowing
function fn(x: unknown) {
if (typeof x === "object" && x && "val" in x) return (x as { val: unknown }).val;
}
// BAD: Omit on unions (collapses to shared properties)
type R = Omit<A | B, "id">; // WRONG — loses unique properties
// GOOD: DistributiveOmit
type R = DistributiveOmit<A | B, "id">;
// BAD: enum (nominal surprise + JS output)
enum Status { Active = "active" }
// GOOD: as const POJO
const Status = { Active: "active" } as const;
type Status = typeof Status[keyof typeof Status];references/generics-and-type-level.md