天天看點

TS内置類型與拓展

TS内置類型與拓展

​TypeScript​

​​具有類型系統,且是​

​JavaScript​

​​的超集,其可以編譯成普通的​

​JavaScript​

​​代碼,也就是說,其是帶有類型檢查的​

​JavaScript​

​。

内置類型

​TypeScript​

​提供了幾種實用程式類型來促進常見的類型轉換,這些類型在全局範圍内可用。

Partial

​Partial<Type>​

​​構造一個類型使​

​Type​

​的所有屬性都設定為可選。

/**
 * Make all properties in T optional
 */

type Partial = {
    [P in keyof T]?: T[P];
};
      
interface Example {
    a: string;
    b: number;
}

type PartialExample = Partial;

/**
 * PartialExample
 * interface {
 *     a?: string | undefined;
 *     b?: number | undefined;
 * }
 */
      

Required

​Required<Type>​

​​構造一個類型使​

​Type​

​​的所有屬性都設定為​

​required​

​​,與​

​Partial<Type>​

​功能相反。

/**
 * Make all properties in T required
 */

type Required = {
    [P in keyof T]-?: T[P];
};
      
interface Example {
    a?: string;
    b?: number;
}

type RequiredExample = Required;

/**
 * RequiredExample
 * interface {
 *     a: string;
 *     b: number;
 * }
 */
      

Readonly

​Required<Type>​

​​構造一個類型使​

​Type​

​​的所有屬性都設定為​

​readonly​

​​,這意味着構造類型的屬性都是隻讀的,不能被修改,這對使用​

​Object.freeze()​

​方法的對象非常有用。

/**
 * Make all properties in T readonly
 */

type Readonly = {
    readonly [P in keyof T]: T[P];
};
      
interface Example {
    a: string;
    b: number;
}

type ReadonlyExample = Readonly;

/**
 * ReadonlyExample
 * interface {
 *     readonly a: string;
 *     readonly b: number;
 * }
 */
      

Record

​Record<Keys, Type>​

​​構造一個對象類型,其屬性鍵為​

​Keys​

​​,其屬性值為​

​Type​

​​,通常可以使用​

​Record​

​來表示一個對象。

/**
 * Construct a type with a set of properties K of type T
 */

type Record = {
    [P in K]: T;
};
      
type RecordType = Record;

const recordExample: RecordType ={
  a: 1,
  b: "1"
}
      

Pick

​Pick<Type, Keys>​

​​通過從​

​Type​

​​中選擇一組屬性​

​Keys​

​來構造一個類型。

/**
 * From T, pick a set of properties whose keys are in the union K
 */

type Pick = {
    [P in K]: T[P];
};
      
interface Example {
    a: string;
    b: number;
    c: symbol;
}

type PickExample = Pick;

/**
 * PickExample
 * interface {
 *     a: string;
 *     b: number;
 * }
 */
      

Omit

​Omit<Type, Keys>​

​​通過從​

​Type​

​​中選擇所有屬性然後删除​

​Keys​

​​來構造一個類型,與​

​Pick<Type, Keys>​

​功能相反。

/**
 * Construct a type with the properties of T except for those in type K.
 */

type Omit = Pick>;
      
interface Example {
    a: string;
    b: number;
    c: symbol;
}

type OmitExample = Omit;

/**
 * OmitExample
 * interface {
 *     c: symbol;
 * }
 */
      

Exclude

​Exclude<UnionType, ExcludedMembers>​

​​通過從​

​UnionType​

​​中排除可配置設定給​

​ExcludedMembers​

​的所有聯合成員來構造類型。

/**
 * Exclude from T those types that are assignable to U
 */

type Exclude = T extends U ? never : T;
      
type ExcludeExample = Exclude<"a"|"b"|"c"|"z", "a"|"b"|"d">;

/**
 * ExcludeExample
 * "c" | "z"
 */
      

Extract

​Extract<Type, Union>​

​​通過從​

​Type​

​​中提取所有可配置設定給​

​Union​

​​的聯合成員來構造一個類型,與​

​Exclude<UnionType, ExcludedMembers>​

​功能相反。

/**
 * Extract from T those types that are assignable to U
 */

type Extract = T extends U ? T : never;
      
type ExtractExample = Extract<"a"|"b"|"c"|"z", "a"|"b"|"d">;

/**
 * ExtractExample
 * "a" | "b"
 */
      

NonNullable

​NonNullable<Type>​

​​通過從​

​Type​

​​中排除​

​null​

​​和​

​undefined​

​來構造一個類型。

/**
 * Exclude null and undefined from T
 */

type NonNullable = T extends null | undefined ? never : T;
      
type NonNullableExample = NonNullable;

/**
 * NonNullableExample
 * string | number
 */
      

Parameters

​Parameters<Type>​

​​從函數類型​

