skip to content
usubeni fantasy logo Usubeni Fantasy

TypeScript Generic Parsing

/ 6 min read

This Post is Available In: CN EN ES JA

Introduction to Generics

Generics can be understood as passing types as variables to type definitions, just like passing parameters to functions. For example:

function identity<Type>(arg: Type): Type {
return arg;
}
let output = identity<string>("myString");

By enclosing Type in <>, we can pass the type to the generic function. The effect of the above function is: it accepts an argument of type Type and returns a result of type Type.

Since Type is a parameter, its name can be freely chosen. We often use T as the parameter name, so it can be written like this:

function identity<T>(arg: T): T {
return arg;
}

Type Inference

When using a generic function, you can omit the explicit declaration of the type T (and we usually do). In this case, TypeScript will automatically infer the type of T:

let output = identity("myString");
// output is also of type string

In the example above, if the type <string> is not explicitly specified, TypeScript will directly infer the type of "myString" as T, so the function will return a string.

Drawing a Circle

By default, generics can be any type, which reduces readability. When operating or calling methods on a type that has been “genericized”, it cannot pass the type check because it is of any type. To solve this problem, you can use extends to draw a circle around the generic and restrict its type.

interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // arg must have the length property, checked by type inference
return arg;
}

In the above code, <T extends Lengthwise> indicates that T must be a type with the length property. Any type with the length property satisfies the requirements of this generic, for example, it can also be an array.

Binding Ability

According to the official website, generics are used for type reuse, which is indeed very effective after the simple introduction above. But besides type reuse, what other applications does generics have?

My answer is the linkage of types. T can bind to other generics used within the same type definition.

Take a look at this example again, in fact, it binds the input type and the output type:

function identity<Type>(arg: Type): Type {
return arg;
}

Now let’s look at a more obvious example of “type binding”.

function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a"); // works
getProperty(x, "m"); // error, because `Key` is bound to the key of `Type`, and 'm' is not a key of `Type`

Mapped Types

const myMap = {
a: () => {},
b: (someString: string) => {},
c: (someNumber: number) => {},
};
type MyMap = typeof myMap;
type MyKey = keyof MyMap;

Suppose there is an object with keys ‘a’, ‘b’, and ‘c’, and the values are different functions. Now we need to get the type of an object with keys and corresponding function parameters. How can we achieve this?

const myMap = {
a: () => {},
b: (someString: string) => {},
c: (someNumber: number) => {},
};
function wrapper<K extends keyof typeof myMap>(key: K, fn: (typeof myMap)[K]) {
return async function (...arg: Parameters<typeof fn>) {
// do something
await Promise.resolve();
fn.apply(null, arg);
};
}
const wrappedMap: {
[K in keyof typeof myMap]: ReturnType<typeof wrapper>;
} = {} as any;
for (const key in myMap) {
const k = key as keyof typeof myMap;
wrappedMap[k] = wrapper(k, myMap[k]);
}

Practice

Below is a question. If you are interested, you can think about how to improve the type of the following JS function. (The answer is below, don’t scroll down yet)

Question

First, we have a myMap, but instead of using it directly, we wrap it with a wrapper function. This allows us to perform some pre-processing before executing the function. Now, the question is, how should we define the type of wrappedMap?

const myMap = {
a: () => {},
b: (someString) => {},
c: (someNumber) => {},
};
function wrapper(_key, fn) {
return async function (...arg) {
// do something
await Promise.resolve();
fn.apply(null, arg);
};
}
const wrappedMap = {};
for (const key in myMap) {
const k = key;
wrappedMap[k] = wrapper(k, myMap[k]);
}

Answer

const myMap = {
a: () => {},
b: (someString: string) => {},
c: (someNumber: number) => {},
};
function wrapper<K extends keyof typeof myMap>(key: K, fn: (typeof myMap)[K]) {
return async function (...arg: Parameters<typeof fn>) {
// do something
await Promise.resolve();
fn.apply(null, arg);
};
}
const wrappedMap: {
[K in keyof typeof myMap]: ReturnType<typeof wrapper>;
} = {} as any;
for (const key in myMap) {
const k = key as keyof typeof myMap;
wrappedMap[k] = wrapper(k, myMap[k]);
}
const myMap = {
a: () => {},
b: (someString: string) => {},
c: (someNumber: number) => {},
};
type MyMap = typeof myMap;
type MyKey = keyof MyMap;
function wrapper<K extends MyKey, T extends MyMap[K]>(_key: K, fn: T) {
return async function (...arg: Parameters<T>) {
await Promise.resolve();
(fn as any).apply(null, arg);
};
}
type WrappedMap = {
[K in MyKey]: ReturnType<typeof wrapper<K, MyMap[K]>>;
};
const wrappedMap: Partial<WrappedMap> = {};
for (const key in myMap) {
const k = key as MyKey;
wrappedMap[k] = wrapper(k, myMap[k]);
}

Now it is indeed achieved that the type of WrappedMap is the return value of wrapper, but the line (fn as any).apply(null, arg) seems awkward, doesn’t it?

Why do we still need to set fn as any?

Because for TypeScript, a, b, c are not bound to the parameter types of their values at all, so even if we use T to restrict them, it has no effect. This sentence may be a bit convoluted, but the answer 2 below may be clearer.

Answer 2

const myMap: MyMap = {
a: () => {},
b: (someString: string) => {},
c: (someNumber: number) => {},
};
interface MyMapArgs {
a: [];
b: [someString: string];
c: [someNumber: number];
}
type MyMap = {
[K in keyof MyMapArgs]: (...args: MyMapArgs[K]) => void;
};
type MyKey = keyof MyMap;
function wrapper<K extends MyKey, F extends MyMap[K]>(_key: K, fn: F) {
return async function (...arg: Parameters<F>) {
await Promise.resolve();
fn.apply(null, arg);
};
}
type WrappedMay = {
[K in MyKey]: ReturnType<typeof wrapper<K, MyMap[K]>>;
};
const wrappedMap: Partial<WrappedMay> = {};
for (const key in myMap) {
const k = key as MyKey;
wrappedMap[k] = wrapper(k, myMap[k]);
}

The solution to remove (fn as any) is to first create another map to map the things you need to associate, which is MyMapArgs above, and then use this mapping to create MyMap. This way, TypeScript finally understands that these two things are related.

P.S. For more detailed information, please refer to issues#30581 and pull#47109

评论组件加载中……