Home > Enterprise >  infer types per key to multiple mapping objects/types
infer types per key to multiple mapping objects/types

Time:08-05

I am wondering how to create generic type mappings.

As you can see on the image and/or in the playground..

The code "runs" fine, but the typings are somehow weird/mixed.

Imagine there is even another mapping to a Subject, so I want to be able to do something like this, type-safe of course, .. :

subjMap[key].next(accessorMap[key]());

Is this even possible with different objects/maps.. Or do I need to create one object per "key" so to say..?

type KeyToTypeMapping = {
    iaModel: string;
    debug: boolean;
};

type MappingKeys = keyof KeyToTypeMapping;

type KeyToTypedAccessor<T> = Record<keyof T, () => T[keyof T]>;

const accessorMap: KeyToTypedAccessor<KeyToTypeMapping> = {
    iaModel: () => "string",
    debug: () => true
};

const stringResult = accessorMap.iaModel();

// TS tells me that the type of `stringResult` is string | boolean
console.log(stringResult);

type KeyToTypedFn<T> = {
    [Key in keyof T]: { next: (_: T[Key]) => void };
};

// here is another map with the same keys and "base-types"..
// .. there could be even more ..
const subjMap: KeyToTypedFn<KeyToTypeMapping> = {
    iaModel: { next: (_) => { console.log('next', _); } },
    debug: { next: (_) => { console.log('next', _); } },
}

Object.keys(accessorMap).forEach(k => {
    // sadly we need to cast here..
    const typedKey = k as keyof KeyToTypeMapping;

    const val = accessorMap[typedKey]();

    // here TS complains that val (type of string | boolean) is not assignable to never ..
    subjMap[typedKey].next(val);
});

CodePudding user response:

Just don't use Record so you can access the key directly !

type KeyToTypeMapping = {
    iaModel: string;
    debug: boolean;
};

type KeyToTypedAccessor<T> = { [P in keyof T]: () => T[P] }

declare const accessorMap: KeyToTypedAccessor<KeyToTypeMapping>

const stringResult = accessorMap.iaModel();

Playground

CodePudding user response:

I think you're looking for a mapped type, which would look like this:

type KeyToTypedAccessor<T> = {
    [Key in keyof T]: () => T[Key];
};

Playground link

Full Example:

type KeyToTypeMapping = {
    iaModel: string;
    debug: boolean;
};

type KeyToTypedAccessor<T> = {
    [Key in keyof T]: () => T[Key];
};

const accessorMap: KeyToTypedAccessor<KeyToTypeMapping> = {
    iaModel: () => "string",
    debug: () => true
};

const stringResult = accessorMap.iaModel();
//    ^? −−−− string
const booleanResult = accessorMap.debug();
//    ^? −−−− boolean

console.log(stringResult);

In a comment you said you were stuck at this point:

Object.keys(accessorMap).forEach(k => {
    // sadly we need to cast here..
    const typedKey = k as keyof KeyToTypeMapping;

    const val = accessorMap[typedKey]();

    // here TS complains that val (type of string | boolean) is not assignable to never ..
    subjMap[typedKey].next(val);
});

Unfortunately, you can't fix that in a typesafe way without doing something so that TypeScript knows either:

  1. Which key of KeyToTypeMapping typedKey is, so it knows what the value type is. It could be either (iaModel leading to a string value, or debug leading to a boolean value).

    or

  2. That the same KeyToTypeMapping key is being used both for indexing into subjMap and accessorMap.

#1 leads to some fairly ugly repetitive code, so we don't want that (in this case).

Re #2, you'd think TypeScript would know from your code (or the even-more-obvious subjMap[typedKey].next(accessorMap[typedKey]();) that the key is the same, but it wants a generic function to help it:

function setNext<Key extends keyof KeyToTypeMapping>(
    subjMap: KeyToTypedFn<KeyToTypeMapping>,
    accessorMap: KeyToTypedAccessor<KeyToTypeMapping>,
    key: Key,
) {
    subjMap[key].next(accessorMap[key]());
}

Then we can do:

Object.keys(accessorMap).forEach((key) => {
    // (As you say, you're stuck with the type assertion on `key`.)
    setNext(subjMap, accessorMap, key as any as keyof KeyToTypeMapping);
});

Playground link


(Side note: Type assertions aren't casts. A cast [in languages that have them] can actually convert a value from one thing to another, for instance converting a two's complement int bit pattern to a IEEE-754 double-precision floating point double bit pattern with an equivalent or near-equivalent numeric value despite the fact the bit patterns for [say] 42 are very different between those two formats. TypeScript's type assertions never do any kind of conversion. They don't even exist at runtime. So they're just assertions, just you telling TypeScript "trust me, this ? is an X." :-) )

  • Related