Youth Training Camp | "Introduction to TypeScript" Notes

发表于 2022-01-28 14:30 2897 字 15 min read

cos avatar

cos

FE / ACG / 手工 / 深色模式强迫症 / INFP / 兴趣广泛养两只猫的老宅女 / remote

本文介绍了TypeScript的发展历程、核心语法与优势,强调其作为JavaScript超集在静态类型检查、代码可读性、可维护性及团队协作中的重要作用。通过基础类型、高级类型(如联合、交叉、泛型、映射类型)以及类型保护与守卫,深入讲解了TypeScript在实际工程中的应用,包括Web和Node项目中的配置与使用。

This article has been machine-translated from Chinese. The translation may contain inaccuracies or awkward phrasing. If in doubt, please refer to the original Chinese version.

This lesson covered TypeScript’s use cases and basic syntax, advanced type applications, 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, enabling TypeScript for React development
  • 2020-09: Vue released version 3.0 with official TypeScript support
  • 2021-11: v4.5 was released

Why TypeScript

image.png

Dynamic types perform type matching during execution. JS’s weak typing performs implicit type conversion at runtime, while static types do not.

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

  • Enhanced readability: TSDoc-based syntax parsing, IDE enhancement
  • Enhanced maintainability: exposes most errors at compile time
  • In large multi-person collaborative projects, you can achieve better stability and development efficiency

TypeScript is a superset of JS

  • Contains and is compatible with all JS features, supports coexistence
  • Supports gradual introduction and upgrade

Basic Syntax

Basic Data Types

JS ==> TS

image.png

As you can see, the TS type definition syntax is: let variableName: type = value;

TypeScript Basic Types

Object Types

Interfaces - TypeScript Chinese Website

// Create an object with the following properties, typed as IBytedancer
// I indicates a custom type (a naming convention) to distinguish from classes and objects
const bytedancer: IBytedancer = {
  jobId: 9303245,
  name: 'Lin',
  sex: 'man',
  age: 28,
  hobby: 'swimming',
};
// Define a type IBytedancer
interface IBytedancer {
  /* Readonly property readonly: constrains property from being assigned outside 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 type
}
/* Error: Cannot assign to "jobId" because it is a read-only property */
bytedancer.jobId = 12345;
/* Success: with index signature, any property can be added */
bytedancer.plateform = 'data';
/* Error: missing property "name", while hobby is optional */
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 make it pass
  (type: 'date', timestamp?: string): Date;
  (type: 'string' | 'date', timestamp?: string): Date | string;
}
/* Error: Type "(type: any, timestamp: any) => string | Date" is not assignable to type "IGetDate".
 Type "string | Date" is not assignable to type "string".
 Type "Date" is not assignable 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 + square brackets" notation */
type IArr1 = number[];
/* Generic notation - these two are most commonly used */
type IArr2 = Array<string | number | Record<string, number>>;
/* Tuple notation */
type IArr3 = [number, number, string, string];
/* Interface notation */
interface IArr4 {
  [key: number]: any;
}

const arrl: 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 reverse mapping from 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 reverse mapping from enum values to enum names */
enum EnumExample {
  add = '+',
  mult = '*',
}
EnumExample['add'] === '+';
EnumExample['+'] === 'add';
enum ECorlor {
  Mon,
  Tue,
  Wed,
  Thu,
  Fri,
  Sat,
  Sun,
}
ECorlor['Mon'] === 0;
ECorlor[0] === 'Mon';
/* Generics */
type INumArr = Array<number>;

TypeScript Generics

Generics - if you’ve learned C++ before, you know what this is. It’s similar to C++: a feature that doesn’t specify the concrete type in advance, but specifies the type when used

function getRepeatArr(target) {
 return new Array(100).fill(target);
}
type IGetRepeatArr = (target: any) => any[];
/* A feature that doesn't specify the 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 constrain scope

/* Generic constraint: restricts the generic to conform to string */
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);

/* Generic parameter default type */
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, no argument 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’ll encounter a situation where you know more about a value’s details than TypeScript does. Usually this happens when you clearly know an entity has a more specific type than its current type.

Through type assertions, you can tell the compiler, “trust me, I know what I’m doing”. Type assertions are like type casting in other languages, but they don’t perform special data checking or restructuring. They have no runtime impact 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 for 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 the as keyword */
    return result as Record<string, T> ;
}

There are a few points to note in the above code:

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

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

String/Number Literals

/* Allows specifying fixed values that strings/numbers must have */
/* 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 declarations are verbose with much repetition. Advanced Types

const bookList = [
  {
    // Plain JS
    author: 'xiaoming',
    type: 'history',
    range: '2001 -2021',
  },
  {
    author: 'xiaoli',
    type: 'Story',
    theme: 'love',
  },
];
// TS verbose version
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; A union type represents a value that can be one of several types
  • Intersection type: IA & IB; Multiple types overlaid into one type that contains all the features of the required types

The above code can be simplified with TS to:

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

Type Protection and Type Guards

  • When accessing union types, for program safety, only the intersection 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 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 through type guards: define a function whose return value is a type predicate, effective within the child scope

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

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

Or use typeof and instanceof checks

// Implement function reverse that can reverse arrays or strings
function reverse(target: string | Array<any>) {
  /* typeof type protection */
  if (typeof target === 'string') {
    return target.split('').reverse().join('');
  }
  /* instanceof type protection */
  if (target instanceof Object) {
    return target.reverse();
  }
}

