深入Typescript

Typescirpt做了什么?

  • Javascript的超集
  • 通过类型注解进行类型检查从而保证代码类型安全

Compile Error

function greeter(person: string) {
    return "Hello, " + person;
}

let user = [0, 1, 2];

// error TS2345: Argument of type 'number[]' is not assignable to parameter of type 'string'.
document.body.innerHTML = greeter(user);

为什么要用Typescript?

  • 尽可能减少线上的运行错误
  • 更加可维护,代码即文档(降低首次开发的效率)
  • 帮助重构,IDE智能提示等

IntelliSense

但实际呢?

许多长时间的typescript开发者

对typescript的理解和使用水平还停留在入门阶段

开发仅限于基本的类型标注且any泛滥

// assets/utils/report.ts
export function converStringFromObj(obj: any) { }

// assets/utils/bom.ts
export function reportConfig(config: any, init: boolean = false) { }

// assets/utils/copy.ts
let copyInput: any;

// components/AppGuide/types.ts
// tslint:disable:variable-name
export const StatusMap: any = {
  all: '下载', // 不能判断
  down: '下载', // 未安装
  open: '打开', // 已安装
  install: '安装',
  ing: '下载中',
  failed: '下载失败',
};

天天吐槽基础库没有文档

又不知道写declaration file(.d.ts)

// assets/types/global.d.ts
// 及其重要的库

declare module '@tencent/impush-client' {
  const impushClient: any;
  export default impushClient;
}

declare module '@tencent/edu-report' {
  const eduReport: any;
  export default eduReport;
}

declare module '@tencent/im-user' {
  const User: any;
  export default User;
}

许多来自Definitely Types的报错却不明白为什么

// class Container
// 类型“typeof Container”的参数不能赋给类型“ComponentType<Matching<{ courseInfo: CourseInfoState; catalogInfo: CatalogInfoState; discountInfo...”的参数。
//   不能将类型“typeof Container”分配给类型“FunctionComponent<Matching<{ courseInfo: CourseInfoState; catalogInfo: CatalogInfoState; discount...”。
//     类型“typeof Container”提供的内容与签名“(props: PropsWithChildren<Matching<{ courseInfo: CourseInfoState; catalogInfo: CatalogInfoState; discountInfo: DiscountInfoState; playInfo: PlayInfoState; userInfo: UserInfoState; controlInfo: ControlInfoState; topicList: Topic[]; } & typeof import("d:/projects/m-core/src/pages/course/actions/index"), ContainerProps>>, context?: any): ReactElement<any, string | ((props: any) => ReactElement<any, string | any | (new (props: any) => Component<any, any, any>)> | null) | (new (props: any) => Component<any, any, any>)> | null”不匹配。ts(2345)
export default connect(
  (state: AppState) => ({ ...state }),
  dispatch =>
    bindActionCreators(
      actions,
      dispatch,
    ),
)(Container);

没有细究tsconfig.json配置

{
  "compilerOptions": {
    "baseUrl": ".",
    // ...
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "noImplicitAny": true,
    "lib": ["es6", "es2017", "dom", "dom.iterable", "scripthost"],
    "target": "es5",
    "module": "ESNext",
    "jsx": "react",
    "noUnusedLocals": true,
    "typeRoots": ["node_modules/@types", "src/assets/types"],
    "sourceMap": true,
    "importHelpers": true
  },
  "include": ["src/**/*"],
  "exclude": ["src/edu_modules/**/*", ".template/**/*"]
}

复杂场景不知道如何实现正确的类型校验

export default function debounce(fn: Function, delay: number, immediate?: boolean) {
  let timer: number = null;

  return function (...args: any[]) {
    const context = this;

    clearTimeout(timer);

    const callNow = !timer;
    timer = window.setTimeout(() => {
      fn.apply(context, args);
      timer = null;
    }, delay);

    if (immediate && callNow) {
      fn.apply(context, args);
    }
  };
}

结合实践

更好的使用typescript

  • Fundamentals
  • Advanced
  • Roadmap

基础部分

Basic Types

  • boolean
  • number
  • bigint
  • string
  • symbol
  • array
  • tuple
  • enum
  • any
  • void
  • null, undefined
  • never
  • object
  • unknown
// Fixed Length Tuples
interface NumStrTuple 
  extends Array<number | string> {
  0: number;
  1: string;
  // using the numeric literal type '2'
  length: 2;
}

