- Prettify
- Branded Types
- Open Union Type
- Assert never and Exhaustive switch/case Matching
- Type Safe Function Return Type Overload
- Guarantee That The Function Always Returns a Value
- Type Transformation Cheat Sheet
- Example of Simple Type Safe SQL Query Builder
"Prettifying" a type in TypeScript refers to making complex or deeply nested types more readable and understandable, especially in IDE hover previews and error messages.
type Prettify<T> = {
[K in keyof T]: T[K];
} & {};{ [K in keyof T]: T[K]; } flattens intersection types. & {} forces TypeScript
to compute the final shape of the type.
declare const __brand: unique symbol;
type Brand<B> = { [__brand]: B };
export type Branded<T, B> = T & Brand<B>;type UserId = Branded<number, "userId">;
type OrderId = Branded<number, "orderId">;
function getUser(id: UserId){}
const userId = 'user-123' as UserId;
const orderId = 'order-456' as OrderId;
getUser(userId); // OK
getUser(orderId); // Error: Argument of type 'OrderId' is not assignable to
// parameter of type 'UserId'Brand exists only on compile time.
Derived brands
type Unit<U extends string> = Branded<number, U>;
const div = <A extends string, B extends string>(a: Unit<A>, b: Unit<B>) =>
(a / b) as Unit<`${A}/${B}`>;
const distance = 5 as Unit<"ft">;
const time = 10 as Unit<"sec">;
const speed = div(distance, time); //speed has brand "ft/sec"type Options = "a" | "b" | "c" | (string & {});
function fn(o: Options) {};
//Intellisense will suggests values "a", "b", "c" for the parameter o, but
// will accept any other stringconst assertNever = (value: never): never => {
throw new Error(`Unexpected value ${value}`);
};type T = "a" | "b";
function fn1(v: T) {
switch (v) {
case "a":
console.log(v);
break;
default:
// case "b" is missing, but there is no compile time error
throw new Error(`Unexpected value ${v}`);
}
}
function fn2(v: T) {
switch (v) {
case "a":
console.log(v);
break;
default:
// case "b" is missing, and there IS a compile time error
assertNever(v);
}
}
//exhaustive match w/o errors
function fn3(v: T) {
switch (v) {
case "a":
console.log(v);
break;
case "b":
console.log(v);
break;
default:
assertNever(v);
}
}function foo<T extends boolean>(p: T): string;
function foo<T extends number>(p: T): number;
function foo(p: boolean | number): string | number {
if (typeof p === "boolean") return p ? "yes" : "no";
return p * 2;
}
const a = foo(true); // a: string = "yes"
const b = foo(3); // b: number = 6Same with generics and conditional types:
function foo2<
T extends boolean | number,
R = T extends boolean ? string : T extends number ? number : never
>(p: T): R {
switch (typeof p) {
case "boolean":
return (p ? "true" : "false") as R;
case "number":
return (p * 2) as R;
default:
return assertNever(p);
}
}
const a2 = foo2(true); // a: string = "yes"
const b2 = foo2(3); // b: number = 6Both implementations still do not check if the wrong type returned by the function implementation on compile time.
/**
* Guarantee that function of type F always returns a value
*/
export type ReturnNonVoid<F extends (...args: any) => any> =
ReturnType<F> extends void ? never : F;
const map =
<A, B>(fn: ReturnNonVoid<(a: A) => B>) =>
(a: A): B => {
return fn(a);
};
function num2string(n: number) {
console.log(n.toString());
// missing return n.toString();
}
// compile time error
let k = map(num2string)(5);type U = "id" | "email";
type A = U[]; // ⇒ ("id" | "email")[]type T = ["id", "email"];
type U = T[number]; // ⇒ "id" | "email"const fields = ["id", "email"] as const;
// type: readonly ["id", "email"]
// prevents further type inference from widening to type string[]function get(): readonly ["id", "email"] {
// prevents caller type inference from widening to type string[]
return ["id", "email"] as const;
}type T = ["id", "email"];
type R = readonly [...T]; // ⇒ readonly ["id", "email"]type A<T> = (keyof T)[];
type User = { id: number; email: string };
type Keys = A<User>; // ⇒ ("id" | "email")[]type U = "a" | "b" | "c";
//items can be a tuple of values from union type U only
function select<K extends U[]>(items: [...K]) {}
select(["a", "c"])// items inferred as tuple: ["a", "c"]type K = ["id", "email"];
type Result<T, K extends readonly (keyof T)[]> = Pick<T, K[number]>;
// Extracts only the fields listed in KDatabase schema (generated by some introspection tool)
type MySchema = {
order: {
id: number;
user: string;
email: string;
total: number;
};
orderItem: {
id: number;
orderId: number;
sku: string;
description: string;
quantity: number;
};
};Usage:
const ctx = createDbContext<MySchema>();
//selectFrom() accepts only "order" or "orderItem" table names
//select() accepts only array of column names from a selected table or "*"
// r2 type is { user: string, email: string }[]
const r2 = ctx.selectFrom("order").select(["email", "user"]).execute();
// r3 type is { description: string, quantity: number }[]
const r3 = ctx
.selectFrom("orderItem")
.select(["description", "quantity"])
.execute();
// r4 type is { id: number, user: string, email: string, total: number }[]
const r4 = ctx.selectFrom("order").select("*").execute();