It won’t always be this troublesome. In fact, only when two types have no overlap do you need type guards. Like the book example above, automatic type inference can be performed.

// Implement the logBook function type
// The function accepts a book type and logs its related characteristics
function logBook(book: IBookItem) {
 // Union type + type protection = automatic type inference
 if (book.type === 'history'){
  console.log(book.range)
    } else{
        console.log book.theme);
    }
}

Let’s look at another case: implement a subset-safe merge function that merges sourceObj into targetObj, where sourceObj must be a subset of targetObj

function merge1(sourceObj, targetObj) {
  // In JS, implementing this without pollution is complex
  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 correct, you can do this
  return { ...sourceObj, ...targetObj };
}

A simple approach would be to write two types in TS for validation, but this results in verbose implementation, adding to target requires source to be updated accordingly, maintaining duplicate x, y

interface ISource0bj {
  x?: string;
  y?: string;
}
interface ITarget0bf {
  x: string;
  y: string;
}
type IMerge = (source0bj: ISource0bj, target0bj: ITarget0bj) => ITargetObj;
/* Type implementation is verbose: if obj type is complex, declaring source and target requires
repeating a lot of code twice
Error-prone: if target adds/removes keys, source needs to be updated accordingly */

Improving with generics, here are several key 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 ones — mapped types. In mapped types, the new type transforms each property in the old type in the same way. (Just use it directly, it’s built into TS)

  • The keyword keyof is equivalent to getting all keys of an object as string literals
  • The keyword in is equivalent to getting one possible value from string literals, combined with generic P, it represents each key
  • The keyword ?, by setting object optional options, can automatically infer subset types
interface IMerge {
  <T extends Record<string, any>>(sourceObj: Partial<T>, targetObj: T): T;
}
// Partial internal implementation
type IPartial<T extends Record<string, any>> = {
  [P in keyof T]?: T[P];
};
// Index types: keyword [keyof], equivalent to getting all keys of an object as string literals, e.g.
type IKeys = keyof { a: string; b: number }; // => type IKeys ="a" | "b"
// Keyword [in], equivalent to getting one possible value from string literals, combined with generic P, represents each key
// Keyword [ ? ], by setting object optional options, can automatically infer subset types

Function Return Value Types

Function return value types are not clear at definition time, and should also be expressed through generics

The code below: delayCall accepts a function as input, implements a 1s delay before running function func, returns a promise whose result is the return result of the input function

// How to implement the type declaration for function delayCall
// delayCall accepts a function as input, implements a 1s delay before running the function
// Returns a promise whose result is the return result of the input function
function delayCall(func) {
    return new Promisd(resolve => {
        setTimeout(() => {
            const result= func );
            resolve(result);
        },1000);
    });
}
  • The keyword extends, when appearing with generics, indicates type inference. Its expression can be compared to a ternary expression

    • Like T === judgedType ? TypeA : TypeB -> T extends judgedType ? TypeA : TypeB
  • The keyword infer, appearing in type inference, indicates defining a type variable that can be used to refer to a type

    A simple infer example:

    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 represents the function parameters to be inferred.

    The entire statement means: if T is assignable to (...args: infer P) => any, then the result is the parameter P from the (...args: infer P) => any type, otherwise it returns T.

    • Here it’s equivalent to referring to the function’s return value type as R
type IDelayCall = <T extends () => any>(func: T) => ReturnType<T>;
type IReturnType<T extends (...args: any) => any> = T extends (...args: any) => inferR ? R : any;

// Keyword [extends] when appearing with generics, indicates type inference, its expression can be compared to a ternary expression
// Like T === judgedType ? TypeA : TypeB
// Keyword [infer] appearing in type inference, indicates defining a type variable that can be used to refer to a type
// In this scenario, the function's return value type is used as a variable, represented by the new generic R, used in the type inference match result

Engineering Application

TypeScript Engineering Application — Web

  1. Configure webpack loader related configuration
  2. Configure tsconfig.js file (can define loose to strict)
  3. Run webpack to start/build
  4. When the loader processes TS files, it performs compilation and type checking

Related loaders:

  1. or babel-loader

TypeScript Engineering Application — Node

Using TSC for compilation

  1. Install Node and npm
  2. Configure tsconfig.js file
  3. Use npm to install tsc
  4. Use tsc to compile and get JS files

image.png

Summary and Reflections

This lesson covered TypeScript’s use cases and basic syntax, comparison with JS, advanced type applications, and later delved deeper into type protection and type guards, finally summarizing how TypeScript is applied in engineering. TypeScript, as a superset of JS, adds type checking functionality that can expose errors in code at the compile stage — something dynamic types like JS lack. In large multi-person collaborative projects, using TS often results in 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~

喜欢的话,留下你的评论吧~

© 2020 - 2026 cos @cosine
Powered by theme astro-koharu · Inspired by Shoka