// Weixin Uin
type CGIData = {
    uin: number | bigint
};

// object is non-primitive type
declare function create(o: object | null): void;

create({ prop: 0 }); // OK
create(null); // OK

create(42); // Error
create("string"); // Error
create(false); // Error
create(undefined); // Error

// type assertions
let someValue: any = "this is a string";

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

Interface

  • optional properties
  • readonly properties
  • Excess property check
  • function types
  • indexable types
  • class types
  • hybrid types
  • extending interfaces
// Readonly properties
interface Point {
    readonly x: number;
    readonly y: number;
}

// Optional Properties
interface SquareConfig {
    color?: string;
    width?: number;
}

interface SquareConfig {
    color?: string;
    width?: number;
    // Indexable Properties
    // [propName: string]: any;
}

function createSquare(config: SquareConfig): { color: string; area: number } {}
// Error, fails silently in Javascript
createSquare({ colour: "red", width: 100 }); 

// Function Types
interface SearchFunc {
    (source: string, subString: string): boolean;
}

class Square implements SquareConfig {
    color: string;
    width: number;
}

Class

  • public, protected, private
  • readonly
  • accessors
  • static properties
  • abstract class
class Employee {
    static sum = 1;

    readonly state: string;
    private _fullName: string;
    protected age: number;

    get fullName(): string {
        return this._fullName;
    }

    set fullName(newName: string) {
        this._fullName = newName;
    }
}


abstract class Animal {
    abstract makeSound(): void;
    move(): void {
        console.log("roaming the earth...");
    }
}

Function

  • typing function
  • optional and default parameters
  • rest parameters
  • this parameters
  • overloads
// typing function
function add(x: number, y: number): number {
    return x + y;
}

// optional parameters
function buildName(
    firstName: string = 'Bob', 
    lastName?: string, 
    ...restOfName: string[],
) {
    if (lastName)
        return firstName + " " + lastName;
    else
        return firstName;
}

function f(this: void) {
    // make sure `this` is unusable in this standalone function
}

// Overloads
function pickCard(x: {suit: string; card: number; }[]): number;
function pickCard(x: number): { suit: string; card: number; };
function pickCard(x): any {
    if (typeof x == "object") {

    }
    else if (typeof x == "number") {

    }
}

Generic

  • generic function
  • generic interface
  • generic class
  • generic constraints
  • 泛型默认参数
// generic function
function identity<T>(arg: T): T {
    return arg;
}
let output = identity<string>("myString")

interface GenericIdentityFn<T> {
    (arg: T): T;
}

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

function loggingIdentity<T extends []>(arg: T): T {
    console.log(arg.length);  // no more error
    return arg;
}
loggingIdentity(3);  // Error
loggingIdentity([3]);  

Advanced Types

  • intersection types
  • union types
  • type guards & type assertions
function extend<First, Second>(first: First, second: Second): First & Second {
    const result: Partial<First & Second> = {};
    for (const prop in first) {
        if (first.hasOwnProperty(prop)) {
            (<First>result)[prop] = first[prop];
        }
    }
    for (const prop in second) {
        if (second.hasOwnProperty(prop)) {
            (<Second>result)[prop] = second[prop];
        }
    }
    return <First & Second>result;
}

function padLeft(value: string, padding: string | number) {}

Namespace & Module

  • Shorthand ambient modules
  • Wildcard module declarations
  • Do not use namespaces in modules
// Module
declare module "path" {
    export function normalize(p: string): string;
    export function join(...paths: any[]): string;
    export var sep: string;
}

// Namespace
namespace Validation {
    export interface StringValidator {
        isAcceptable(s: string): boolean;
    }

    const lettersRegexp = /^[A-Za-z]+$/;

    export class LettersOnlyValidator implements StringValidator {
        isAcceptable(s: string) {
            return lettersRegexp.test(s);
        }
    }
}

进阶部分

Declaration Merging

// Merging Interfaces
interface Box {
    height: number;
    width: number;
}
interface Box {
    scale: number;
}
let box: Box = {height: 5, width: 6, scale: 10};

