Home > Software design >  What does the `as` keyword actually do inside a mapped type?
What does the `as` keyword actually do inside a mapped type?

Time:12-06

I am a novice in typescript, The following example of as confuses me.

type foo = "a" | "b" | 1 | 2;
type bar = {
    [k in foo as number]: any
}

The example is type checked. and bar type is translated into

type bar = {
    [x: number]: any
}

why k in foo as number can be translated into x: number?

CodePudding user response:

The as keyword allow us to create type assertions.

In

type bar = {
    [k in foo as number]: any
}

k should be keyof foo, but since you are using as to asserts the type with number, bar will be

type bar = {
  [k: number]: any
}

For example, take this little function:

const handlePost = async (request: Request) => {
  const requestBody = await request.formData();
  const name = requestBody.get("name");
  const type = requestBody.get("type");
  ...
};

In this example, name and type are of type FormDataEntryValue | null so we should check if they are a File, string or null.

If we are pretty sure that the name attribute in our form is correct, and that we are sending a plain string, we could do the following:

const handlePost = async (request: Request) => {
  const requestBody = await request.formData();
  const name = requestBody.get("name") as string;
  const type = requestBody.get("type") as string;
  ...
};

Now name and type are both string.

CodePudding user response:

Your code is using key remapping in mapped types. This is not a type assertion. Both key remapping and type assertions can use the as keyword, but they are for quite different purposes; it is almost coincidental that they use the same keyword.

A normal non-remapped mapped type is of the form {[T in KK]: V<T>} which defines a type parameter T that iterates over the union members of the keylike type KK (which must be a subtype of PropertyKey, a.k.a. string | number | symbol), and then for each of these union members T, the output has a property with key T and value V<T> (where V is some type function, like type V<T> = {x: T}). So the value type can be any function of the type parameter T, but the key type is restricted to be just T itself. (Because the type parameter is required to be keylike, the name of the type parameter often reflects that by being something like K for Key, or P for Property.) This is very useful, but sometimes people want to compute the keys as well as the values.

Key remapping lets you have the type parameter iterate over any union whatsoever, (not just keylike types), and then you compute the keys and the values from that type parameter. It's of the form {[T in U as K<T>]: V<T>} which defines a type parameter T that iterates over the union members of the type U, and then for each of these union members T, the output has a property with key K<T> (which must be a keylike type function, like type K<T extends string> = `key_${T}`) and value V<T>.

Your example

type Foo = "a" | "b" | 1 | 2;
type Bar = {
  [K in Foo as number]: any
}

is a degenerate case, since you are iterating over Foo but completely ignoring it when computing the key type. Instead, the key type is number, no matter what K is. So the output will have a single number index signature with value type any.

If you wanted a slightly more realistic example, you could compute the key as a template literal type that depends on K, like this:

type Baz = {
  [K in Foo as `key_${K}`]: any
}
/* type Baz = {
    key_a: any;
    key_b: any;
    key_1: any;
    key_2: any;
} */

And if you want an example where you iterate over a non-key type, it could look like this:

type KVPair = { k: "a", v: string } | { k: "b", v: number } | { k: "c", v: boolean };
type Qux = { [T in KVPair as T["k"]]: T["v"] };
/* type Qux = {
    a: string;
    b: number;
    c: boolean;
} */

This is quite a powerful feature, which again, has essentially nothing to do with type assertions.

Playground link to code

  • Related