Last active
July 27, 2024 03:12
-
-
Save trepichio/ef044ce63a41fc6e8cbc6e083dd3cf35 to your computer and use it in GitHub Desktop.
Typescript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" }); // ヽ(ಠ_ಠ)ノ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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