​Type​

​的參數中使用的類型構造元組類型。

/**
 * Obtain the parameters of a function type in a tuple
 */

type Parameters any> = T extends (...args: infer P) => any ? P : never;
      
type FnType = (a1: number, a2: string) => void;

type ParametersExample = Parameters;

/**
 * ParametersExample
 * [a1: number, a2: string]
 */
      

ConstructorParameters

​ConstructorParameters<Type>​

​從構造函數類型的類型構造元組或數組類型,其産生一個包含所有參數類型的元組類型。

/**
 * Obtain the parameters of a constructor function type in a tuple
 */

type ConstructorParameters any> = T extends abstract new (...args: infer P) => any ? P : never;
      
interface Example{
  fn(a: string): string;
}

interface ExampleConstructor{
    new(a: string, b: number): Example;
}

declare const Example: ExampleConstructor;

type ConstructorParametersExample = ConstructorParameters;

/**
 * ConstructorParametersExample
 * [a: string, b: number]
 */
      

ReturnType

​ReturnType<Type>​

​​構造一個由函數​

​Type​

​的傳回類型組成的類型。

/**
 * Obtain the return type of a function type
 */

type ReturnType any> = T extends (...args: any) => infer R ? R : any;
      
type FnType = (a1: number, a2: string) => string | number;

type ReturnTypeExample = ReturnType;

/**
 * ReturnTypeExample
 * string | number
 */
      

InstanceType

​InstanceType<Type>​

​​構造一個由​

​Type​

​中構造函數的執行個體類型組成的類型。

/**
 * Obtain the return type of a constructor function type
 */

type InstanceType any> = T extends abstract new (...args: any) => infer R ? R : any;
      
interface Example{
  fn(a: string): string;
}

interface ExampleConstructor{
    new(a: string, b: number): Example;
}

declare const Example: ExampleConstructor;

type InstanceTypeExample = InstanceType;

// const a: InstanceTypeExample = new Example("a", 1); // new ExampleConstructor => Example

/**
 * InstanceTypeExample
 * Example
 */
      

ThisParameterType

​ThisParameterType<Type>​

​​提取函數類型的​

​this​

​​參數的類型,如果函數類型沒有​

​this​

​​參數,則為​

​unknown​

​。

/**
 * Extracts the type of the 'this' parameter of a function type, or 'unknown' if the function type has no 'this' parameter.
 */

type ThisParameterType = T extends (this: infer U, ...args: any[]) => any ? U : unknown;
      
function toHex(this: Number) {
  return this.toString(16);
}

type ThisParameterTypeExample = ThisParameterType;

console.log(toHex.apply(27)); // 1b

/**
 * ThisParameterTypeExample
 * Number
 */
      

OmitThisParameter

​OmitThisParameter<Type>​

​​從​

​Type​

​​中移除​

​this​

​​參數,如果​

​Type​

​​沒有顯式聲明此參數,則結果隻是​

​Type​

​​,否則,從​

​Type​

​建立一個不帶此參數的新函數類型。泛型被删除,隻有最後一個重載簽名被傳播到新的函數類型中。

/**
 * Removes the 'this' parameter from a function type.
 */

type OmitThisParameter = unknown extends ThisParameterType ? T : T extends (...args: infer A) => infer R ? (...args: A) => R : T;
      
function toHex(this: Number) {
  return this.toString(16);
}

type OmitThisParameterExample = OmitThisParameter;

const toHex27: OmitThisParameterExample = toHex.bind(27);
console.log(toHex27()); // 1b

/**
 * OmitThisParameterExample
 * () => string
 */
      

ThisType

​ThisType<Type>​

​​可以在對象字面量中鍵入​

​this​

​​,并提供通過上下文類型控制​

​this​

​​類型的便捷方式,其隻有在​

​--noImplicitThis​

​的選項下才有效。

/**
 * Marker for contextual 'this' type
 */
interface ThisType { }
      
// const foo1 = {
//     bar() {
//          console.log(this.a); // error
//     }
// }

const foo2: { bar: () => void } & ThisType<{ a: number }> = {
    bar() {
         console.log(this.a); // ok
    }
}
      

Uppercase

​Uppercase<StringType>​

​​将​

​StringType​

​​轉為大寫,​

​TS​

​​以内置關鍵字​

​intrinsic​

​來通過編譯期來實作。

/**
 * Convert string literal type to uppercase
 */

type Uppercase = intrinsic;
      
type UppercaseExample = Uppercase<"abc">;

/**
 * UppercaseExample
 * ABC
 */
      

Lowercase

​Lowercase<StringType>​

​​将​

​StringType​

​轉為小寫。

/**
 * Convert string literal type to lowercase
 */

type Lowercase = intrinsic;
      
type LowercaseExample = Lowercase<"ABC">;

