TS內置類型與拓展
TypeScript
具有類型系統,且是JavaScript
的超集,其可以編譯成普通的JavaScript
代碼,也就是說,其是帶有類型檢查的JavaScript
。
內置類型
TypeScript
提供了幾種實用程序類型來促進常見的類型轉換,這些類型在全局范圍內可用。
Partial
Partial<Type>
構造一個類型使Type
的所有屬性都設置為可選。
/**
* Make all properties in T optional
*/
type Partial<T> = {
[P in keyof T]?: T[P];
};
interface Example {
a: string;
b: number;
}
type PartialExample = Partial<Example>;
/**
* PartialExample
* interface {
* a?: string | undefined;
* b?: number | undefined;
* }
*/
Required
Required<Type>
構造一個類型使Type
的所有屬性都設置為required
,與Partial<Type>
功能相反。
/**
* Make all properties in T required
*/
type Required<T> = {
[P in keyof T]-?: T[P];
};
interface Example {
a?: string;
b?: number;
}
type RequiredExample = Required<Example>;
/**
* RequiredExample
* interface {
* a: string;
* b: number;
* }
*/
Readonly
Required<Type>
構造一個類型使Type
的所有屬性都設置為readonly
,這意味着構造類型的屬性都是只讀的,不能被修改,這對使用Object.freeze()
方法的對象非常有用。
/**
* Make all properties in T readonly
*/
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
interface Example {
a: string;
b: number;
}
type ReadonlyExample = Readonly<Example>;
/**
* 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<K extends keyof any, T> = {
[P in K]: T;
};
type RecordType = Record<string, string|number>;
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<T, K extends keyof T> = {
[P in K]: T[P];
};
interface Example {
a: string;
b: number;
c: symbol;
}
type PickExample = Pick<Example, "a"|"b">;
/**
* 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<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
interface Example {
a: string;
b: number;
c: symbol;
}
type OmitExample = Omit<Example, "a"|"b">;
/**
* OmitExample
* interface {
* c: symbol;
* }
*/
Exclude
Exclude<UnionType, ExcludedMembers>
通過從UnionType
中排除可分配給ExcludedMembers
的所有聯合成員來構造類型。
/**
* Exclude from T those types that are assignable to U
*/
type Exclude<T, U> = 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, U> = 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> = T extends null | undefined ? never : T;
type NonNullableExample = NonNullable<number|string|null|undefined>;
/**
* NonNullableExample
* string | number
*/
Parameters
Parameters<Type>
從函數類型Type
的參數中使用的類型構造元組類型。
/**
* Obtain the parameters of a function type in a tuple
*/
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
type FnType = (a1: number, a2: string) => void;
type ParametersExample = Parameters<FnType>;
/**
* ParametersExample
* [a1: number, a2: string]
*/
ConstructorParameters
ConstructorParameters<Type>
從構造函數類型的類型構造元組或數組類型,其產生一個包含所有參數類型的元組類型。
/**
* Obtain the parameters of a constructor function type in a tuple
*/
type ConstructorParameters<T extends abstract new (...args: any) => 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<ExampleConstructor>;
/**
* ConstructorParametersExample
* [a: string, b: number]
*/
ReturnType
ReturnType<Type>
構造一個由函數Type
的返回類型組成的類型。
/**
* Obtain the return type of a function type
*/
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
type FnType = (a1: number, a2: string) => string | number;
type ReturnTypeExample = ReturnType<FnType>;
/**
* ReturnTypeExample
* string | number
*/
InstanceType
InstanceType<Type>
構造一個由Type
中構造函數的實例類型組成的類型。
/**
* Obtain the return type of a constructor function type
*/
type InstanceType<T extends abstract new (...args: any) => 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<ExampleConstructor>;
// 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> = T extends (this: infer U, ...args: any[]) => any ? U : unknown;
function toHex(this: Number) {
return this.toString(16);
}
type ThisParameterTypeExample = ThisParameterType<typeof toHex>;
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<T> = unknown extends ThisParameterType<T> ? T : T extends (...args: infer A) => infer R ? (...args: A) => R : T;
function toHex(this: Number) {
return this.toString(16);
}
type OmitThisParameterExample = OmitThisParameter<typeof toHex>;
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<T> { }
// 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<S extends string> = intrinsic;
type UppercaseExample = Uppercase<"abc">;
/**
* UppercaseExample
* ABC
*/
Lowercase
Lowercase<StringType>
將StringType
轉為小寫。
/**
* Convert string literal type to lowercase
*/
type Lowercase<S extends string> = intrinsic;
type LowercaseExample = Lowercase<"ABC">;
/**
* LowercaseExample
* abc
*/
Capitalize
Capitalize<StringType>
將StringType
首字母轉為大寫。
/**
* Convert first character of string literal type to uppercase
*/
type Capitalize<S extends string> = intrinsic;
type CapitalizeExample = Capitalize<"abc">;
/**
* CapitalizeExample
* Abc
*/
Uncapitalize
Uncapitalize<StringType>
將StringType
首字母轉為小寫。
/**
* Convert first character of string literal type to lowercase
*/
type Uncapitalize<S extends string> = intrinsic;
type UncapitalizeExample = Uncapitalize<"ABC">;
/**
* CapitalizeExample
* aBC
*/
拓展
TypeScript
中常用的一些語法以及概念。
泛型
泛型Generics
是指在定義函數、接口或類的時候,不預先指定具體的類型,而在使用的時候再指定類型的一種特性。舉一個簡單的例子,如果需要實現一個生成數組的函數,這個數組會填充默認值,這個數組填充的類型不需要事先指定,而可以在使用的時候指定。當然在這里使用new Array
組合fill
函數是一個效果。
function createArray<T>(value: T, length: number): T[] {
const result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
console.log(createArray<number>(1, 3)); // 不顯式地指定`number`也可以自動推斷
我們也可以約束T
的類型只能為number
與string
。
const createArray = <T extends number|string>(value: T, length: number): T[] => {
const result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
console.log(createArray<number>(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<T, K extends keyof T> = {
[P in K]: T[P];
};
在傳遞泛型的時候可以為T
指定默認值,使用范型編寫class
即泛型類也是完全支持的。
class Example<T = number> {
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<number>(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<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
有一些應用,tuple
轉union
,如[string, number, symbol] -> string | number | symbol
。
type ElementOf<T> = T extends Array<infer E> ? E : never;
type TTuple = [string, number, symbol];
type ToUnion = ElementOf<TTuple>; // 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> = T extends (args1: infer R, ...rest: any[]) => any ? R : never;
type firstArg = FirstParameter<fn>; // 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 = <T extends DateParams>(...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<typeof Date>));
};
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: <T extends unknown[]>(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
/// <reference types="jquery" />
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
的子類型呢。
Greyhound → Greyhound
。Greyhound → Animal
。Animal → Animal
。Animal → Greyhound
。
讓我們來思考一下如何解答這個問題,首先我們假設f
是一個以Dog → Dog
為參數的函數,它的返回值並不重要,為了具體描述問題,我們假設函數結構體是這樣的f :(Dog → Dog ) → String
,現在我想給函數f
傳入某個函數g
來調用,我們來瞧瞧當g
為以上四種類型時,會發生什么情況。
1.我們假設g : Greyhound → Greyhound
,f(g)
的類型是否安全?
不安全,因為在f
內調用它的參數(g)
函數時,使用的參數可能是一個不同於灰狗但又是狗的子類型,例如GermanShepherd
牧羊犬。
2.我們假設g : Greyhound → Animal
,f(g)
的類型是否安全?
不安全。理由同1
。
3.我們假設g : Animal → Animal
,f(g)
的類型是否安全?
不安全。因為f
有可能在調用完參數之后,讓返回值也就是Animal
動物狗叫,並非所有動物都會狗叫。
4.我們假設g : Animal → Greyhound
,f(g)
的類型是否安全?
是的,它的類型是安全的,首先f
可能會以任何狗的品種來作為參數調用,而所有的狗都是動物,其次,它可能會假設結果是一條狗,而所有的灰狗都是狗。
如上所述,我們得出結論(Animal → Greyhound) ≼ (Dog → Dog)
返回值類型很容易理解,灰狗是狗的子類型,但參數類型則是相反的,動物是狗的父類。用合適的術語來描述這個奇怪的表現,可以說我們允許一個函數類型中,返回值類型是協變的,而參數類型是逆變的。返回值類型是協變的,意思是A ≼ B
就意味着(T → A ) ≼ ( T → B )
,參數類型是逆變的,意思是A ≼ B
就意味着(B → T ) ≼ ( A → T )
即A
和B
的位置顛倒過來了。一個有趣的現象是在TypeScript
中,參數類型是雙向協變的,也就是說既是協變又是逆變的,而這並不安全,但是現在你可以在TypeScript 2.6
版本中通過--strictFunctionTypes
或--strict
標記來修復這個問題。
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://segmentfault.com/q/1010000040197076
https://www.cnblogs.com/terrymin/p/13897214.html
https://www.cnblogs.com/wangleicode/p/10937707.html
https://blog.csdn.net/qq_43869822/article/details/121664818
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