Last active
January 24, 2025 22:56
-
-
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…
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
// 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