/**
 * LowercaseExample
 * abc
 */
      

Capitalize

​Capitalize<StringType>​

​​将​

​StringType​

​首字母轉為大寫。

/**
 * Convert first character of string literal type to uppercase
 */

type Capitalize = intrinsic;
      
type CapitalizeExample = Capitalize<"abc">;

/**
 * CapitalizeExample
 * Abc
 */
      

Uncapitalize

​Uncapitalize<StringType>​

​​将​

​StringType​

​首字母轉為小寫。

/**
 * Convert first character of string literal type to lowercase
 */

type Uncapitalize = intrinsic;
      
type UncapitalizeExample = Uncapitalize<"ABC">;

/**
 * CapitalizeExample
 * aBC
 */
      

拓展

​TypeScript​

​中常用的一些文法以及概念。

泛型

泛型​

​Generics​

​​是指在定義函數、接口或類的時候,不預先指定具體的類型,而在使用的時候再指定類型的一種特性。舉一個簡單的例子,如果需要實作一個生成數組的函數,這個數組會填充預設值,這個數組填充的類型不需要事先指定,而可以在使用的時候指定。當然在這裡使用​

​new Array​

​​組合​

​fill​

​函數是一個效果。

function createArray(value: T, length: number): T[] {
  const result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

console.log(createArray(1, 3)); // 不顯式地指定`number`也可以自動推斷
      

我們也可以限制​

​T​

​​的類型隻能為​

​number​

​​與​

​string​

​。

const createArray = (value: T, length: number): T[] => {
  const result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

console.log(createArray(1, 3));
// console.log(createArray(true, 3)); // Argument of type 'boolean' is not assignable to parameter of type 'string | number'.(2345)
      

多個類型也可以互相限制,例如上邊的​

​Pick​

​​,在這裡的​

​K​

​​必須是​

​T​

​​中​

​key​

​的子集。

type Pick = {
    [P in K]: T[P];
};
      

在傳遞泛型的時候可以為​

​T​

​​指定預設值,使用範型編寫​

​class​

​即泛型類也是完全支援的。

class Example {
    public value: T;
    public add: (x: T, y: T) => T;
    constructor(value: T, add: (x: T, y: T) => T){
      this.value = value;
      this.add = add;
    }
}

let example = new Example(1, (x, y) => x + y);
console.log(example.value); // 1
console.log(example.add(1, 2)); // 3
      

斷言

類型斷言​

​Type Assertion​

​​可以用來手動指定一個值的類型,由于​

​<Type>value​

​​的文法容易與​

​TSX​

​​沖突,是以通常都是使用​

​value as Type​

​​的文法。通常當​

​TypeScript​

​不确定一個聯合類型的變量到底是哪個類型的時候,我們隻能通路此聯合類型的所有類型中共有的屬性或方法。

interface Cat {
    name: string;
    run(): void;
}
interface Fish {
    name: string;
    swim(): void;
}

function getName(animal: Cat | Fish) {
    return animal.name;
}
      

而有時候,我們确實需要在還不确定類型的時候就通路其中一個類型特有的屬性或方法。

interface Cat {
    name: string;
    run(): void;
}
interface Fish {
    name: string;
    swim(): void;
}

function isFish(animal: Cat | Fish) {
    if (typeof animal.swim === "function") { // Property 'swim' does not exist on type 'Cat | Fish'. Property 'swim' does not exist on type 'Cat'.(2339)
        return true;
    }
    return false;
}
      

上面的例子中,擷取​

​animal.swim​

​​的時候會報錯,此時可以使用類型斷言,将​

​animal​

​​斷言成​

​Fish​

​​。當然這裡隻是舉一個例子說明斷言的使用,因為濫用斷言是不提倡的,類型斷言隻能夠欺騙​

​TypeScript​

​編譯器,而無法避免運作時的錯誤,濫用類型斷言可能會導緻運作時錯誤。

interface Cat {
    name: string;
    run(): void;
}
interface Fish {
    name: string;
    swim(): void;
}

function isFish(animal: Cat | Fish) {
    if (typeof (animal as Fish).swim === "function") {
        return true;
    }
    return false;
}
      

單個斷言即​

​value as Type​

​​是有一定條件的,當​

​S​

​​類型是​

​T​

​​類型的子集,或者​

​T​

​​類型是​

​S​

​​類型的子集時,​

​S​

​​能被成功斷言成​

​T​

​​。這是為了在進行類型斷言時提供額外的安全性,完全毫無根據的斷言是危險的,如果你想這麼做,你可以使用​

​any​

​​。

如果認為某個值​​

​value​

​​必定是某種類型​

​Type​

​​,而單個斷言無法滿足要求,可以使用雙重斷言,即​

​value as unknown as Type​

​​,使用​

​value as any as Type​

​​也是同樣的效果,但是若使用雙重斷言,則可以打破要使得​

​A​

​​能夠被斷言為​

​B​

​​,隻需要​

​A​

​​相容​

​B​

​​或​

​B​

​​相容​

​A​

​​即可的限制,将任何一個類型斷言為任何另一個類型。通常來說除非迫不得已,不要使用雙重斷言。

此外類型斷言之是以不被稱為類型轉換,是因為類型轉換通常意味着某種運作時的支援,而類型斷言隻會影響​​

​TypeScript​

​​編譯時的類型,類型斷言語句在編譯結果中會被删除,也就是說類型斷言純粹是一個編譯時文法,同時其也是一種為編譯器提供關于如何分析代碼的方法。

與類型斷言相關的還有一個​​

​!​

​​的表達式,其在​

​TypeScript 2.7​

​​被加入,其稱為​

​definite assignment assertion​

​​顯式指派斷言,顯式指派斷言允許你在執行個體屬性和變量聲明之後加一個感歎号​

​!​

​​,來告訴​

​TypeScript​

​​這個變量确實已被指派,即使​

​TypeScript​

​不能分析出這個結果。

let x: number;
let y!: number;
console.log(x + x); // Variable 'x' is used before being assigned.(2454)
console.log(y + y); // ok
      

既然說到了​

​!​

​​,那麼也可以說一下​

​?​

​​,在​

​interface​

​​中​

​?​

​​和​

​undefined​

​​并不是等效的,在下面的例子中,在​

​b​

​​未将​

​?​

​​聲明的情況下,其在​

​interface​

​​下是​

​required​

​​,​

​TypeScript​

​​認為其是必須指定的​

​key​

​​即使其值隻能為​

​undefined​

​。

interface Example{
  a?: number;
  b: undefined;
}

const example1: Example = {}; // Property 'b' is missing in type '{}' but required in type 'Example'.(2741)
const example2: Example = { b: undefined }; // ok
      

infer

​infer​

​​示在​

​extends​

​​條件語句中待推斷的類型變量,也可以認為其是一個占位符,用以在使用時推斷。例如上邊的​

​ReturnType​

​​就是通過​

​infer​

​​進行推斷的,首先是範型限制了一個函數類型,然後在後邊進行​

​infer​

​占位後進行推斷。

type ReturnType any> = T extends (...args: any) => infer R ? R : any;
      

有一些應用,​

​tuple​

​​轉​

​union​

​​,如​

​[string, number, symbol] -> string | number | symbol​

​。

type ElementOf = T extends Array ? E : never;

type TTuple = [string, number, symbol];

type ToUnion = ElementOf; // string | number | symbol
      

還有一個比較離譜的實作。

type TTuple = [string, number, symbol];
type Res = TTuple[number]; // string | number | symbol

// https://stackoverflow.com/questions/44480644/string-union-to-string-array/45486495#45486495
      

還比如擷取函數參數的第一個參數類型。

type fn = (a: number, b: string, ddd: boolean) => void;

type FirstParameter = T extends (args1: infer R, ...rest: any[]) => any ? R : never;

type firstArg = FirstParameter;  // number
      

函數重載

​TypeScript​

​允許聲明函數重載,即允許一個函數接受不同數量或類型的參數時,作出不同的處理。當然,最終聲明即從函數内部看到的真正聲明與所有重載相容是很重要的。這是因為這是函數體需要考慮的函數調用的真實性質。

function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string | void {
    if (typeof x === "number") {
        return Number(x.toString().split("").reverse().join(""));
    } else if (typeof x === "string") {
        return x.split("").reverse().join("");
    }
}
      

還有一個比較實用的簡單例子,在​

​ios​

​​上的​

​Date​

​​對象是不接受形如​

​2022-04-05 20:00:00​

​​的字元串去解析的,當在​

​safari​

​的控制台執行時,會出現一些異常行為。這個字元串的解析在谷歌浏覽器或者安卓上就沒有問題,是以需要做一下相容處理。

// safari
const date = new Date("2022-04-05 20:00:00");
console.log(date.getDay()); // NaN

// chrome
const date = new Date("2022-04-05 20:00:00");
console.log(date.getDay()); // 2      

是以需要對時間日期對象做一下簡單的相容,但是做相容時又需要保證​

​TS​

​的聲明,這時就可以使用函數重載等方式處理。

function safeDate(): Date;
function safeDate(date: Date): Date;
function safeDate(timestamp: number): Date;
function safeDate(dateTimeStr: string): Date;
function safeDate(
    year: number,
    month: number,
    date?: number,
    hours?: number,
    minutes?: number,
    seconds?: number,
    ms?: number
): Date;
function safeDate(
    p1?: Date | number | string,
    p2?: number,
    p3?: number,
    p4?: number,
    p5?: number,
    p6?: number,
    p7?: number
): Date | never {
    if (p1 === void 0) {
        // 無參建構
        return new Date();
    } else if (p1 instanceof Date || (typeof p1 === "number" && p2 === void 0)) {
        // 第一個參數為`Date`或者`Number`且無第二個參數
        return new Date(p1);
    } else if (typeof p1 === "number" && typeof p2 === "number") {
        // 第一和第二個參數都為`Number`
        return new Date(p1, p2, p3 || 1, p4 || 0, p5 || 0, p6 || 0, p7 || 0);
    } else if (typeof p1 === "string") {
        // 第一個參數為`String`
        return new Date(p1.replace(/-/g, "/"));
    }
    throw new Error("No suitable parameters");
}

console.log(safeDate("2022-04-05 20:00:00").getDay()); // 2
      
type DateParams =
    | []
    | [string]
    | [number, number?, number?, number?, number?, number?, number?]
    | [Date];
const safeDate = (...args: T): Date => {
    const copyParams = args.slice(0);
    if (typeof copyParams[0] === "string") copyParams[0] = copyParams[0].replace(/-/g, "/");
    return new Date(...(args as ConstructorParameters));
};

console.log(safeDate("2022-04-05 20:00:00").getDay()); // 2
      

聲明檔案

對于全局變量的聲明檔案主要有以下幾種文法:

  • ​declare var​

    ​聲明全局變量。
  • ​declare function​

    ​聲明全局方法。
  • ​declare class​

    ​聲明全局類。
  • ​declare enum​

    ​聲明全局枚舉類型。
  • ​declare namespace​

    ​聲明含有子屬性的全局對象。
  • ​interface​

    ​​和​

    ​type​

    ​聲明全局類型。
  • ​declare module​

    ​拓展聲明。

我們可以通過​

​declare​

​​關鍵字來告訴​

​TypeScript​

​​,某些變量或者對象已經聲明,我們可以選擇把這些聲明放入​

​.ts​

​​或者​

​.d.ts​

​​裡。​

​declare namespace​

​表示全局變量是一個對象,包含很多子屬性。

// global.d.ts
declare namespace App {
    interface Utils {
        onload: (fn: (...args: T) => void, ...args: T) => void;
    }
}

declare interface Window{
  utils: App.Utils
}

// main.ts
window.utils = {
  onload: () => void 0
}
      

對于子產品的聲明檔案主要有以下幾種文法:

  • ​export​

    ​導出變量。
  • ​export namespace​

    ​導出含有子屬性的對象。
  • ​export default ES6​

    ​預設導出。
  • ​export = ​

    ​​導出​

    ​CommonJs​

    ​子產品。

子產品的聲明檔案與全局變量的聲明檔案有很大差別,在子產品的聲明檔案中,使用​

​declare​

​​不再會聲明一個全局變量,而隻會在目前檔案中聲明一個局部變量,隻有在聲明檔案中使用​

​export​

​​導出,然後在使用方​

​import​

​​導入後,才會應用到這些類型聲明,如果想使用子產品的聲明檔案而并沒有實際的​

​export​

​​時,通常會顯示标記一個空導出​

​export {}​

​​。對于子產品的聲明檔案我們更推薦使用 ​

​ES6​

​​标準的​

​export default​

​​和​

​export​

​。

// xxx.ts
export const name: string = "1";

// xxxxxx.ts
import { name } from "xxx.ts";
console.log(name); // 1 // typeof name === "string"
      

如果是需要擴充原有子產品的話,需要在類型聲明檔案中先引用原有子產品,再使用​

​declare module​

​擴充原有子產品。

// xxx.d.ts
import * as moment from "moment";

declare module "moment" {
    export function foo(): moment.CalendarKey;
}

// xxx.ts
import * as moment from "moment";
moment.foo();
      
import Vue from "vue";

declare module "vue/types/vue" {
    interface Vue {
        copy: (str: string) => void;
    }
}
      

還有一些諸如​

​.vue​

​​檔案、​

​.css​

​​、​

​.scss​

​​檔案等,需要在全局中進行聲明其​

​import​

​時對象的類型。

declare module "*.vue" {
    import Vue from "vue/types/vue";
    export default Vue;
}
      
declare module "*.module.css" {
  const classes: { readonly [key: string]: string };
  export default classes;
}
      
declare module "*.module.scss" {
  const classes: { readonly [key: string]: string };
  export default classes;
}
      

在聲明檔案中,還可以通過三斜線指令即​

​///​

​​來導入另一個聲明檔案,在全局變量的聲明檔案中,是不允許出現​

​import​

​​、​

​export​

​​關鍵字的,一旦出現了,那麼他就會被視為一個子產品或​

​UMD​

​庫,就不再是全局變量的聲明檔案了,故當我們在書寫一個全局變量的聲明檔案時,如果需要引用另一個庫的類型,那麼就必須用三斜線指令了。

// types/jquery-plugin/index.d.ts
/// 
declare function foo(options: JQuery.AjaxSettings): string;

// src/index.ts
foo({});
      

協變與逆變

子類型在程式設計理論上是一個複雜的話題,而他的複雜之處來自于一對經常會被混淆的現象。簡單來說,協變即類型收斂,逆變即類型發散。在這裡由下面的例子引起關于這個問題的讨論,在這裡我們定義了一個父類型以及一個子類型,而且我們驗證了這個子類型在​

​TS​

​​中是​

​OK​

​的。

type SuperType = (value: number|string) => number|string; // 父類型
type SubType = (value: number|string|boolean) => number; // 子類型 參數逆變 傳回值協變

const subFn: SubType = (value: number|string|boolean) => 1;
const superFn: SuperType = subFn; // ok
      

首先我們可以探讨一下子類型,明顯​

​number​

​​是​

​number|string​

​​的子類型,那麼下面這個例子是完全​

​OK​

​​的,這同樣也是一個協變的過程,由此看來在上邊例子的​

​SubType​

​​确實是​

​SuperType​

​的子類型。

type SuperType = number|string; // 父類型
type SubType = number; // 子類型

const subValue: SubType = 1;
const superValue: SuperType = subValue; // ok
      

那麼此時就回到最上邊的例子,這個函數參數​

​value​

​​的類型就很奇怪,明明是子類型,反而類型的種類更多了,這個其實就是所謂的逆變,其實這就是為了保證類型的收斂是安全的。此時我們的​

​subFn​

​​實際代表的函數是​

​SuperType​

​​類型的,當我們實際調用的時候,傳遞的參數由于是​

​SuperType​

​​類型的即​

​number|string​

​​,是以必定是​

​SubType​

​​類型的子類即​

​number|string|boolean​

​​,這樣也就保證了函數參數的收斂安全,之後當函數執行完成進行傳回值時,由于函數實際定義時的傳回類型是​

​number​

​​,那麼在傳回出去的時候也必定是​

​number|string​

​的子類,這樣也就保證了函數傳回值的收斂安全。我們可以通過這個圖示去了解這個函數子類型的問題,類似于以下的調用過程,由此做到類型收斂的安全。

父類型參數 -> 子類型參數 -> 執行 -> 子類型傳回值 -> 父類型傳回值
number|string -> number|string|boolean -> ... -> number -> number|string
      

我們可以進行一個總結: 除了函數參數類型是逆變,都是協變。将一個函數賦給另一個函數變量時,要保證參數類型發散,即比目标類型範圍小。目标函數執行時是執行的原函數,傳入的參數類型會收斂為原函數參數類型。協變表示類型收斂,即類型範圍縮小或不變,逆變反之。本質是為了保證執行時類型收斂是安全的。

另外可以看一下 ​​這篇文章​​​ 對于協變與逆變的描述。

開始文章之前我們先約定如下的标記,​​

​A ≼ B​

​​意味着​

​A​

​​是​

​B​

​​的子類型;​

​A → B​

​​指的是以​

​A​

​​為參數類型,以​

​B​

​​為傳回值類型的函數類型;​

​x : A​

​​意味着​

​x​

​​的類型為​

​A​

​​。

假設我有如下三種類型:​​

​Greyhound ≼ Dog ≼ Animal​

​​。

​​

​Greyhound​

​​灰狗是​

​Dog​

​​狗的子類型,而​

​Dog​

​​則是​

​Animal​

​​動物的子類型,由于子類型通常是可傳遞的,是以我們也稱​

​Greyhound​

​​是​

​Animal​

​​的子類型,問題: 以下哪種類型是​

​Dog → Dog​

​的子類型呢。

  1. ​Greyhound → Greyhound​

    ​。
  2. ​Greyhound → Animal​

    ​。
  3. ​Animal → Animal​

    ​。
  4. ​Animal → Greyhound​

    ​。

讓我們來思考一下如何解答這個問題,首先我們假設​

​f​

​​是一個以​

​Dog → Dog​

​​為參數的函數,它的傳回值并不重要,為了具體描述問題,我們假設函數結構體是這樣的​

​f :(Dog → Dog ) → String​

​​,現在我想給函數​

​f​

​​傳入某個函數​

​g​

​​來調用,我們來瞧瞧當​

​g​

​為以上四種類型時,會發生什麼情況。

tsconfig.json

{
  "compilerOptions": {
    /* Basic Options */
    "target": "es5" /* target用于指定編譯之後的版本目标: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
    "module": "commonjs" /* 用來指定要使用的子產品标準: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
    "lib": ["es6", "dom"] /* lib用于指定要包含在編譯中的庫檔案 */,
    "allowJs": true,                       /* allowJs設定的值為true或false,用來指定是否允許編譯js檔案,預設是false,即不編譯js檔案 */
    "checkJs": true,                       /* checkJs的值為true或false,用來指定是否檢查和報告js檔案中的錯誤,預設是false */
    "jsx": "preserve",                     /* 指定jsx代碼用于的開發環境: 'preserve', 'react-native', or 'react'. */
    "declaration": true,                   /* declaration的值為true或false,用來指定是否在編譯的時候生成相應的".d.ts"聲明檔案。如果設為true,編譯每個ts檔案之後會生成一個js檔案和一個聲明檔案。但是declaration和allowJs不能同時設為true */
    "declarationMap": true,                /* 值為true或false,指定是否為聲明檔案.d.ts生成map檔案 */
    "sourceMap": true,                     /* sourceMap的值為true或false,用來指定編譯時是否生成.map檔案 */
    "outFile": "./",                       /* outFile用于指定将輸出檔案合并為一個檔案,它的值為一個檔案路徑名。比如設定為"./dist/main.js",則輸出的檔案為一個main.js檔案。但是要注意,隻有設定module的值為amd和system子產品時才支援這個配置 */
    "outDir": "./",                        /* outDir用來指定輸出檔案夾,值為一個檔案夾路徑字元串,輸出的檔案都将放置在這個檔案夾 */
    "rootDir": "./",                       /* 用來指定編譯檔案的根目錄,編譯器會在根目錄查找入口檔案,如果編譯器發現以rootDir的值作為根目錄查找入口檔案并不會把所有檔案加載進去的話會報錯,但是不會停止編譯 */
    "composite": true,                     /* 是否編譯建構引用項目  */
    "incremental": true,                   /* 是否啟用增量編譯*/
    "tsBuildInfoFile": "./",               /* 指定檔案用來存儲增量編譯資訊 */
    "removeComments": true,                /* removeComments的值為true或false,用于指定是否将編譯後的檔案中的注釋删掉,設為true的話即删掉注釋,預設為false */
    "noEmit": true,                        /* 不生成編譯檔案,這個一般比較少用 */
    "importHelpers": true,                 /* importHelpers的值為true或false,指定是否引入tslib裡的輔助工具函數,預設為false */
    "downlevelIteration": true,            /* 當target為'ES5' or 'ES3'時,為'for-of', spread, and destructuring'中的疊代器提供完全支援 */
    "isolatedModules": true,               /* isolatedModules的值為true或false,指定是否将每個檔案作為單獨的子產品,預設為true,它不可以和declaration同時設定 */
    "newLine": "lf",                       /* 指定換行符。可選`crlf`和`LF`兩種 */

    /* Strict Type-Checking Options */
    "strict": true /* strict的值為true或false,用于指定是否啟動所有類型檢查,如果設為true則會同時開啟下面這幾個嚴格類型檢查,預設為false */,
    "noImplicitAny": true,                 /* noImplicitAny的值為true或false,如果我們沒有為一些值設定明确的類型,編譯器會預設認為這個值為any,如果noImplicitAny的值為true的話。則沒有明确的類型會報錯。預設值為false */
    "strictNullChecks": true,              /* strictNullChecks為true時,null和undefined值不能賦給非這兩種類型的值,别的類型也不能賦給他們,除了any類型。還有個例外就是undefined可以指派給void類型 */
    "strictFunctionTypes": true,           /* strictFunctionTypes的值為true或false,用于指定是否使用函數參數雙向協變檢查 */
    "strictBindCallApply": true,           /* 設為true後會對bind、call和apply綁定的方法的參數的檢測是嚴格檢測的 */
    "strictPropertyInitialization": true,  /* 設為true後會檢查類的非undefined屬性是否已經在構造函數裡初始化,如果要開啟這項,需要同時開啟strictNullChecks,預設為false */
   "noImplicitThis": true,                /* 當this表達式的值為any類型的時候,生成一個錯誤 */
    "alwaysStrict": true,                  /* alwaysStrict的值為true或false,指定始終以嚴格模式檢查每個子產品,并且在編譯之後的js檔案中加入"use strict"字元串,用來告訴浏覽器該js為嚴格模式 */

    /* Additional Checks */
    "noUnusedLocals": true,                /* 用于檢查是否有定義了但是沒有使用的變量,對于這一點的檢測,使用eslint可以在你書寫代碼的時候做提示,你可以配合使用。它的預設值為false */
    "noUnusedParameters": true,            /* 用于檢查是否有在函數體中沒有使用的參數,這個也可以配合eslint來做檢查,預設為false */
    "noImplicitReturns": true,             /* 用于檢查函數是否有傳回值,設為true後,如果函數沒有傳回值則會提示,預設為false */
    "noFallthroughCasesInSwitch": true,    /* 用于檢查switch中是否有case沒有使用break跳出switch,預設為false */

    /* Module Resolution Options */
    "moduleResolution": "node",            /* 用于選擇子產品解析政策,有'node'和'classic'兩種類型' */
    "baseUrl": "./",                       /* baseUrl用于設定解析非相對子產品名稱的基本目錄,相對子產品不會受baseUrl的影響 */
    "paths": {},                           /* 用于設定子產品名稱到基于baseUrl的路徑映射 */
    "rootDirs": [],                        /* rootDirs可以指定一個路徑清單,在建構時編譯器會将這個路徑清單中的路徑的内容都放到一個檔案夾中 */
    "typeRoots": [],                       /* typeRoots用來指定聲明檔案或檔案夾的路徑清單,如果指定了此項,則隻有在這裡列出的聲明檔案才會被加載 */
    "types": [],                           /* types用來指定需要包含的子產品,隻有在這裡列出的子產品的聲明檔案才會被加載進來 */
    "allowSyntheticDefaultImports": true,  /* 用來指定允許從沒有預設導出的子產品中預設導入 */
    "esModuleInterop": true /* 通過為導入内容建立命名空間,實作CommonJS和ES子產品之間的互操作性 */,
    "preserveSymlinks": true,              /* 不把符号連結解析為其真實路徑,具體可以了解下webpack和nodejs的symlink相關知識 */

    /* Source Map Options */
    "sourceRoot": "",                      /* sourceRoot用于指定調試器應該找到TypeScript檔案而不是源檔案位置,這個值會被寫進.map檔案裡 */
    "mapRoot": "",                         /* mapRoot用于指定調試器找到映射檔案而非生成檔案的位置,指定map檔案的根路徑,該選項會影響.map檔案中的sources屬性 */
    "inlineSourceMap": true,               /* 指定是否将map檔案的内容和js檔案編譯在同一個js檔案中,如果設為true,則map的内容會以//# sourceMappingURL=然後拼接base64字元串的形式插入在js檔案底部 */
    "inlineSources": true,                 /* 用于指定是否進一步将.ts檔案的内容也包含到輸入檔案中 */

    /* Experimental Options */
    "experimentalDecorators": true /* 用于指定是否啟用實驗性的裝飾器特性 */
    "emitDecoratorMetadata": true,         /* 用于指定是否為裝飾器提供中繼資料支援,關于中繼資料,也是ES6的新标準,可以通過Reflect提供的靜态方法擷取中繼資料,如果需要使用Reflect的一些方法,需要引入ES2015.Reflect這個庫 */
  }
  "files": [], // files可以配置一個數組清單,裡面包含指定檔案的相對或絕對路徑,編譯器在編譯的時候隻會編譯包含在files中列出的檔案,如果不指定,則取決于有沒有設定include選項,如果沒有include選項,則預設會編譯根目錄以及所有子目錄中的檔案。這裡列出的路徑必須是指定檔案,而不是某個檔案夾,而且不能使用* ? **/ 等通配符
  "include": [],  // include也可以指定要編譯的路徑清單,但是和files的差別在于,這裡的路徑可以是檔案夾,也可以是檔案,可以使用相對和絕對路徑,而且可以使用通配符,比如"./src"即表示要編譯src檔案夾下的所有檔案以及子檔案夾的檔案
  "exclude": [],  // exclude表示要排除的、不編譯的檔案,它也可以指定一個清單,規則和include一樣,可以是檔案或檔案夾,可以是相對路徑或絕對路徑,可以使用通配符
  "extends": "",   // extends可以通過指定一個其他的tsconfig.json檔案路徑,來繼承這個配置檔案裡的配置,繼承來的檔案的配置會覆寫目前檔案定義的配置。TS在3.2版本開始,支援繼承一個來自Node.js包的tsconfig.json配置檔案
  "compileOnSave": true,  // compileOnSave的值是true或false,如果設為true,在我們編輯了項目中的檔案儲存的時候,編輯器會根據tsconfig.json中的配置重新生成檔案,不過這個要編輯器支援
  "references": [],  // 一個對象數組,指定要引用的項目
}
      

每日一題

https://github.com/WindrunnerMax/EveryDay
      

參考

https://www.typescriptlang.org/play/
https://zhuanlan.zhihu.com/p/443995763
https://zhuanlan.zhihu.com/p/353156044
https://tslang.baiqian.ltd/release-notes/typescript-2.7.html
https://www.typescriptlang.org/docs/handbook/utility-types.html
https://levelup.gitconnected.com/intrinsic-types-in-typescript-8b9f814410d
https://jkchao.github.io/typescript-book-chinese/tips/covarianceAndContravariance.html
https://github.com/xcatliu/typescript-tutorial/blob/master/basics/declaration-files.md
      

繼續閱讀