Skip to content

Instantly share code, notes, and snippets.

@benzittlau
Last active January 24, 2025 22:56
Show Gist options
  • Save benzittlau/e913aa4c4efa638bca57f4b69c5aae2a to your computer and use it in GitHub Desktop.
Save benzittlau/e913aa4c4efa638bca57f4b69c5aae2a to your computer and use it in GitHub Desktop.
The challenge here is to try and implement TypeScript that can handle a set of different operations defined in objects that are internally consistent in their type requirements. By this I mean that each object has a consistent interface except that the types are different object to object. The interface is of a consistent type, but has constrain…
// The challenge here is to try and implement TypeScript that can handle a set of different operations
// defined in objects that are internally consistent in their type requirements. By this I mean that
// each object has a consistent interface except that the types are different object to object. The
// interface is of a consistent type, but has constraints, such as the return type of one function
// being the argument for another function. I then want to be able to have generic code that is
// able to "execute" each of these type specific objects while being type safe and not throwing
// type errors.
//
// The primary issue is that TypeScript infers the types to be a union type, for example String | Number,
// and consequently throws a type error. The structure doesn't allow it to understand that in each
// individual invocation it will be either String *or* Number, and consequently type safe.
// Original Attempt
type ParentType<TArgs> = {
a: () => TArgs;
b: (input: TArgs) => TArgs;
};
const stringObj: ParentType<string> = {
a: () => "a",
b: (input: string) => input,
};
const numberObj: ParentType<number> = {
a: () => 1,
b: (input: number) => input,
};
const OBJECTS = {
str: stringObj,
num: numberObj,
} as const;
const handleArgument = <TArgs>(object: ParentType<TArgs>) => {
const value = object.a();
return object.b(value);
};
const callerFunc = (chooser: "str" | "num") => {
const object = OBJECTS[chooser];
const result = handleArgument(object);
};
// Claude's first solution
type ParentType<TArgs> = {
a: () => TArgs;
b: (input: TArgs) => TArgs;
};
const stringObj: ParentType<string> = {
a: () => "a",
b: (input: string) => input,
};
const numberObj: ParentType<number> = {
a: () => 1,
b: (input: number) => input,
};
// Define a type map to preserve type relationships
type ObjectMap = {
str: ParentType<string>;
num: ParentType<number>;
};
// Create OBJECTS with explicit typing
const OBJECTS: ObjectMap = {
str: stringObj,
num: numberObj,
};
// Make handleArgument preserve the type relationship
const handleArgument = <TArgs>(object: ParentType<TArgs>) => {
const value = object.a();
return object.b(value);
};
// Use type inference to maintain type safety
const callerFunc = <K extends keyof ObjectMap>(chooser: K) => {
const object = OBJECTS[chooser];
const result = handleArgument(object);
return result;
};
// Claude's second solution
type ParentType<TArgs> = {
a: () => TArgs;
b: (input: TArgs) => TArgs;
};
const stringObj: ParentType<string> = {
a: () => "a",
b: (input: string) => input,
};
const numberObj: ParentType<number> = {
a: () => 1,
b: (input: number) => input,
};
const OBJECTS = {
str: stringObj,
num: numberObj,
} as const;
// Instead of making handleArgument generic, make it handle unions directly
const handleArgument = (object: ParentType<string> | ParentType<number>) => {
const value = object.a();
return object.b(value);
};
const callerFunc = (chooser: "str" | "num") => {
const object = OBJECTS[chooser];
const result = handleArgument(object);
};
// My best solution
type Transformer<TArgs> = {
get: () => TArgs;
transform: (input: TArgs) => TArgs;
};
const stringObj: Transformer<string> = {
get: () => "world",
transform: (input: string) => `hello ${input}`,
};
const numberObj: Transformer<number> = {
get: () => 1,
transform: (input: number) => input + 1,
};
type RunnableTransformer<TArgs> = Transformer<TArgs> & {
run: (cb: (transformer: Transformer<TArgs>) => TArgs) => TArgs;
};
const toRunnableTransformer = <TArgs>(
transformer: Transformer<TArgs>
): RunnableTransformer<TArgs> => {
return {
...transformer,
run: (cb) => cb(transformer),
} as RunnableTransformer<TArgs>;
};
const TRANSFORMERS = {
str: toRunnableTransformer(stringObj),
num: toRunnableTransformer(numberObj),
} as const;
type TransformKeys = keyof typeof TRANSFORMERS;
const handleTransformer = <TArgs>(object: Transformer<TArgs>) => {
const value = object.get();
console.log("Original value was:", value);
const transformed = object.transform(value);
console.log("Transformed value was:", value);
return transformed;
};
const callerFunc = (chooser: TransformKeys) => {
const transformer = TRANSFORMERS[chooser];
transformer.run(handleTransformer);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment