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 ↗