interface Cloner {
    clone(animal: Animal): Animal;
}
interface Cloner {
    clone(animal: Sheep): Sheep;
}
interface Cloner {
    clone(animal: Dog): Dog;
    clone(animal: Cat): Cat;
}
// as
interface Cloner {
    clone(animal: Dog): Dog;
    clone(animal: Cat): Cat;
    clone(animal: Sheep): Sheep;
    clone(animal: Animal): Animal;
}
// Merging Namespaces
namespace Animals {
    export class Zebra { }
}

namespace Animals {
    export interface Legged { numberOfLegs: number; }
    export class Dog { }
}

namespace Animal {
    let haveMuscles = true;

    export function animalsHaveMuscles() {
        return haveMuscles;
    }
}

namespace Animal {
    export function doAnimalsHaveMuscles() {
        return haveMuscles;  // Error, because haveMuscles is not accessible here
    }
}

Declaration Merging

// Merging Namespaces with Classes
class Album {
    label: Album.AlbumLabel;
}
namespace Album {
    export class AlbumLabel { }
}

// Merging Namespaces with Fucntions
function buildLabel(name: string): string {
    return buildLabel.prefix + name + buildLabel.suffix;
}

namespace buildLabel {
    export let suffix = "";
    export let prefix = "Hello, ";
}

console.log(buildLabel("Sam Smith"));
// Module Augmentation

declare module "./observable" {
    interface Observable<T> {
        map<U>(f: (x: T) => U): Observable<U>;
    }
}

// Global Augmentation
declare global {
    interface Array<T> {
        find((x: T) => boolean): T
    }
}

Type Compatibility

// based on structural subtyping(结构子类型)
interface Named {
    name: string;
}

class Person {
    name: string;
}

let p: Named;
// OK, because of structural typing
p = new Person();

// comparing functions
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;

y = x; // OK
x = y; // Error

// return type same as comparing primitive and object types
let x = () => ({name: "Alice"});
let y = () => ({name: "Alice", location: "Seattle"});

x = y; // OK
y = x; // Error, because x() lacks a location property
// function parameter bivariance

// Unsound: 
(e: Event) => void
(e: MouseEvent) => void

// optional parameters and rest parameters

// Usound: 
(args: any[], callback: (...args: any[]) => void)
([1, 2], (x: number, y: number) => void)

Type Compatibility

// different enum types are incompatible
enum Status { Ready, Waiting };
enum Color { Red, Blue, Green };

let status = Status.Ready;
status = Color.Green;  // Error


// only compare instance properties in compatibility
// protected & private properties must originated from the same class
class Animal {
    feet: number;
    constructor(name: string, numFeet: number) { }
}

class Size {
    feet: number;
    constructor(numFeet: number) { }
}

let a: Animal;
let s: Size;

a = s;  // OK
s = a;  // OK
// generic types that do not have their type arguments specified, replace with any

interface NotEmpty<T> {
    data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;

x = y;  // Error, because x and y are not compatible

let identity = function<T>(x: T): T {
    // ...
}

let reverse = function<U>(y: U): U {
    // ...
}

identity = reverse;  // OK, because (x: any) => any matches (y: any) => any

Type Inference

  • where to take place
  • best common type
  • contextual typing
// take place in initializing variables, setting parameter default values, determining function return types ...
let x = 3; // number
function square(x = 3) { return x * x } // (x: number) => number

// best common type
let arr = [0, 1, null] // number[]
let arr = [new Dog(), new Elephant(), new Cat()]; // Animal[] ? or (Dog|Elephant|Cat)[]

// contextual typing
window.onmousedown = function(e) {} // (e: MouseEvent) => void

function wrap<T>(v: T) {
    return { value: v };
}

let o = wrap('secret'); // { value: string }

--strict

