Are you out of TypeScript

preface

TypeScript by now, the language features are quite numerous and complex. This document focuses on language features that are particularly useful and, if used flexibly, can greatly improve the coding experience. In order to cover as much of the useful knowledge as possible, features are only briefly introduced, and these types of benefits can be accumulated in actual coding.

Generic

The template looks like this

function firstElement<Type> (arr: Type[]) :Type {
  return arr[0];
}
const s = firstElement(["a"."b"."c"]);
// s is of type 'string'
const n = firstElement([1.2.3]);
// n is of type 'number'
Copy the code

inference

function map<I.O> (arr: I[], func: (arg: I) => O) :O[] {
  return arr.map(func);
}
Copy the code

The map function dynamically deduces the type of I and O based on the parameters passed in

Type constraints

function strOrNum<T extends (number | string) > (input: T) :T {
    return input
}
// The argument passed in must be a string or a number
Copy the code

Generic type

Generics are more widely used in the field of generic types

interface Box<T> {
 content: T;
}
type Box<T> = T | Array<T>
Copy the code

Function Overloads

function makeDate(timestamp: number) :Date;
function makeDate(m: number, d: number, y: number) :Date;
function makeDate(mOrTimestamp: number, d? :number, y? :number) :Date {
  if(d ! = =undefined&& y ! = =undefined) {
    return new Date(y, mOrTimestamp, d);
  } else {
    return new Date(mOrTimestamp); }}const d1 = makeDate(12345678); // correct
const d2 = makeDate(5.5.5); // correct
const d3 = makeDate(1.3); // error!
Copy the code

The above function has three overloaded signatures, and an error will be reported if the function call does not match any of them.

This statement

This must be declared first in the function argument declaration, and this is not declared as a parameter or argument.

