Skip to content

Instantly share code, notes, and snippets.

@trepichio
Last active July 27, 2024 03:12
Show Gist options
  • Save trepichio/ef044ce63a41fc6e8cbc6e083dd3cf35 to your computer and use it in GitHub Desktop.
Save trepichio/ef044ce63a41fc6e8cbc6e083dd3cf35 to your computer and use it in GitHub Desktop.
Typescript
class ClassZ { private a?: 1; }
class ClassX { private a?: 1; }
class ClassY { private a?: 1; }
class ClassW { private a?: 1; }
class ClassA { private a?: 1; }
class ClassZ1 extends ClassZ { private b?: 1; }
class ClassZ2 extends ClassZ { private b?: 1; }
class ClassZ3 extends ClassZ { private b?: 1; }
class A {}
class B {}
interface ClassConstruct {
new (
prop1: ClassZ,
prop2: any,
): A
}
type ClassConstructor = {
class: ClassConstruct
otherClasses: { new (): B }[]
}
enum numericEnum1 {
A = 1,
B = 2
}
enum numericEnum2 {
C = 1,
D = 2,
}
enum stringEnum2 {
A = 'A',
B = 'B',
C = 'C',
}
numericEnum1[numericEnum1.A]//?
stringEnum2[stringEnum2.A]//?
type enums = keyof typeof numericEnum1 | keyof typeof numericEnum2 | keyof typeof stringEnum2
type stringEnum = { [K in enums & string]: K }
function convertEnumsToStringEnum(enums: any[]) {
return enums.map((item) => {
return Object.fromEntries(
Object.values(item)
.filter(value => typeof value === 'string')
.map(value => [value, value])
) as stringEnum
}).reduce((acc, curr) => {
return { ...acc, ...curr }
})
}
// export function convertEnumToStringEnumObject<T>(enumLike: T) {
// type stringEnum<T> = { [K in T & string]: K }
// return Object.values(enumLike)
// .filter((value) => typeof value === 'string')
// .map((value) => [value, value])
// .reduce(
// (acc, [key, value]) => Object.assign(acc, { [key]: value }),
// {}
// ) as stringEnum<keyof typeof enumLike>
// }
const stringEnum = convertEnumsToStringEnum([numericEnum1, numericEnum2, stringEnum2]) as stringEnum//?
// const stringEnum = Object.fromEntries(
// Object.values(numericEnum2)
// .filter(value => typeof value === 'string')
// .map(value => [value, value])
// ) as stringEnum
type ObjectMapConstraint = Record<string, Record<string, ClassConstructor>>;
function createObjectMap<T extends ObjectMapConstraint>(obj: T): T { return obj; }
const objectMap = createObjectMap({
parent1: {
[stringEnum[stringEnum.A]]: {
class: ClassA,
otherClasses: [ClassZ1, ClassZ2, ClassZ3]
},
[stringEnum[stringEnum.D]]: {
class: ClassX,
otherClasses: [ClassZ1, ClassZ2, ClassZ3]
},
},
parent2: {
[stringEnum2[stringEnum2.A]]: {
class: ClassX,
otherClasses: [ClassZ1, ClassZ2, ClassZ3]
},
[stringEnum2[stringEnum2.C]]: {
class: ClassY,
otherClasses: [ClassZ1, ClassZ2, ClassZ3]
}
},
parent3: {
someChild: {
class: ClassW,
otherClasses: [ClassZ1, ClassZ2, ClassZ3]
}
}
})
type ObjectMap = typeof objectMap;
class factory {
static create<K extends keyof ObjectMap>(
parent: K,
child: `${keyof ObjectMap[K] & string}`
): any[] {
const objMap: ObjectMapConstraint = objectMap
const MyClass = objMap[parent][child].class
return objMap[parent][child].otherClasses.map((other) => {
return new MyClass(new other(), 123)
})
}
}
factory.create("parent1", "D")
factory.create("parent2", "A")
factory.create("parent1", numericEnum1.B) // error, as expected
factory.create("parent3", "someChild")
type ErrorBrand<Err extends string> = Readonly<{
[key in Err]: void;
}>;
const booPonies = <T>(
ponies: keyof T extends "pony" ? ErrorBrand<"No ponies allowed!"> : T
) => {};
booPonies("no ponies"); // Good
booPonies({ pony: "neigh" }); // ヽ(ಠ_ಠ)ノ
/* If-else functional */
export const conditionally =
<Props, Result>(options: {
if: (props: Props) => any;
then: (props: Props) => Result | Result;
else: (props: Props) => Result | Result;
}) =>
(props: Props) => {
return options.if(props) ? options.then(props) : options.else(props);
};
/* Try-catch functional */
export function tryCatch<Props, Result>({
tryer,
catcher,
}: {
tryer: (props: Props) => Result;
catcher: (props: Props, message: string) => Result;
}) {
return (props: Props) => {
try {
return tryer(props);
} catch (e) {
return catcher(props, e.message);
}
};
}
type Enum = { [s: number]: string };
/* Check if a key is property of enum */
export function isEnumKey<T extends Enum>(
enumSrc: T,
key: unknown
): key is keyof T {
return Number.isInteger(enumSrc[key as keyof T]);
}
/* Check if enum has a given value */
export function isEnumValue<T extends Enum>(
enumSrc: T,
value: unknown
): value is T[keyof T] {
return Number.isInteger(
enumSrc[enumSrc[value as keyof T] as any as keyof T]
);
}
/* Transform enum to list of keys */
export function enumToKeys<T extends Enum>(enumSrc: T): (keyof T)[] {
return Object.keys(enumSrc).filter((key: keyof T | any) =>
isEnumKey(enumSrc, key)
) as (keyof T)[];
}
/* Transform enum to list of values */
export function enumToValues<T extends Enum>(enumSrc: T): T[keyof T][] {
return enumToKeys(enumSrc).map((key: keyof T) => enumSrc[key]);
}
/* Transform enum value to its appropriate key */
export function enumValueToKey<T extends Enum>(
enumSrc: T,
value: T[keyof T]
): keyof T | undefined {
return (enumSrc as any)[value];
}
/* Transform enum to entries */
export function enumToEntries<T extends Enum>(enumSrc: T): [keyof T, T[keyof T]][] {
return enumToValues(enumSrc).map((value: T[keyof T]) => [
enumValueToKey(enumSrc, value) as keyof T,
value,
]);
}
/* Transform enum to mapped Object */
export function convertEnumToStringEnumObject<T>(enumLike: T) {
type stringEnum<T> = { [K in T & string]: K }
return Object.values(enumLike)
.filter((value) => typeof value === 'string')
.map((value) => [value, value])
.reduce(
(acc, [key, value]) => Object.assign(acc, { [key]: value }),
{}
) as stringEnum<keyof typeof enumLike>
}
/* Create a function that transforms enum value into CSS class */
interface UiClass {
prefix?: string;
suffix?: string;
delimiter?: string;
}
const DEFAULT_CONFIG: UiClass = {
suffix: "",
prefix: "",
delimiter: "-",
};
const uiClassByEnum = (enumSrc: Enum, config: UiClass = {}) => {
const { suffix, prefix, delimiter } = { ...DEFAULT_CONFIG, ...config };
return (enumValue: number) => {
const enumKey = (enumSrc as any)[enumValue]
.match(/[A-Z][a-z]+/g)
.join(delimiter)
.toLowerCase();
return [prefix, enumKey, suffix].filter(Boolean).join(delimiter);
};
};
// Example:
const asDayOfWeekUiClass = uiClassByEnum(DayOfWeek, {
prefix: "obv",
suffix: "element",
delimiter: "_",
});
console.log(asDayOfWeekUiClass(DayOfWeek.Wednesday)); // obv_wednesday_element
enum RolesEnum {
Read = 1,
Write = 2,
ReadWrite = 3,
}
const asRolesWeekUiClass = uiClassByEnum(RolesEnum, {
prefix: "ng",
suffix: "element",
});
console.log(asRolesWeekUiClass(DayOfWeek.Wednesday)); // ng-read-write-element
/* Project the list of objects from an enum */
function fromEnum<T extends Enum, C>(
enumSrc: T,
projection: (
item: [keyof T, T[keyof T]],
index: number,
array: [keyof T, T[keyof T]][]
) => C,
skip?: (
value: [keyof T, T[keyof T]],
index: number,
array: [keyof T, T[keyof T]][]
) => boolean
) {
let entries = enumToEntries(enumSrc);
if (skip) entries = entries.filter(skip);
return entries.map(projection);
}
// Example:
interface Option<T> {
label: keyof T;
value: T[keyof T];
}
const options: Option<typeof DayOfWeek>[] = fromEnum(
DayOfWeek,
([label, value]: [keyof typeof DayOfWeek, DayOfWeek]) => ({
label,
value,
})
);
console.log(options);
/*
[
{ label: 'Monday', value: 1 },
{ label: 'Tuesday', value: 2 },
{ label: 'Wednesday', value: 3 },
{ label: 'Thursday', value: 4 },
{ label: 'Friday', value: 5 },
{ label: 'Saturday', value: 6 },
{ label: 'Sunday', value: 7 }
]
*/
export const roundTo = (number: number, digits = 0): number => {
let negative = false;
let n = number;
if (n < 0) {
negative = true;
n *= -1;
}
const multiplicator = 10 ** digits;
n = parseFloat((n * multiplicator).toFixed(11));
n = +(Math.round(n) / multiplicator).toFixed(2);
if (negative) {
n = +(n * -1).toFixed(2);
}
return n;
};
/* Const Assertion To Derive Types From Literal Expressions */
// The derived types can help to enforce type safety from a single source of truth.
export const payGrades = {
low: "1",
average: "2",
high: "3"
} as const;
type t = typeof payGrades;
type payGradeType = keyof t; // 'low' | 'average' | 'high'
type payValueType = t[keyof t]; // '1' | '2' | '3'
const hisPay: payValueType = '3'; //okay
const myPay: payValueType = '4'; // error
/* Exhaustive Checks With “never” Type */
// if another developer adds a new literal type into the DataTypes, and forgets
// to update the switch statement, a compile time error will be thrown.
type DataTypes = "client" | "order";
function getProcessName(c: DataTypes): string {
switch (c) {
case "client":
return "register" + c;
case "order":
return "process" + c;
default:
return assertUnreachable(c);
}
}
// With the exhaustive type checking in place, we can detect a missing condition
// at compile time instead of run time.
function assertUnreachable(x: never): never {
throw new Error("something is very wrong");
}
/* Use Opaque Type To Simulate Nominal Typing Behavior */
type Person = { name: string};
type OpaqueType<K, T> = K & { _brand: T };
type Customer = OpaqueType<Person, "Customer">;
type VIPCustomer = OpaqueType<Person, "VIP">;
function getVIPName(vip: VIPCustomer) {
return vip.name;
}
const cust = { name: "John" } as Customer;
const vip = { name: "Mark" } as VIPCustomer;
console.log("vip name:", getVIPName(vip)); //vip name: Mark
// Error: Argument of type 'Customer' is not assignable to parameter of type 'VIPCustomer'.
console.log("vip name:", getVIPName(cust));
/* Lookup property type from an Object Type */
// TypeScript is about types. Often, we need to extract
// an existing object property type from a complex object type.
// We use conditional types and never to filter out
// the required data type definitions in lookup type definition below.
type PropertyType<T, Path extends string> = Path extends keyof T
? T[Path]
: Path extends `${infer K}.${infer R}`
? K extends keyof T
? PropertyType<T[K], R>
: never
: never;
type lookup<T, Key, prop extends string> = Key extends keyof T
? PropertyType<T[Key], prop>
: never;
type Thing<T extends string | number | symbol, U> = { tag: T } & U;
type TagMapEntry<T extends string | number | symbol, U> = {
[key in T]: Thing<T, U>;
};
type TagMap = TagMapEntry<"cow", { moo: "milk" }> &
TagMapEntry<"cat", { meow: "notmilk" }>;
type Cow = TagMap["cow"]; // Cow = Thing<'cow', {moo: 'milk'}>
type Cat = TagMap["cat"]; // Cat = Thing<'cat', {meow: 'notmilk'}>
type CowOrCat = TagMap["cat" | "cow"]; // CowOrCat = Cat | Cow
type Dog = TagMap["dog"]; // Fails to compile, no dog in the map
/*
A few key observations here:
1. Unlike with sets where we used | to add elements, here we use
the & (intersection) type operator to add entries to our map (TagMap).
2. Unlike most value maps (i.e. instances of Dictionary or Map you commonly
see in languages) where you can query only a single thing at a time,
Typescript type maps allow you to query several values at once as seen with
CowOrCat. When you do this, you get back a type set.
3. In the extreme case, you can query TagMap[keyof TagMap] and get back
the entire type set of all the mapped types.
*/
// One can also associate multiple types to a given key.
// To do this in a general and extensible way is a bit tricky
// if you don’t know all the types in one place:
type AddKeyToMap<M, K extends string | number | symbol, V> =
Omit<M, K> & { [key in K]: M[Extract<K, keyof M>] | V }
type Map1 = AddKeyToMap<{}, 'cat', 'meow'>;
type Map2 = AddKeyToMap<Map1, 'cat', 'purr'>;
type Cat2 = Map2['cat']; // 'meow' | 'purr'
type Definition = {
vals: number[];
};
type ErrorBrand<Err extends string> = Readonly<{
[k in Err]: void;
}>;
// Entry point for our builder. Unlike the value builder
// pattern example, we don't need a definition value storing the set of
// added numbers because we're going to stash them in a type and check
// them at compile time rather than runtime.
const makeAThing = () => {
// Users can call addNumber as the next step in the builder.
// Initialize the Nums typeset to empty set.
return {
addNumber: addNumber<never>(),
};
};
type AddNumberBuilder<Nums extends number> = {
// When the user calls addNumber(x: T), check that T doesn't
// already exist in Nums. If it doesnt, continue the builder
// and add T to the Nums set.
addNumber: <T extends number>(
x: T extends Nums ? ErrorBrand<"Number already declared"> : T
) => AddNumberBuilder<Nums | T>;
// Finalize the builder, returning a function that asserts that x
// exists in our Nums set.
done: () => <T extends number>(
x: T extends Nums ? T : ErrorBrand<"Not a legal number">
) => void;
};
// To implement the partial evaluation builder, the addNumber function
// returns a function that returns an AddNumberBuilder.
const addNumber = <Nums extends number>() => {
return <T extends number>(
x: T extends Nums ? ErrorBrand<"Number already declared"> : T
): AddNumberBuilder<Nums | T> => {
return {
addNumber: addNumber<Nums | T>(),
done: done(),
};
};
};
// The done function returns a function that returns our validation
// function.
const done = <Nums extends number>() => {
return () => {
return <T extends number>(
x: T extends Nums ? T : ErrorBrand<"Not a legal number">
) => {};
};
};
const myBuilder = makeAThing().addNumber(7).addNumber(5).done();
myBuilder(5);
myBuilder(7);
myBuilder(8); // Argument of type 'number' is not assignable to parameter of type
// 'Readonly<{ "Not a legal number": void; }>'.(2345)
const myOther = makeAThing()
.addNumber(7)
.addNumber(5)
.addNumber(5) // Argument of type 'number' is not assignable to parameter of
// type 'Readonly<{ "Number already declared": void; }>'.(2345)
.done();
// Firstly define the entities
type DataSchema = {
client: {
dto: { id: string; name: string };
entity: { clientId: string; clientName: string };
};
order: {
dto: { id: string; amount: number };
entity: { orderId: string; quantity: number };
};
};
// transvers to lookup the property type
type PropertyType<T, Path extends string> = Path extends keyof T
? T[Path]
: never;
type lookup<T, Key extends keyof T, prop extends string> = PropertyType<
T[Key],
prop
>;
// one liner version
type lookup2<
T,
Key extends keyof T,
prop extends string
> = prop extends keyof T[Key] ? T[Key][prop] : never;
// mapTo and MapFrom
type MapTo<T extends string> = `to${Capitalize<T>}`;
type MapFrom<T extends string> = `from${Capitalize<T>}`;
type ExtractMapperTo<Type> = {
[Key in keyof Type as MapTo<Key extends string ? Key : never>]: (
args: lookup<Type, Key, "dto">
) => lookup<Type, Key, "entity">;
};
type ExtractMapperFrom<Type> = {
[Key in keyof Type as MapFrom<Key extends string ? Key : never>]: (
args: lookup<Type, Key, "entity">
) => lookup<Type, Key, "dto">;
};
// Then all these mapper methods are automatically created
type mapper = ExtractMapperTo<DataSchema> & ExtractMapperFrom<DataSchema>;
// Our first goal achieved!
declare const m: mapper;
m.toClient({ id: "123", name: "John" });
m.fromClient({ clientId: "123", clientName: "John" });
m.toOrder({ id: "123", amount: 3 });
m.fromOrder({ orderId: "345", quantity: 4 });
// Derive the data type names into a union type
type PropToUnion<T> = { [k in keyof T]: k }[keyof T];
type DataTypes = PropToUnion<DataSchema>;
// Second goal achieved
function getProcessName(c: DataTypes): string {
switch (c) {
case "client":
return "register" + c;
case "order":
return "process" + c;
default:
return assertUnreachable(c);
}
}
function assertUnreachable(x: never): never {
throw new Error("something is very wrong");
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment