banner
cos

cos

愿热情永存,愿热爱不灭,愿生活无憾
github
tg_channel
bilibili

Training Camp | "Introduction to TypeScript" Notes

In this lesson, the teacher discussed the uses and basic syntax of TypeScript, the application of advanced types, type protection, and type guards.

What is TypeScript#

Development History#

  • 2012-10: Microsoft released the first version of TypeScript (0.8)
  • 2014-10: Angular released version 2.0 based on TypeScript
  • 2015-04: Microsoft released Visual Studio Code
  • 2016-05: @types/react was released, allowing TypeScript to develop React
  • 2020-09: Vue released version 3.0, officially supporting TypeScript
  • 2021-11: Version 4.5 was released

Why TypeScript#

image.png

Dynamic typing matches types during execution, while JavaScript's weak typing performs implicit type conversion at runtime, which is not the case with static typing.

TypeScript is a static type language: Java, C/C++, etc.

  • Enhanced readability: Based on syntax parsing TSDoc, IDE enhancements
  • Enhanced maintainability: Exposes most errors at compile time
  • In large projects with multiple collaborators, it can achieve better stability and development efficiency.

TypeScript is a superset of JS.

  • It includes compatibility with all JS features, supporting coexistence.
  • Supports progressive adoption and upgrades.

Basic Syntax#

Basic Data Types#

js ==> ts

image.png

As you can see, the type definition in ts is: let variableName: type = value;

TypeScript Basic Types

Object Types#

Interfaces · TypeScript Chinese Website

// Create an object with the following properties, type is IBytedancer
// I indicates a custom type (a naming convention), distinguishing it from classes and objects
const bytedancer: IBytedancer = {
    jobId: 9303245,
    name: 'Lin',
    sex: 'man',
    age: 28,
    hobby: 'swimming',
}
// Define a type as IBytedancer
interface IBytedancer {
	/* Readonly property readonly: constrains the property from being assigned outside of object initialization */
	readonly jobId: number;
    name: string;
    sex: 'man' | 'woman' | 'other';
    age: number;
    /* Optional property: defines that this property may not exist */
    hobby?: string;
    /* Index signature: constrains all object properties to be subtypes of this property */
    [key: string]: any; // any any type
}
/* Error: Cannot assign to "jobId" because it is a readonly property */
bytedancer.jobId = 12345;
/* Success: Under index signature, any property can be added */
bytedancer.platform = 'data';
/* Error: Missing property "name", while hobby can be omitted */
const bytedancer2: IBytedancer = {
    jobId: 89757,
    sex: "woman",
    age: 18,
}

Function Types#

js:

function add(x, y!) {
	return x + y;
}
const mult = (x, y) =>  x * y;

ts:Functions · TypeScript Chinese Website

function add(x: number, y: number): number {
	return x + y;
}
const mult: (x: number, y: number) => number = (x, y) => x * y;
// Simplified syntax, defining interface IMult
interface IMult {
	(x: number, y: number): number ;
}
const mult: IMult = (x, y) => x * y;

As you can see, the format is function functionName(parameter: type...): returnType.

Function Overloading#

/* Overload the getDate function, timestamp is an optional parameter */
function getDate(type: 'string', timestamp?: string): string;
function getDate(type: 'date', timestamp?: string): Date;
function getDate(type: 'string' | 'date', timestamp?: string): Date | string {
    const date = new Date(timestamp);
    return type === 'string' ? date.toLocaleString() : date;
};
const x = getDate('date'); // x: Date
const y = getDate('string', '2018-01-10'); // y: string

The simplified form is as follows:

interface IGetDate {
	(type : 'string', timestamp ?: string): string; // Changing the return type to any here would work
	(type : 'date', timestamp?: string): Date;
	(type: 'string' | 'date', timestamp?: string): Date | string;
}
/* Error: Cannot assign type "(type: any, timestamp: any) => string | Date" to type "IGetDate".
	Cannot assign type "string | Date" to type "string".
	Cannot assign type "Date" to type "string". ts(2322) */
const getDate2: IGetDate = (type, timestamp) => {
	const date = new Date(timestamp); 
	return type === 'string' ? date.toLocaleString() : date;
}

Array Types#

type is used to give a type a new name, similar to typedef in C++.

/* "Type + brackets" indicates */
type IArr1 = number[];
/* Generic representation, these two are the most commonly used */ 
type IArr2 = Array<string | number | Record<string, number>>;
 /* Tuple representation */
type IArr3 = [number, number, string, string];
/* Interface representation */
interface IArr4 {
	[key: number]: any;
}

const arr1: IArr1 = [1, 2, 3, 4, 5, 6];
const arr2: IArr2 = [1, 2, '3', '4', { a: 1 }];
const arr3: IArr3 = [1, 2, '3', '4'];
const arr4: IArr4 = ['string', () => null, {}, []];

TypeScript Supplementary Types#

  • Void type: represents no assignment
  • Any type: is a subtype of all types
  • Enum type: supports forward and backward mapping of enum values to enum names
/* Void type, represents no assignment */
type IEmptyFunction = () => void;
/* Any type, is a subtype of all types */
type IAnyType = any;
/* Enum type: supports forward and backward mapping of enum values to enum names */
enum EnumExample {
    add = '+',
	mult = '*',
}
EnumExample['add'] === '+';
EnumExample['+'] === 'add';
enum EColor { Mon, Tue, Wed, Thu, Fri, Sat, Sun };
EColor['Mon'] === 0;
EColor[0] === 'Mon';
/* Generic */
type INumArr = Array<number>;

TypeScript Generics#

Generics, if you have learned C++, are quite similar: a feature that does not specify a concrete type in advance but specifies the type when used.

function getRepeatArr(target) {
	return new Array(100).fill(target); 
}
type IGetRepeatArr = (target: any) => any[];
/* Does not specify a concrete type in advance but specifies the type when used */
type IGetRepeatArrR = <T>(target: T) => T[];

Generics can also be used in the following scenarios:

/* Generic interface & multiple generics */
interface IX<T, U> {
	key: T;
	val: U;
}
/* Generic class */
class IMan<T> {
	instance: T;
}
/* Generic alias */
type ITypeArr<T> = Array<T>;

Generics can also impose constraints.

/* Generic constraints: restrict generics to conform to strings */
type IGetRepeatStringArr = <T extends string>(target: T) => T[];
const getStrArr: IGetRepeatStringArr = target => new Array(100).fill(target);
/* Error: Argument of type "number" is not assignable to parameter of type "string" */
getStrArr(123);

/* Default type for generic parameters */
type IGetRepeatArr<T = number> = (target: T) => T[]; // Similar to default assignment in structures
const getRepeatArr: IGetRepeatArr = target => new Array(100).fill(target); // Here, IGetRepeatArr is a type alias, and no parameter is passed to this type alias
/* Error: Argument of type "string" is not assignable to parameter of type "number" */
getRepeatArr('123');

Type Aliases & Type Assertions#

Type Assertions#

Sometimes you may encounter situations where you know more about a certain value than TypeScript does. This usually happens when you clearly know that an entity has a more precise type than its existing type.

Through type assertions, you can tell the compiler, "Trust me, I know what I'm doing." Type assertions are similar to type conversions in other languages, but they do not perform special data checks and destructuring. They have no runtime effect and only work at compile time. TypeScript assumes that you, the programmer, have performed the necessary checks.

let someValue: any = "this is a string";

let strLength: number = (someValue as string).length;

Basic Types · TypeScript Chinese Website

/* Defined the alias type IObjArr through the type keyword */
type IObjArr = Array<{
	key: string;
	[objKey: string]: any;
}>
function keyBy<T extends IObjArr>(objArr: Array<T>) {
	/* When type is not specified, result type is {} */
	const result = objArr.reduce((res, val, key) => {
		res[key] = val;
		return res;
	}, {});
    /* Assert result type as the correct type using as keyword */
    return result as Record<string, T>; 
}

In the above code, several points need attention:

reduce() function executes a reducer function you provide on each element of the array (in ascending order), summarizing the results into a single return value.

Syntax: arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])

String/Number Literals#

/* Allows specifying that the string/number must be a fixed value */
/* IDomTag must be one of html, body, div, span */
type IDomTag = 'html' | 'body' | 'div' | 'span';
/* IOddNumber must be one of 1, 3, 5, 7, 9 */
type IOddNumber = 1 | 3 | 5 | 7 | 9;

Advanced Types#

Union/Intersection Types#

Writing types for a book list -> ts type declaration is cumbersome and has a lot of repetition. Advanced Types

const bookList = [ {	// Regular js
	author:'xiaoming',
    type:'history',
    range: '2001 -2021',
}, {
    author:'xiaoli',
    type:'Story',
    theme:'love',
}] 
// ts cumbersome
interface IHistoryBook {
    author: String;
    type: String;
    range: String;
}
interface IStoryBook { 
    author: String;
    type: String;
    theme: String;
}
type IBookList = Array<IHistoryBook | IStoryBook>;
  • Union type: IA | IB; Union types indicate that a value can be one of several types.
  • Intersection type: IA & IB; Multiple types combined into one type, containing all the characteristics of the required types.

The above code can be simplified in ts as follows:

type IBookList = Array<{
	author: string;
} & ({
	type: 'history';
	range: string;
} | {
	type: 'story';
	theme: string;
})>; 
/* Restricts author to be of type string, while type can only be either 'history' or 'story', and different types may have different possible properties. */

Type Protection and Type Guards#

  • When accessing union types, for program safety, only the intersection part of the union type can be accessed.
interface IA { a: 1, a1: 2 }
interface IB { b: 1, b1: 2 }
function log(arg: IA | IB) {
    /* Error: Property "a" does not exist on type "IA | IB". Property "a" does not exist on type "IB".
    Conclusion: When accessing union types, for program safety, only the intersection part of the union type can be accessed. */

	if(arg.a) {
        console.log(arg.a1);
    } else {
        console.log(arg.b1);
    }
}

The above error can be resolved using type guards: define a function whose return value is a type predicate, effective in the child scope.

interface IA { a: 1, a1: 2 }
interface IB { b: 1, b1: 2 }

/* Type guard: define a function whose return value is a type predicate, effective in the child scope */
function getIsIA(arg: IA | IB): arg is IA {
    return !!(arg as IA).a;
}
function log2(arg: IA | IB) {
    /* No error now */
	if(getIsIA(arg)) {
        console.log(arg.a1);
    } else {
        console.log(arg.b1);
    }
}

Or use typeof and instance checks.

// Implement function reverse to reverse an array or string
function reverse(target: string | Array<any>) {
	/* typeof type guard */
    if (typeof target === 'string') {
       return target.split('').reverse().join('');
    }
    /* instance type guard */
    if (target instanceof Object) {
        return target.reverse();
    }
}

It won't always be this complicated; in fact, only when the two types have no overlapping points is a type guard needed. For the book example above, automatic type inference can occur.

// Implement function logBook type
// The function accepts book types and logs relevant features
function logBook(book: IBookItem) {
	// Union type + type guard = automatic type inference
	if (book.type === 'history') {
		console.log(book.range);
    } else {
        console.log(book.theme);
    }
}

Now let's look at a case where we implement a merge function that does not pollute subsets, merging sourceObj into targetObj, sourceObj must be a subset of targetObj.

function merge1(sourceObj, targetObj) {	// In js, implementation is complex, this is how to avoid pollution
    const result = { ...sourceObj };
    for(let key in targetObj) {
        const itemVal = sourceObj[key];
        itemVal && ( result[key] = itemVal );
    }
    return result;
}
function merge2(sourceObj, targetObj) {// If the types of these two parameters are fine, it can be done like this
    return { ...sourceObj, ...targetObj };
}

A simple idea is to write two types in ts for judgment, but this would lead to cumbersome implementation, requiring source to link to remove target, maintaining two copies of x and y.

interface ISourceObj { 
    x?: string; 
    y?: string; 
}
interface ITargetObj {
    x: string;
    y: string;
}
type IMerge = (sourceObj: ISourceObj, targetObj: ITargetObj) => ITargetObj;
/* Type implementation is cumbersome: if the obj types are complex, declaring source and target would require a lot of repetition and is prone to errors: if the target adds/removes keys, the source needs to link to remove. */

By using generics, we can improve this, involving several knowledge points.

  • Partial: A common task is to make every property of a known type optional.

TypeScript provides a way to create new types from old types—mapped types. In mapped types, the new type transforms each property in the old type in the same form. (Just write it, ts has built-in support.)

  • The keyword keyof, which represents all keys in an object as a string literal.
  • The keyword in, which represents one of the possibilities in the string literal, combined with generic P, indicates each key.
  • The keyword ?, by setting object optional options, can automatically infer the subset type.
interface IMerge {
    <T extends Record<string, any>>(sourceObj: Partial<T>, targetObj: T): T;
}
// Internal implementation of Partial
type IPartial<T extends Record<string, any>> = {
            [P in keyof T]?: T[P];
}
// Index type: The keyword [keyof] represents all keys in an object as a string literal, for example
type IKeys = keyof{a: string; b: number}; // => type IKeys ="a" | "b"
// The keyword [in] represents one of the possibilities in the string literal, combined with generic P, indicates each key.
// The keyword [ ? ] sets the object optional options, allowing automatic inference of subset types.

Function Return Value Types#

The return value type of a function is not clear at the time of definition and should also be expressed through generics.

The following code delayCall accepts a function as a parameter, implementing a delay of 1 second before running the function func, returning a promise, with the result being the return result of the input function.

// How to implement the type declaration for the delayCall function
// delayCall accepts a function as a parameter, implementing a delay of 1 second before running the function
// It returns a promise, with the result being the return result of the input function
function delayCall(func) {
    return new Promise(resolve => {
        setTimeout(() => {
            const result = func();
            resolve(result);
        }, 1000);
    });
}
  • The keyword extends when following generics indicates type inference, which can be compared to a ternary expression.

    • For example, T === condition? TypeA: TypeB -> T extends condition? TypeA: TypeB
  • The keyword infer appears in type recommendations, indicating defining a type variable, which can be used to refer to types.

    infer A simple example is as follows:

    type ParamType<T> = T extends (...args: infer P) => any ? P : T;
    

    In this conditional statement T extends (...args: infer P) => any ? P : T, infer P indicates the parameters to be inferred for the function.

    The entire statement means: If T can be assigned to (...args: infer P) => any, then the result is the parameters P in the type (...args: infer P) => any, otherwise return T.

    • Here it is equivalent to referring to the return value type of this function as R.
type IDelayCall = <T extends () => any>(func: T) => ReturnType<T>;
type IReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
    
// The keyword [extends] when following generics indicates type inference, which can be compared to a ternary expression.
// For example, T === condition? TypeA: TypeB
// The keyword [infer] appears in type recommendations, indicating defining a type variable, which can be used to refer to types.
// In this scenario, the return value type of the function is treated as a variable, using the new generic R to represent it, used in the result of the type recommendation.

Engineering Applications#

TypeScript Engineering Applications — Web#

  1. Configure webpack loader related configurations
  2. Configure tsconfig.js file (loose — strict, both can be defined)
  3. Run webpack start/package
  4. Loader processes ts files, compiling and type checking.

Related loaders:

  1. awesome-typescript-loader
  2. or babel-loader

TypeScript Engineering Applications — Node#

Use TSC to compile.

  1. Install Node and npm
  2. Configure tsconfig.js file
  3. Use npm to install tsc
  4. Use tsc to run the compilation to obtain js files.

image.png

Summary and Reflections#

In this lesson, the teacher discussed the uses and basic syntax of TypeScript, compared it with JS, applied advanced types, and further elaborated on type protection and type guards. Finally, he summarized how TypeScript can be applied in engineering. As a superset of JS, TypeScript adds type-checking functionality, which can expose errors in the code at compile time, a feature that dynamic types like JS do not possess. In large projects with multiple collaborators, using TS often leads to better stability and development efficiency.

Most of the content cited in this article comes from Teacher Lin Huang's class and the official TS documentation~

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.