Type operations in Typescript

Posted by plasko on Fri, 14 Jan 2022 10:55:39 +0100

From the official website: https://www.typescriptlang.org/docs/handbook/2/types-from-types.html

TypeScript's type system is very powerful (important) because it allows other types to express types. The simplest form of this idea is generics, and we actually have a variety of type operators to use. We can also express types with values we already have.

 

I generic paradigm

Generic types in typescript are similar to those in other languages. They can be used for both functions and classes. Examples:

 1 function loggingIdentity<Type>(arg: Type): Type {
 2   console.log(arg);
 3   return arg;
 4 }
 5 
 6 interface Lengthwise {
 7   length: number;
 8 }
 9 
10 function loggingIdentityWithLengthWise<Type extends Lengthwise>(arg: Type): Type {
11   console.log(arg.length);
12   return arg;
13 }
14 
15 let u1 = loggingIdentity<string>("Hello");
16 let u2 = loggingIdentityWithLengthWise<string>("Hello")
17 
18 class GenericNumber<NumType> {
19   zeroValue: NumType;
20   add: ((x: NumType, y: NumType) => NumType);
21 }
22 
23 let myGenericNumber = new GenericNumber<number>();
24 myGenericNumber.zeroValue = 0;
25 myGenericNumber.add = function (x, y) {
26   return x + y;
27 };
28 console.log(myGenericNumber.add(3,2));

When you create a factory using generics in TypeScript, you need to reference the class type through its constructor. For example,

 1 class BeeKeeper {
 2   hasMask: boolean = true;
 3 }
 4 
 5 class ZooKeeper {
 6   nametag: string = "Mikle";
 7 }
 8 
 9 class Animal {
10   numLegs: number = 4;
11 }
12 
13 class Bee extends Animal {
14   keeper: BeeKeeper = new BeeKeeper();
15 }
16 
17 class Lion extends Animal {
18   keeper: ZooKeeper = new ZooKeeper();
19 }
20 
21 function createInstance<A extends Animal>(c: new () => A): A {
22   return new c();
23 }
24 
25 createInstance(Lion).keeper.nametag;
26 createInstance(Bee).keeper.hasMask;

 

II keyof, typeof

keyof

Gets a collection of all key s of type

 1 interface Person {
 2   name: string;
 3   age: number;
 4 }
 5 type personKeys = keyof Person;
 6 //Equivalent to: type personKeys = 'name' | 'age'
 7 let p1 = {
 8   name: 'thia',
 9   age: 30
10 }
11 function getPersonVal (k: personKeys) {
12   return p1[k]
13 }
14 /**Equivalent to
15 function getPersonVal(k: 'name' | 'age'){
16   return p1[k]
17 }
18 */
19 getPersonVal('name')   //ok
20 getPersonVal('gender')   //error

typeof

typeof has two functions in typescript:

  • Gets the type of data
  • Type of captured data
1 let str1 = 'hello';
2 //let Declared variables 'string' As value
3 let t = type of str1;   
4 //type The declared type is 'string'As type
5 type myType = typeof str1;
6 
7 let a = t;
8 let str2: myType = 'world';

 

III Index access type

We can use the index access type to find a specific attribute of another type:

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

The index type itself is a type, so we can completely use the union keyof, or other types:

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 you try to index a property that does not exist, you may even see an error:

type I1 = Person["alve"];
Property 'alve' does not exist on type 'Person'.
 

IV Condition type

The form of condition type is a bit like condition? Trueexpression: conditional expression () in false expression javascript:

SomeType extends OtherType ? TrueType : FalseType;

 

When the type extensions on the left can be assigned to the type on the right, you will get the type in the first branch (the "true" branch); Otherwise, you get the type in the latter branch (the "false" branch).

From the above example, the condition type may not be immediately useful - we can tell ourselves whether Dog extends Animal selects number or string! But the power of conditional types is to use them with generics.

Example:

 1 interface IdLabel {
 2   id: number /* some fields */;
 3 }
 4 interface NameLabel {
 5   name: string /* other fields */;
 6 }
 7  
 8 function createLabel(id: number): IdLabel;
 9 function createLabel(name: string): NameLabel;
10 function createLabel(nameOrId: string | number): IdLabel | NameLabel;
11 function createLabel(nameOrId: string | number): IdLabel | NameLabel {
12   throw "unimplemented";
13 }
14 
15 type NameOrId<T extends number | string> = T extends number
16   ? IdLabel
17   : NameLabel;
18 
19 function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
20   throw "unimplemented";
21 }
22  
23 let a = createLabel("typescript");
24  
25 let b = createLabel(2.8);
26    
27 let c = createLabel(Math.random() ? "hello" : 42);

Condition type constraint

The following example lets the typescript template know that the incoming type has a property called message

1 type MessageOf<T extends { message: unknown }> = T["message"];
2  
3 interface Email {
4   message: string;
5 }
6  
7 type EmailMessageContents = MessageOf<Email>;

Infer in condition type

