Typescript generics, keyof, typeof, index type, mapping

Typescript generics

A major part of software engineering is building components that not only have well-defined and consistent APIs, but are also reusable. Components that can handle today’s and tomorrow’s data will give you the most flexible ability to build large software systems.

Use generic type variables

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

If we want to call the length of arg

When we do this, the compiler will give us an error, we are using the .length member of arg, but we are not saying that arg has this member, these type variables represent any and all types, so someone using this function may Pass in a number representative, which does not have a .length member.

function loggingIdentity<Type>(arg: Type[]): Type[] {
  console.log(arg.length);
  return arg;
}

//or
function loggingIdentity<Type>(arg: Array<Type>): Array<Type> {
  console.log(arg.length); // Array has a .length, so no more error
  return arg;
}

keyof type operator

The keyof operator takes an object type and produces a string or numeric literal union of its keys. The following type p has the same type as type P = “x” | “y”

type Point = { x: number; y: number };
type P = keyof Point;

If the type has a string or number index signature, keyof will return these types:

Reference to index type

type Arrayish={[n:number]:unknown}
type A = keyof Arrayish;
//type A = number

type Mapish = { [k: string]: boolean };
type M = keyof Mapish;
//type M = string | number

typeof type operator return type

Return type

  • Use ReturnType to receive the return type of the basic type
type Predicate = (x: unknown) => boolean;
type K = ReturnType<Predicate>;

// type K = boolean
  • If you use it on an object, you need to use typeof

Change to next

It should be noted here that Typescript intentionally limits the types of expressions you can use typeof.

Specifically, it is only legal to use typeof on an identifier (i.e., a variable name) or its properties. This helps avoid the confusing trap of writing code that you think is executing but isn’t

Index access type

The index access type here is different from the index type. The index type is such as [prop:number]:string, etc. The index access type simply accesses the type through the index.

type Person = { age: number; name: string; alive: boolean };
type Age = Person["age"];
//type Age = number

An index type is basically a type, so we can completely use unions, keyof, or other types

type Person = { age: number; name: string; alive: boolean };

type I1 = Person["age" | "name"];
     
//type I1 = string | number
 
type I2 = Person[keyof Person];
//type I2 = string | number | boolean
 
type AliveOrName = "alive" | "name";
type I3 = Person[AliveOrName];
//type I3 = string | boolean

//If used directly without attributes, an error will be reported.
type I1 = Person["alve"];
Property 'alve' does not exist on type 'Person'.

Another example of using arbitrary types for indexing is using number to get the type of an array element. We can combine this with typeof. Conveniently obtain the element type of an array literal

I have never known how to get the data type of a row of objects in an array, and now I finally have the answer.

Use type when indexing. Const cannot be used otherwise it will not take effect.

Refactoring with types

type key = "age";
type Age = Person[key];

Condition type

Have a good case record and keep it

Condition type constraints

Often a check of a conditional type will give us some new information, just as narrowing with type guards can give us a more specific type, a conditional type true branch will further restrict the generics by the type we check

type MessageOf<T> = T["message"];
Type '"message"' cannot be used to index type 'T'.

In this example, Typescript errors because T does not know that there is a property named message. We can constrain T and Typescript will no longer report errors.

type MessageOf<T extends { message: unknown }> = T["message"];
 
interface Email {
  message: string;
}
 
type EmailMessageContents = MessageOf<Email>;
//type EmailMessageContents = string result display

Infer in conditional type

We just found ourselves using conditional types to apply constraints and then extract the types. This ended up being a common operation, and conditional types made it easier. Conditional types provide us with a way for the infer keyword to infer from the types we compare in our true branch. For example, we can infer the element type in Flatten instead of getting it manually using index access types:

type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;

We use the infer keyword to declaratively introduce a new generic type variable called Item, instead of specifying how to retrieve the element type of Type in the true branch. This is a structure where we don’t have to think about how to dig and explore the types of things we’re interested in.

Mapping type

When you don’t want to repeat yourself, there are times when one genre needs to be based on another

Mapping types are built on the index signature syntax and are used to declare attribute types that have not been declared in advance.

interface Horse={};
type OnlyBoolsAndHorses = {
  [key: string]: boolean | Horse;
};
 
const conforms: OnlyBoolsAndHorses = {
  del: true,
  rodney: false,
};

A mapped type is a generic type that uses a union of PropertyKeys (often created via keyof) and iterates over the keys to create the type:

type OptionsFlags<Type> = {
  [Property in keyof Type]: boolean;
};

The above code will change all properties of type Type and change their value to boolean

Mapping modifier

Two additional modifiers can be applied during mapping: readonly and ? affect mutability and optionality respectively.

These modifiers can be added by prefixing them with – or +. If you don’t add a prefix, + .

  • Move out readonly
// Removes 'readonly' attributes from a type's properties
type CreateMutable<Type> = {
  -readonly [Property in keyof Type]: Type[Property];
};
 
type LockedAccount = {
  readonly id: string;
  readonly name: string;
};
 
type UnlockedAccount = CreateMutable<LockedAccount>;

 //result
type UnlockedAccount = {
    id: string;
    name: string;
}
  • Remove is not required?
// Removes 'optional' attributes from a type's properties
type Concrete<Type> = {
  [Property in keyof Type]-?: Type[Property];
};
 
type MaybeUser = {
  id: string;
  name?: string;
  age?: number;
};
 
type User = Concrete<MaybeUser>;

//result
type User = {
    id: string;
    name: string;
    age: number;
}
  • Remap via as
type MappedTypeWithNewProperties<Type> = {
    [Properties in keyof Type as NewKeyType]: Type[Properties]
}

Property names can be recreated using template literal types

type Getters<Type> = {
    [Property in keyof Type as `get${Capitalize<string & amp; Property>}`]: () => Type[Property]
};
 
interface Person {
    name: string;
    age: number;
    location: string;
}
 
type LazyPerson = Getters<Person>;
//result       
type LazyPerson = {
    getName: () => string;
    getAge: () => number;
    getLocation: () => string;
}

Keys can be filtered out by generating never by conditional type

// Remove the 'kind' property
type RemoveKindField<Type> = {
    [Property in keyof Type as Exclude<Property, "kind">]: Type[Property]
};
 
interface Circle {
    kind: "circle";
    radius: number;
}
 
type KindlessCircle = RemoveKindField<Circle>;
//result          
type KindlessCircle = {
    radius: number;
}

You can map any union, not just the string | number | symbol union, but any type of union:

type EventConfig<Events extends { kind: string }> = {
    [E in Events as E["kind"]]: (event: E) => void;
}
 
type SquareEvent = { kind: "square", x: number, y: number };
type CircleEvent = { kind: "circle", radius: number };
 
type Config = EventConfig<SquareEvent | CircleEvent>
       
type Config = {
    square: (event: SquareEvent) => void;
    circle: (event: CircleEvent) => void;
}

Template literal

The same syntax as a template literal string in JavaScript, but used in type position. When used with a specific literal type, a template literal generates a new string literal type by linking the content.

type World = "world";
 
type Greeting = `hello ${World}`;
//Generate results
type Greeting = "hello world"

A collection of every possible string literal that can be identified by each union member. It is recommended that large string unions be generated in advance and only used in smaller cases.

type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
 
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;

//result
type AllLocaleIDs = "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"