  • --noImplicitAny
  • --noImplicitThis
  • --alwaysStrict
  • --strictBindCallApply
  • --strictNullChecks
  • --strictFunctionTypes
  • --strictPropertyInitialization

--strictNullChecks

// Compiled with --strictNullChecks
let x: number;
let y: number | undefined;
let z: number | null | undefined;
x = 1;  // Ok
y = 1;  // Ok
z = 1;  // Ok
x = undefined;  // Error
y = undefined;  // Ok
z = undefined;  // Ok
x = null;  // Error
y = null;  // Error
z = null;  // Ok
x = y;  // Error
x = z;  // Error
y = x;  // Ok
y = z;  // Error
z = x;  // Ok
z = y;  // Ok
// Compiled with --strictNullChecks

// 类型“string | null”的参数不能赋给类型“string”的参数。
// 不能将类型“null”分配给类型“string”。
export function getContentWidth(el: HTMLElement): number {
  const w = el.offsetWidth;
  const s = window.getComputedStyle(el);

  return w - parseFloat(s.paddingLeft) - parseFloat(s.paddingRight);
}

// CGI Data
export interface Detail {
  sys_time?: number; // 系统时间
  cid?: number;
  name?: string;
  summary?: string;
  cover_url?: string;
  uin?: number;
  // ...
}

--strictBindCallApply

function foo(a: number, b: string): string {
    return a + b;
}

let a = foo.apply(undefined, [10]);              // error: too few argumnts
let b = foo.apply(undefined, [10, 20]);          // error: 2nd argument is a number
let c = foo.apply(undefined, [10, "hello", 30]); // error: too many arguments
let d = foo.apply(undefined, [10, "hello"]);     // okay! returns a string

--strictFunctionTypes

declare let f1: (x: Animal) => void;
declare let f2: (x: Dog) => void;
declare let f3: (x: Cat) => void;


f1 = f2;  // Error with --strictFunctionTypes
f2 = f1;  // Ok
f2 = f3;  // Error

interface Comparer<T> {
    compare: (a: T, b: T) => number;
}

declare let animalComparer: Comparer<Animal>;
declare let dogComparer: Comparer<Dog>;

animalComparer = dogComparer;  // Error
dogComparer = animalComparer;  // Ok

--strictPropertyInitialization

// ensure that each instance property of a class gets initialized in the constructor body

class C {
    foo: number;
    bar = "hello";
    baz: boolean;
//  ~~~
//  Error! Property 'baz' has no initializer and is not definitely assigned in the
//         constructor.

    constructor() {
        this.foo = 42;
    }
}

Conditional Types

// Based on never, extends
Extract<T, U> = T extends U ? T : never;
Exclude<T, U> = T extends U ? never : T;
NonNullable = T extends null ? never : T;

type TypeName<T> =
    T extends string ? "string" :
    T extends number ? "number" :
    T extends boolean ? "boolean" :
    T extends undefined ? "undefined" :
    T extends Function ? "function" :
    "object";

type T0 = TypeName<string>;  // "string"
type T1 = TypeName<"a">;  // "string"
type T2 = TypeName<true>;  // "boolean"
type T3 = TypeName<() => void>;  // "function"
type T4 = TypeName<string[]>;  // "object"
// infer
ReturnType<T extends (...args: any) => any> = 
    T extends (...args: any) => infer R 
        ? R : any;
InstanceType<T extends new (...args: any) => any> = 
    T extends new (...args: any) => infer R 
        ? R : any;

// Infers prop type from component C
type GetProps<C> = C extends ComponentType<infer P> 
    ? P : never;

Operators & Modifers

// typeof 获取隐式推断的类型
type BankSelectProps = Partial<
  typeof BankSelect.defaultProps
>;

// keyof
type Readonly<T> = { readonly [P in keyof T]: T[P] };
type Partial<T> = { [P in keyof T]?: T[P] };
type Pick<T, K entends keyof T> = { [P in K]: T[P] };
type Record<K extends string | number | symbol, U> = { [P in K]: U };

// keyof (A & B) = keyof A | keyof B


// +, -
type Writable<T> = { -readonly [P in keyof T]: T[P] };
type Require<T> = { [P in keyof T]-?: T[P] };

Operators & Modifers

// typeof 获取隐式推断的类型
type BankSelectProps = Partial<
  typeof BankSelect.defaultProps
>;

// keyof
type Readonly<T> = { readonly [P in keyof T]: T[P] };
type Partial<T> = { [P in keyof T]?: T[P] };
type Pick<T, K entends keyof T> = { [P in K]: T[P] };
type Record<K extends string | number | symbol, U> = { [P in K]: U };

// keyof (A & B) = keyof A | keyof B


// +, -
type Writable<T> = { -readonly [P in keyof T]: T[P] };
type Require<T> = { [P in keyof T]-?: T[P] };

// as const
const PLATFORMS = ['qq', 'weixin', 'h5'] as const

Writing .d.ts

  1. First, identifying the structure of a library
  2.  

未来展望

快速接入实践

Typescript Deep In

By Moxhe

Typescript Deep In

  • 353