We just found ourselves using conditional types to apply constraints and then extract types. This eventually becomes a common operation, and condition types make it easier.

Conditional types provide us with a way to infer the types we compare in real branches using the infer keyword. For example, we can infer the element type, Flatten, instead of getting it "manually" using the index access type:

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

Here, we use the infer keyword to declaratively introduce a new generic type variable Item, instead of specifying how to retrieve the element type in the real branch. T his makes it unnecessary for us to consider how to mine and explore the structure of the types we are interested in.

Infor we can use keywords to write some useful auxiliary type aliases. For example, for simple cases, we can extract the return type from the function type:

1 type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
2   ? Return
3   : never;
4  
5 type Num = GetReturnType<() => number>;
6  
7 type Str = GetReturnType<(x: string) => string>;
8  
9 type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>;

When inferring from a type with multiple call signatures (such as the type of an overloaded function), inferring is based on the last signature (this may be the most relaxed and all inclusive case). Overload resolution cannot be performed based on parameter type list.

 

declare function stringOrNum(x: string): number;
declare function stringOrNum(x: number): string;
declare function stringOrNum(x: string | number): string | number;
 
type T1 = ReturnType<typeof stringOrNum>;
type T1 = string | number
 

V mapping type

When you don't want to repeat yourself, sometimes one type needs to be based on another. The mapping type is based on the syntax of the index signature and is used to declare attribute types that are not declared in advance:

Example: OptionsFlags will get all attribute types from the Type and change their values to bool Type

 1 type OptionsFlags<Type> = {
 2   [Property in keyof Type]: boolean;
 3 };
 4 
 5 type FeatureFlags = {
 6   darkMode: () => void;
 7   newUserProfile: () => void;
 8 };
 9  
10 type FeatureOptions = OptionsFlags<FeatureFlags>;
11 
12 // type FeatureOptions = {
13 //     darkMode: boolean;
14 //     newUserProfile: boolean;
15 // }

In TypeScript 4.1 and later, you can remap keys in a mapping type using the as clause in the mapping type:

type MappedTypeWithNewProperties<Type> = {
  [Properties in keyof Type as NewKeyType]: Type[Properties]
}
Can use Template text type And other functions to create a new attribute name from the previous attribute name:
 
 1 type Getters<Type> = {
 2     [Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]
 3 };
 4  
 5 interface Person {
 6     name: string;
 7     age: number;
 8     location: string;
 9 }
10  
11 type LazyPerson = Getters<Person>;
12 // type LazyPerson = {
13 //     getName: () => string;
14 //     getAge: () => number;
15 //     getLocation: () => string;
16 // }
 

Vi Template text type

Template text types are based on String text type And can be extended into many strings through union.

They are similar to template text strings in JavaScript Has the same syntax, but is used for type location. When used with a specific text type, the template text generates a new string text type by connecting the content.

When unions are used at interpolation locations, the type is a collection of each possible string literal that can be represented by each union member. For each interpolation position in the template text, the union is cross multiplied:

1 type EmailLocaleIDs = "welcome_email" | "email_heading";
2 type FooterLocaleIDs = "footer_title" | "footer_sendoff";
3  
4 type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
5 // type AllLocaleIDs = "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"

Reasoning using template text

 

Note that we did not benefit from all the information provided in the original delivery object. Given the change of a firstName (that is, a firstNameChanged event), we should expect the callBack to receive a parameter string of type. Similarly, the changed callBack age should receive a number parameter. We naively use any to enter the callBack 'parameter. Similarly, the template text type ensures that the data type of the attribute is the same as the first parameter type of the callBack of the attribute.

The key insight that makes this possible is that we can use functions with generics, such as:

  1. The text used in the first parameter is captured as a text type
  2. The literal type can be verified in the union of valid attributes in the generic type
  3. You can use Indexed Access to find the type of a validated property in a generic structure
  4. You can then apply this type information to ensure that the parameters of the callback function are of the same type
 1 type PropEventSource<Type> = {
 2     on<Key extends string & keyof Type>
 3         (eventName: `${Key}Changed`, callback: (newValue: Type[Key]) => void ): void;
 4 };
 5  
 6 declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;
 7  
 8 const person = makeWatchedObject({
 9   firstName: "Saoirse",
10   lastName: "Ronan",
11   age: 26
12 });
13  
14 person.on("firstNameChanged", newName => {
15     console.log(`new name is ${newName.toUpperCase()}`);
16 });
17  
18 person.on("ageChanged", newAge => {
19                           
20     if (newAge < 0) {
21         console.warn("warning! negative age");
22     }
23 })

Here we make on a generic method.

When the user invokes "firstNameChanged" using a string, TypeScript will attempt to infer the Key. To do this, it will match "Changed" with the content before the Key and infer the string "firstName". Once TypeScript determines this, the on method can obtain the type of the original object of firstName, as is the case with string in this example. Similarly, when calling "ageChanged" ", TypeScript will find the age type number of the property

Reasoning can be combined in different ways, usually deconstructing strings and reconstructing them in different ways.