TypeScript Stack Overflow Answers
Selected technical solutions from my Stack Overflow history. These focus on inference, generics, overloads, and class typing — not a vote leaderboard.
Problem
A bindNew helper wrapping new with a Proxy needed ConstructorParameters of an overloaded constructor. TypeScript collapsed to the last overload.
Solution
Don't fight overload inference. Type the constructor as a union of tuples: constructor(...args: [a: number] | [a: string, b: number]). ConstructorParameters then follows that union.
Why it works
TS picks the last overload for many higher-order types. A single rest parameter typed as a union of tuples is one signature, so inference stays exact.
Code
class AClass {
constructor(...args: [a: number] | [a: string, b: number]) {}
}
type t = ConstructorParameters<typeof AClass>;
1 score · Accepted✓
View on Stack Overflow ↗Problem
for (const prop in myObject) typed prop as string, not keyof typeof myObject. The asker wanted a typesafe enumeration.
Solution
That is intentional: types are open and prototypes can grow enumerable keys at runtime. A branded Sealed type plus Object.iterateKeys can return keyof T only after Object.seal, and even then runtime remains unsafe.
Why it works
keyof is a compile-time closed set. for-in walks the runtime object, including inherited enumerables. string is the honest type unless you prove the object cannot grow keys.
Code
Object.seal(myObject);
for (const prop of Object.iterateKeys(myObject)) {
// prop is keyof typeof myObject only if sealed
}
1 score · Accepted · 200 question views✓
View on Stack Overflow ↗Problem
A wrapper needed to return a class that stayed constructable and callable while keeping instance types. A Proxy around the class was the first idea.
Solution
Skip the Proxy (roughly 6× slower, inheritance is painful). Put a generic static create on the base class using ConstructorParameters/InstanceType so subclasses keep their constructors.
Why it works
this: T on a static method is the actual subclass. new this(...args) plus InstanceType<T> preserves Foo vs Bar without wrapping the constructor object.
Code
static create<T extends new (...args: any) => any>(
this: T,
...args: ConstructorParameters<T>
) {
return new this(...args) as InstanceType<T>;
}
0 score · Accepted✓
View on Stack Overflow ↗Problem
Mixins worked on class declarations. Assigned to const User = class {…}, mixin methods disappeared from the type (and merging an interface with that variable failed).
Solution
You cannot merge an interface with a class expression variable. Use an asserts mixin(src, dst) that Object.assign's onto the prototype and narrows the constructor type. Real mixins compose class objects; mutating a prototype is closer to inheritance.
Why it works
Declaration merging keys off a class name. A class expression has no merge target. An assertion function rewrites the constructor type after the runtime mix.
Code
function mixin<M extends object>(
src: new (...args: any) => any,
dst: M
): asserts src is new (...args: any) => unknown & M {
Object.assign(src.prototype, dst);
}
3 score · Accepted · 108 question views✓
View on Stack Overflow ↗Problem
defineEmits<{ change: [id: number] }>() always treats handlers as void. The child needed a string back from the parent.
Solution
Event emitters do not return values. Type an onEvent prop as (() => string) | Array<() => string> and invoke those listeners. Fallthrough attrs can still lose typing in parents.
Why it works
emit is fire-and-forget. Vue already maps onXyz props to listeners; typing that prop is the supported way to describe a callback contract.
Code
const props = defineProps<{
onEvent: (() => string) | Array<() => string>
}>();
2 score · Accepted · 605 question views✓
View on Stack Overflow ↗Problem
typeof foo lost the generic T when wrapping parameters as Readonly and the return as Promise.
Solution
Keep T on the wrapper: type Foo<T> = MakeFunctionAsync<typeof foo<T>>. Map an interface of methods the same way, one generic at a time.
Why it works
typeof foo without <T> instantiates the generic as unknown. Passing T through foo<T> before Parameters/ReturnType preserves the relation between args and result.
Code
type MakeFunctionAsync<T extends (...args: any[]) => any> =
(...args: ReadonlyArrayItems<Parameters<T>>) => Promise<ReturnType<T>>;
type Foo<T> = MakeFunctionAsync<typeof foo<T>>;
2 score · Accepted✓
View on Stack Overflow ↗Problem
A mapped type needed extra named properties. Adding them in the same mapped type is not supported.
Solution
Intersect MappedType & UsualType, or remap keys with as `${K}${Name}`.
Why it works
Mapped types only describe the homomorphic key transform. Intersection is the supported way to mix in extra fields.
Code
type AddName<T, Name extends string> = {
[K in keyof T as K extends string ? `${K}${Name}` : never]: T[K]
};
2 score · Accepted✓
View on Stack Overflow ↗Problem
A recursive SFC collecting child refs could not type the list: this is gone in script setup, and useTemplateRef inferred the inner instance instead of defineExpose.
Solution
On Vue 3.5+, useTemplateRef infers automatically. Import the same file under an alias (CompChild) so inference uses the exposed public type, not the recursive self instance.
Why it works
A self-import named like the current file resolves to the internal instance type. A different binding points at the exported component type that includes defineExpose.
Code
import CompChild from './Comp.vue';
const $comps = useTemplateRef('$comps');
2 score · Accepted · 588 question views✓
View on Stack Overflow ↗Problem
method's arity needed to match args.length, including rejecting extra parameters and wrong types.
Solution
const T extends any[] on args, then method: (...args: NoInfer<T>) => void so the tuple length is the method's parameter list.
Why it works
const type parameters stop T from widening to string[]. NoInfer keeps the method from driving inference; args is the source of truth.
Code
function myFunction<const T extends any[]>(param: {
args: T;
method: (...args: NoInfer<T>) => void;
}): void {}
2 score · Accepted✓
View on Stack Overflow ↗Problem
A superclass method upcase(propertyName) could not see subclass string fields: this[propertyName] was not a string, and keyof this in the base class is too early.
Solution
Parameterize the base: class CanUpcase<T extends object> and KeysOfType<T, string>. The subclass extends CanUpcase<User>.
Why it works
The base class is compiled without the subclass fields. Feeding User back as T makes propertyName a key that is known to be string on that subclass.
Code
class CanUpcase<T extends object> {
upcase(propertyName: KeysOfType<T, string>) {
return (this[propertyName] as string).toUpperCase();
}
}
class User extends CanUpcase<User> {
name = '';
}
2 score · Accepted · 65 question views✓
View on Stack Overflow ↗Problem
Object.defineProperty added methods at runtime. TypeScript never saw them on the object.
Solution
register() uses asserts this is T & { [p in K]: P['value'] }. After mylib.register('teste', { value(a, b) { return a + b } }), mylib.teste is typed.
Why it works
Assertion functions can grow the type of this. The descriptor's value type becomes the property type.
Code
function register<K extends PropertyKey, T extends {}, P extends PropertyDescriptor>(
this: T, p: K, attributes: P
): asserts this is T & { [p in K]: P['value'] } {
Object.defineProperty(this, p, attributes);
}
2 score · Accepted✓
View on Stack Overflow ↗Problem
A generic mapper(key, value) did not narrow value when key === 'obj'. Evaluation of generic parameters is deferred.
Solution
Drop the free generic. Build a union of tuples from the state type: { [K in keyof IListState]: [key: K, value: IListState[K]] }[keyof IListState].
Why it works
A union of tuples is a discriminated pair. Checking key === 'obj' narrows the sibling value without waiting to instantiate a generic.
Code
type Args = {
[K in keyof IListState]: [key: K, value: IListState[K]]
}[keyof IListState];
2 score · Accepted✓
View on Stack Overflow ↗Problem
A CDK BaseStack collected exportParameter(name) calls. Later stacks needed those names typed, but constructors made the accumulated type hard to thread.
Solution
Prefer factories. exportParameter<N extends string>(name: N, …) returns this as this & BaseStack<{ [k in N]: typeof param }>, so each call grows T.
Why it works
Returning a narrowed this from a method is a typed builder. Constructors cannot change the instance type after new.
Code
exportParameter<N extends string>(name: N, value: string) {
const param = new StringParameter(this, name, { parameterName: name, stringValue: value });
return this as this & BaseStack<{ [k in N]: typeof param }>;
}
2 score · Accepted · 66 question views✓
View on Stack Overflow ↗Problem
A calendar SFC rendered a caller-supplied day component. The generic calendar did not know that child's prop type.
Solution
generic="T extends Record<string, any>" and component: Component<T>. Callers pass SessionLink and data: Record<string, T[]> without extra casts.
Why it works
Component<T> is Vue's type for a component whose props are T. The generic on the calendar unifies data rows with that prop type.
Code
defineProps<{
data: Record<string, T[]>,
component: Component<T>,
labelFn: (length: number, date: Date) => string
}>();
1 score · Accepted · 310 question views✓
View on Stack Overflow ↗Problem
useModel(model, method, options) needed options to depend on method: get vs post have different bags.
Solution
Generic M extends Methods, then options?: M extends 'get' ? { id?: number, params?: … } : M extends 'post' ? { data: Omit<InstanceType<T>, 'id'> } : never.
Why it works
Distributing M through conditional types ties the third argument to the string literal passed as method.
Code
function useModel<T extends typeof BaseApiModel, M extends Methods>(
model: T,
method: M,
options?: M extends 'get' ? { id?: number; params?: QueryFilterParams } : never
) {}
1 score · Accepted✓
View on Stack Overflow ↗Problem
An API returned ingredient1, ingredient2, ingredient3. Indexing drink['ingredient' + i] was an implicit any because those keys are specific properties, not a string index.
Solution
Walk Object.entries, keep keys that start with ingredient and a non-empty value, then map to the values. Adding more ingredientN fields does not change the loop.
Why it works
entries() yields string keys without indexed access on a finite interface. Filtering by prefix avoids synthesizing a template key TypeScript will not accept on DrinkResponseType.
Code
const all_ingredients = drinks.flatMap(drink =>
Object.entries(drink)
.filter(([k, v]) => k.startsWith('ingredient') && v)
.map(([, v]) => v)
);
1 score · Accepted · 96 question views✓
View on Stack Overflow ↗Problem
A tutorial typed Animal as { [key: string]: number } so Tiger could add any numeric field. The asker wanted to know if that is idiomatic.
Solution
That is a dictionary, not an object. Typos like lefs slip through, and you cannot add name: string. Use an Animal interface and extend it (Tiger with color).
Why it works
A string index signature is an open map. An interface is a closed shape you can extend per variant, which is what a Tiger actually is.
Code
interface Animal {
legs: number;
age: number;
tail: number;
eyes: number;
}
interface Tiger extends Animal {
color: 'stripes' | 'black';
}
2 score · Accepted · 80 question views✓
View on Stack Overflow ↗Problem
TypedMap extends Map, called super(entries), and set() touched #keyType. Construction threw because private fields were not initialized yet (Chrome's error was misleading).
Solution
super() must run before private fields exist, but super(entries) already calls this.set(). Pass nothing to super and insert entries afterwards. Constructor type checks can live in set().
Why it works
Private fields are installed on the instance after super returns. Using them during the parent constructor is a deadlock: you cannot read this before super, and after super's Map constructor they still are not there.
Code
class TypedMap extends Map {
#keyType;
constructor(keyType, entries = []) {
super();
this.#keyType = keyType;
for (const [k, v] of entries) this.set(k, v);
}
}
3 score · Accepted✓
View on Stack Overflow ↗Problem
A generated API client needed delete(ids) vs delete(params) with different return types. Wrapping overloads in extra generics was failing.
Solution
Return a nested overloaded function from the factory. Declare the two overloads, then one async implementation. Callers see string[] vs BaseSchema from the call shape.
Why it works
Overload lists are resolved at the call site of the returned function. The factory only has to return that function; it does not need to encode both signatures in its own generic params.
Code
function out(ids: string[], params?: ApiParams): Promise<string[]>;
function out(params: ApiParams, params2?: ApiParams): Promise<BaseSchema>;
async function out(param1: string[] | ApiParams, param2: ApiParams = {}) {
return 1 as any;
}
return out;
1 score · Accepted✓
View on Stack Overflow ↗Problem
Generic checkedValue?: TCheckedValue could not take a boolean default. The default true was not assignable to TCheckedValue.
Solution
Widen the prop: checkedValue?: TCheckedValue | true (and uncheckedValue?: TUncheckedValue | false). Or destructure with defaults: const { checkedValue = true, uncheckedValue = false, ...props } = defineProps<...>().
Why it works
The union includes the default literal, so true is legal. Destructuring defaults type as true | TCheckedValue, which is the same idea without withDefaults.
Code
checkedValue?: TCheckedValue | true;
uncheckedValue?: TUncheckedValue | false;
1 score · Accepted✓
View on Stack Overflow ↗ View All Answers ↗