class Handler {
    info: string | undefined
    handle(this: Handler, msg: string){[this](http://this.info/)[.info](http://this.info/) = msg}}const h = new Handler()
h.handle('hello');
Copy the code

Special type

void

Void represents the return value of a function that does not return a value. Also, a context type of return type void does not force a function not to return something. Another is a context function type with a void return type (type vf = () => void) that, when implemented, can return any other value but is ignored.

function noop() {
  return;
}
// function noop(): void


type voidFunc = () = > void;
const func: voidFunc = () = > {
  return true;
};
const value = func();
// const value: void
// If func returns a value, it does not matter; value is of type void
Copy the code

This behavior exists, so the following code is valid even if array.prototype. push returns a number and array.prototype. forEach requires a function that returns type void.

const src = [1.2.3];
const dst = [0];
src.forEach((el) = > dst.push(el));
Copy the code

unknown

Unknown is similar to any, but more secure than any, because any operation performed through unknown is illegal. So use unknown instead of any. Here’s an example with a new feature in typescirpt4.0.

try {
    // do something crazy here
} catch(e) {
    console.error(e.message, 'error occur')
    // By default, e is of type any
}

try {
    // do something crazy here
} catch(e: unknown) {
    console.error(e? .message,'error occur')
    // After typescript 4.0, catch here supports type notation. So it's safer to use unknown to define e otherwise any is like streaking. It's easy to get a Type error
}

Copy the code

never

The never type represents values that have never been observed. In return types, this means that the function throws an exception or terminates the execution of the program.

function fail(msg: string) :never {
 throw new Error(msg)
}

function fn(x: string | number) {
  if (typeof x === "string") {
    // do something
  } else if (typeof x === "number") {
    // do something else
  } else {
    x; // has type 'never'!}}Copy the code

Merging of functions by Declaration.

Interface with

interface Box {
 height: number;
 width: number;
}
interface Box {
 weight: number;
}
let box: Box = {height: 4.with: 5.weight: 6}
Copy the code

Interfaces with the same name will be merged

The e difference between type and interface: An interface can extends, but not type. Interface is an object (and currently only [] and {} are supported), whereas type can be used to play around. Because the interface is essentially needed to support {} | [] some constraints

Namespaces merger

Like interfaces, namespaces are incorporated. A namespace can contain types and values.

namespace Animals {
  export class Zebra {}}namespace Animals {
  export interface Legged {
    numberOfLegs: number;
  }
  export class Dog {}}/ / is equivalent to
namespace Animals {
  export interface Legged {
    numberOfLegs: number;
  }
  export class Zebra {}
  export class Dog {}}Copy the code

If you encounter duplicate named types or values and both are exported, an error will be reported during the merge. (Even with namespaces of the same name, values or types that are not exported are not accessible from other namespaces.)

Assertion & Type Checker & Type narrowing

type narrowing

First, let’s learn about Type narrowing. There are many ways for type narrowing, mainly including the following ways

  • typeof type guard

  • Truthiness narrowing

  • Equality narrowing

  • The in operator narrowing

  • instanceof narrowing

Typeof and instanceof

Type convergence is also possible using typeof and instanceof keyword determination.

function strOrNum(input: string | number) {
  if (typeof input === 'string') {
    // input is string now
  } else {
    // input is number}}Copy the code

Truthiness and equality,

The two approaches are also very similar, so they are combined.

declare const obj: Record<string, unknown> | null
declare const bool1: boolean | undefined
declare const bool2: boolean | null

if (obj) {
    // obj: Record<string, unknown>
} else {
    // obj: null
}

if (obj) {
    // obj: Record<string, unknown>
} else {
    // obj: null
}

if (bool1 === bool2) {
    // bool1: boolean
    // bool2: boolean
}
Copy the code

inThe operator

The in operator is useful for determining whether an object contains a property or method, and can be used in typescript to achieve type-Narrrowing

type Fish = { swim: () = > void };
type Bird = { fly: () = > void };
 
function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    return animal.swim();
  }
 
  return animal.fly();
}
Copy the code

as

There are several ways of making assertions. The simplest is to use the as keyword.

declare const str: any
let res = (str as string).toUpperCase()

declare const num: number
let res2 = (num as unknown as string).toLowerCase()
// If the num type cannot converge directly, assert num as a broader type, such as unknown
Copy the code

Type Guards Type Guards

Type convergence can be achieved by implementing type guard functions yourself. Type guards are more powerful than Typeof.

type Fish = { swim: () = > void };
type Bird = { fly: () = > void };
declare function getSmallPet() :Fish | Bird;
// ---cut---
function isFish(pet: Fish | Bird) :pet is Fish {
  return (pet asFish).swim ! = =undefined;
}

let pet = getSmallPet();

if (isFish(pet)) {
  // The pet type converges to the Fish type
  pet.swim();
} else {
  pet.fly();
}
Copy the code

Assert functions

There is a specific set of functions that throw an error if something unexpected happens. They are called “assertion” functions. For example, Node.js has a special function called assert.

function multiply(x: any, y: any) {
  assert(typeof x === "number");
  assert(typeof y === "number");

  // both x and y is number now
  return x * y;
}
Copy the code

Assert functions can be customized in Typescript. There are two forms of assert function definition

The first kind of

function myAssert(param: any, msg? :string) :asserts param {
  if (! param) {
    throw new Error(msg); }}function multiply(x: any, y: any) {
  myAssert(typeof x === "number");
  myAssert(typeof y === "number");
  // both x and y is number now
  return x * y;
}
Copy the code

The second,

declare function isOdd(param: unknown) :asserts param is 1 | 3 | 5;
function num(input: number) {
  isOdd(input)
  console.log(input)
  // input is 1 | 3 | 5
}
Copy the code

! The suffix

! Suffixes can remove null and undefined from types

declare const obj: {name: string} | null
constname = obj! .name// obj is asserted to {name: string}
Copy the code

For type security, try not to use this identifier.

Type Assertion of third-party library implementation

GitHub – unional/type-plus: Additional types and types adjusted utilities for TypeScript

Mapped Types

In an identifier

A mapping type is a generic type that uses a union of PropertyKeys (typically created by keyof) to iterate over keys to create a type. The IN identifier iterates over Type, returning key

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

+ – Identifier

  • Adds attributes, and – removes attributes. The + sign can be left out, but added to improve readability. Examples are as follows
type Student = {
  id: string;
  readonly name? :string;
};
type CreateMutable<Type> = {-readonly [Property in keyof Type] :Type[Property];
};
type UnlockedAccount = CreateMutable<LockedAccount>;// { // id: string; // name? : string; / /}type CreateImmutable<Type> = {+readonly [Property in keyof Type] :Type[Property];
};// { // readonly id: string; // readonly name? : string; / /}type CreateOptional<Type> = {[Property in keyof Type] +? :Type[Property];
};// { // id? : string; // readonly name? : string; / /}type CreateRequired<Type> = {[Property in keyof Type] -? :Type[Property];
};
// {
//    id: string;
//    readonly  name: string;
// }
Copy the code

as

New notations are added to TypeScript4.1 to support powerful Template Literal Types, as discussed below.

type MappedType<Type> = {[Properties in keyof Type as (string | number)]: Type[Properties]}

type Getters<Type> = {[Property in keyof Type as `getThe ${Capitalize<string & Property(1) = > >} `] :Type[Property]
};

type RemoveKindField<Type> = {[Property in keyof Type as Exclude<Property,"kind">] :Type[Property]};
Copy the code

Conditional Type

The following forms are conditional types. Can do a lot of SAO operation key.

SomeType extends OtherType ? TrueType : FalseType;
Copy the code

Recursive Conditional Types

Typescript4.1 is introduced. With recursive conditional types, various iterations of the loop can be supported. With this feature you can write many advanced types.

type ElementType<T> = T extends ReadonlyArray<infer U> ? ElementType<U> : T;
Copy the code

An error is reported if the recursion depth exceeds 50 levels. This is particularly nice.

Template Literal Types

Typescript4.1 is also introduced.

type World = "world";

type Greeting = `hello ${World} `;
// "hello world"
Copy the code
type EnumNum = 1 | 2 | 3
type EnumGroup = `${EnumNum}${EnumNum}`
12 / / "11" | "" |" 13 "|" 21 "|" 22 "|" 23 "|" 31 "|" 32 "|" 33"
Copy the code
type PropEventSource<Type> = {
    on<Key extends string & keyof Type>
        (eventName: `${Key}Changed`.callback: (newValue: Type[Key]) = > void) :void;
};

declare function makeWatchedObject<Type> (obj: Type) :Type & PropEventSource<Type>;

const person = makeWatchedObject({
  firstName: "Saoirse".lastName: "Ronan".age: 26
});

person.on("firstNameChanged".newName= > {
    // ^ string
    console.log(`new name is ${newName.toUpperCase()}`);
});

person.on("ageChanged".newAge= > {
    // ^ number
    if (newAge < 0) {
        console.warn("warning! negative age"); }})Copy the code

With template Literal types, you can implement a lot of types that you couldn’t implement before. The following example implements a camelCase conversion

type ToCamel<S extends string> =
    S extends `${infer Head}-${infer Tail} `? ` ${Lowercase<Head> ${}Capitalize<ToCamel<Tail> >} ` :S extends Uppercase<S>?Lowercase<S>
    : S extends` ${Capitalize<infer Head> ${}Capitalize<infer Tail>}`
    ? S
    : void
type ToCamelCase<S extends string | number | bigint> = Uncapitalize<ToCamel<S & string>>

type T1 = ToCamelCase<"camel-case"> // camelCase
type T2 = ToCamelCase<"camelCase"> // camelCase
type T3 = ToCamelCase<"CamelCase"> // camelCase
type T4 = ToCamelCase<"CAMELCASE"> // camelcase
Copy the code

Variadic Tuple Types

New features in Typescript4.0.

type Foo<T extends unknown[]> = [string. T,number];

type T1 = Foo<[boolean>;// [string, boolean, number]
type T2 = Foo<[number.number>;// [string, number, number, number]
type T3 = Foo<[]>;  // [string, number]
Copy the code
export type FirstElem<T extends readonly unknown[]> = T[0];
export type LastElem<T extends readonly unknown[]> = T extends readonly [...infer _, infer U] ? U : undefined;

export type RemoveFirst<T extends readonly unknown[]> = T extends readonly [unknown, ...infer U] ? U : [...T];
[unknown,...infer U] can be replaced with [any,...infer U]
export type RomoveLast<T extends readonly unknown[]> = T extends readonly [...infer U, unknown] ? U : [...T];
[...infer U, unknown] can be replaced with [...infer U, any]

type TS1 = FirstElem <[number.string.boolean>;// number
type TS2 = LastElem <[number.string.boolean>;// boolean
type TS3 = RemoveFirst <[number.string.boolean>;// [string, boolean]
type TS4 = RomoveLast <[number.string.boolean>;// [number, string]
Copy the code

Let’s look at a practical example. There is a function merge, which implements the merge input object. You can enter two or more objects. Let’s see how this type is implemented.

type Merge<F extends Record<string | number, unknown>, S extends Record<string | number, unknown>> = {
  [P in keyof F | keyof S]: P extends keyof Omit<F, keyof S> ? F[P] : P extends keyof S ? S[P] : never;
};

type MergeArr<T extends Record<string | number, unknown>[]> = T['length'] extends 0
  ? {}
  : T['length'] extends 1
  ? T[0]
  : T extends [infer P, infer Q, ...infer U]
  ? P extends Record<string | number, unknown>
    ? Q extends Record<string | number, unknown>
      ? U extends Record<string | number, unknown>[]
        ? MergeArr<[Merge<P, Q>, ...U]>
        : MergeArr<[Merge<P, Q>]>
      : never
    : never
  : never;

declare function mixin<
  T extends Record<string | number.unknown>,
  P extends [Record<string | number.unknown>,...Record<string | number.unknown> > []] (target: T, ... rest: P) :MergeArr"[T.P] >const a = mixin({name: 'joe'}, {sex: 'male'}, {haveFun: [1.2.3]}) / /{name: string; sex: string; haveFun: number[]} 
 
 /* ** This is all a hassle because typescript is not smart enough ** the following code should be enough, But typescirpt error type MergeArr < T extends Record < string | number, unknown > [] > = T [' length '] extends 0? {} : T['length'] extends 1 ? T[0] : T extends [infer P, infer Q, ...infer U] ? U extends Record
      
       [] ? MergeArr<[Merge
       

, ...U]> : MergeArr<[Merge

]> : never; * * the other If the typescript support this writing should also can avoid the considerable redundancy code But the typescript still does not support the type MergeArr extends Record < < T string | number, unknown>[]> = T['length'] extends 0 ? {} : T['length'] extends 1 ? T[0] : T extends [infer P, infer Q, ...infer U] ? U extends Record [] ? [P, Q] extends [Record , Record ] ? MergeArr<[Merge

, ...U]> : never : MergeArr<[Merge

]> : never; * /

,>

,> ,> ,>
Copy the code

Infer

The infer identifier appeared above. The meaning of the infer identifier is to infer the type of the infer location according to the input value. This concept may seem abstract at first, but as you read more code you will understand it. For details, please refer to the official website

// used in tuple
type InferSecondElem<T extends unknown[]> = T extends [unknown, infer P. unknown[]] ?P : void

type A = InferSecondElem<[number, boolean, unknown, string]> // boolean

// used in template
type Split<S extends string.D extends string> =
    string extends S ? string[] :
    S extends '' ? [] :
    S extends `${infer T} ${D}${infer U}` ? [T.Split<U.D>] :
    [S];

type B = Split<'split,by,comma', ','> // ["split", "by", "comma"]

// function type
type RetType<T extends (. args: any) = > any> = T extends(... args: any) => inferR ? R : any;

type Res = RetType<() => number> // number
Copy the code

Type utils

Some common Utility types are provided. Most of these are in lib.es5.d.ts files in typescript packages. There is also a lot of encapsulation in the community. Here, for example. Can achieve addition, subtraction, multiplication and division and other operations.