原文链接:TypeScript入门
之前阅读vue源码的时候发现有TypeScript,一脸懵逼,因此需要入个门。
最近在新环境的日常工作中也需要用到TypeScript,学习过程中遇到一些疑惑,做了记录
个人觉得还是比较适合TypeScript入门的同学阅读的,因为我遇到的这些疑惑,可能你也会遇到。
- ts类型中的?,<>意思是什么?
- 什么是duck typing?
- constructor之前的变量定义是什么?
- declare是什么?
- ts中unknown, void, null和undefined,never区别是什么?
- ts中的泛型约束是什么?
- 数组类型的两种定义方式
- ts中的类型断言
- 泛型函数与泛型接口
- 如何理解as const?
- declare global是什么意思?
- 如何在TypeScript环境增加一个全局变量?
- interface可以继承吗?
- typescript中的&是什么意思?
- interface与type的区别是什么?
- enum作为一种类型是什么意思?
- 项目中xxx.d.ts的declare module '*.scss'是什么意思?
declare module
还可以做什么? - typescript如何约束Promise的类型?
- typescript中的keyof如何使用?
- typescript中的typeof如何使用?
- typescript中的
non-null operator
是什么?
ts类型中的?意思是什么?
// https://github.com/vuejs/vue/blob/dev/src/core/observer/watcher.js before: ?Function; options?: ?Object,
这是ts的interface中的一个概念。ts的interface就是"duck typing"或者"structural subtyping",类型检查主要关注the shape that values have。因此我们先来熟悉一下interface,再引出?的解释。
TypeScript普通方式定义函数:
function print(obj: {label: string}) { console.log(obj.label); } let foo = {size: 10, label: "这是foo, 10斤"}; print(foo);
TypeScript interface方式定义函数:
interface labelInterface { label: string; } function print(obj: labelInterface) { console.log(obj.label); } let foo = {size: 10, label: "这是foo, 10斤"}; print(foo);
进入正题,TypeScript中的?
是什么意思?Optional Properties。
Optional Properties
- 并不是interface中的所有属性都是required的,一些存在特定条件下,一些根本不存在。
- Optional Properties适用于"option bags"的设计模式,这种设计模式意思是:我们传递一个对象到函数,这个函数只有几个属性,没有其他更多的属性。
- Optional Property的好处在于,清晰的看清楚有哪些属性,防止传入不属于该interface的属性。
interface SquareConfig { color?: string; width?: number; } function createSquare(config: SquareConfig): {color: string; area: number} { let newSquare = {color: "white", area: 100}; if (config.clor) { // Error: Property 'clor' does not exist on type 'SquareConfig' newSquare.color = config.color; } if (config.width) { newSquare.area = config.width * config.width; } return newSquare; } let mySquare = createSquare({color: "black"});
Interfaces with optional properties are written similar to other interfaces, with each optional property denoted by a ? at the end of the property name in the declaration.
什么是?
和Optional Properties呢?interface的某些非required属性名的末尾,添加?
这是一个optional property,其实就是字面意思,条件属性。
Optional Property只是属性名,也就是options?: ?Object,
中options后的问号,拿属性值类型前的问号是什么意思,也就是?Object
,是什么意思?
此处的问号代表属性值类型是否可以是null类型,但是只有strictNullChecks为on时,值类型才能为null。
/** * @type {?number} * strictNullChecks: true -- number | null * strictNullChecks: off -- number * */ var nullable;
我们的例子中,options?:?Object的意思是options的值类型可以是Object,null(仅在strictNullChecks为true时允许)。
ts类型中的<>什么意思?
deps: Array<Dep>a newDeps: Array<Dep>
ts中的数组类型与java中的定义类似:
let list: number[] = [1, 2, 3]; let list: Array<number> = [1, 2, 3];
什么是duck typing?
duck test。如果"走路像鸭子,叫声像鸭子,那么这就是鸭子"。
在computer programming,用于'判断对象是否可以按照预期的目的使用'。
通常的typing中,适用性取决于对象的type。duck typing不一样,对象的适用性取决于指定method或property的存在与否,而不是取决于对象自身的类型。
前端工程师基本都是duck typing,因为JavaScript没有type。 --这话是我说的
Python3 example
class Duck: def fly(self): print("Duck flying") class Airplane: def fly(self): print("Airplane flying") class Whale: def swim(self): print("Whale swimming") def lift_off(entity): entity.fly() duck = Duck() airplane = Airplane() whale = Whale() lift_off(duck) # prints `Duck flying` lift_off(airplane) # prints `Airplane flying` lift_off(whale) # Throws the error `'Whale' object has no attribute 'fly'`
Javascript example
class Duck { fly() { console.log("Duck flying") } } class Airplane { fly() { console.log("Airplane flying") } } class Whale { swim() { console.log("Whale swimming") } } function liftOff(entity) { entity.fly() } const duck = new Duck(); const airplane = new Airplane(); const whale = new Whale(); liftOff(duck); // Duck flying liftOff(airplane); // Airplane flying liftOff(whale); // Uncaught TypeError: entity.fly is not a function
constructor之前的变量定义是什么?
例如vnode的定义:
export default class VNode { tag: string | void; data: VNodeData | void; children: ?Array<VNode>; text: string | void; elm: Node | void; ns: string | void; context: Component | void; // rendered in this component's scope key: string | number | void; componentOptions: VNodeComponentOptions | void; componentInstance: Component | void; // component instance parent: VNode | void; // component placeholder node // strictly internal raw: boolean; // contains raw HTML? (server only) isStatic: boolean; // hoisted static node isRootInsert: boolean; // necessary for enter transition check isComment: boolean; // empty comment placeholder? isCloned: boolean; // is a cloned node? isOnce: boolean; // is a v-once node? asyncFactory: Function | void; // async component factory function asyncMeta: Object | void; isAsyncPlaceholder: boolean; ssrContext: Object | void; fnContext: Component | void; // real context vm for functional nodes fnOptions: ?ComponentOptions; // for SSR caching fnScopeId: ?string; // functional scope id support constructor () ... }
http://www.typescriptlang.org...
typeScript中的class要比es6的多一项:property。这和java或者c#中的一致。
property constructor method
实际上es6提供了一种私有变量,仅仅能在class内部访问。
class Rectangle { #height = 0; #width; constructor(height, width) { this.#height = height; this.#width = width; } }
冒号后面的:VNode什么意思?
export function cloneVNode (vnode: VNode): VNode { ... }
TypeScript中的函数返回值类型。
declare是什么?
声明这是一个definition。
- declare是ts中用于写定义文件的关键字。
- declare可以定义全局变量,全局函数,全局命名空间,class等等。
- declare可以按照下面这样去使用:
declare var foo:number; declare function greet(greeting: string): void; declare namespace myLib { function makeGreeting(s: string): string; let numberOfGreeting: number; } declare function getWidget(n: number): Widget; declare function getWidget(s: string): Widget[]; declare class Greeter { constructor(greeting: string); greeting: string; showGreeting(): void; }
ts中any,unknown, void, null和undefined,never区别是什么?
null,undefined就是js中的意思。
any: 任意类型,谨慎使用,避免使typescript变成anyscript
unknown: 与any类似,但是比any更加安全
void: 通常用于返回值的函数
never:never occur 从来不会发生的类型,例如永远不会有结果的,抛出异常或者死循环。
ts中的泛型约束是什么?
基于string(boolean, Function)类型
function loggingIdentity<T extends string>(arg: T): T { console.log(arg.length); return arg; } loggingIdentity("hello"); // 5 loggingIdentity(2); // Argument of type 'number' is not assignable to parameter of type 'string'.
基于自定义的interface
interface Lengthwise { length: number; } function loggingIdentity<T extends Lengthwise>(arg: T): T { console.log(arg.length); // Now we know it has a .length property, so no more error return arg; } loggingIdentity(3); // Error, number doesn't have a .length property loggingIdentity({length: 10, value: 3}); // 10
ts2.8发布说明
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html 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"
同时支持type和interface两种类型的泛型约束
interface reduxModel<T> { reducers: T extends string ? {[x in T]: () => void}: T, } type TType = "foo" | "bar" | 'baz' interface TInterface { "foo": () => void, "bar": () => void, 'baz': () => void } const ireducers = { "foo": () => void } const model : reduxModel<TType> = { reducers: ireducers // 正常运行 } const model : reduxModel<TInterface> = { reducers: ireducers // Type '{ foo: () => undefined; }' is missing the following properties from type 'TInterface': "bar", 'baz' }
数组类型的两种定义方式
Array<类型>
Array后面加一个<>,<>内声明元素类型。
type Foo= Array<string>;
interface Bar { baz: Array<{ name: string, age: number, }> }
类型[]
元素类型后面加一个[]。
type Foo = string[]
interface Bar { baz : { name: string, age: number, }[] }
ts中的类型断言
TypeScript允许我们覆盖推断和分析出的视图类型为我们想要的任意方式,这种机制叫做类型断言(Type Assertion),类型断言会告诉编译器你比它更加知道具体是哪种类型,编译器不用再二次推断了。
类型断言往往是发生在编译器编译期间,用于提示编译器如何分析我们的代码。
- 语法
- 迁移js代码
- 类型断言的问题
- 指定event类型
- 慎用as any和as unknown
- type与类型断言
语法
interface Foo { name: string, } type Any = any; let a:Foo = {} as Foo; let a:Foo = {} as Any;
any是任意类型的子类型,所以任意类型都可以被as any,还是建议谨慎使用,避免变为anyscript。
迁移js代码
var foo = {}; foo.bar = 123; // Error: property 'bar' does not exist on `{}` foo.bas = 'hello'; // Error: property 'bas' does not exist on `{}`
interface Foo { bar: number; bas: string; } var foo = {} as Foo; foo.bar = 123; foo.bas = 'hello'; // 注释掉这一行也不会报错
类型断言的问题
foo.bas = 'hello'; // 注释掉这一行也不会报错
如果是下面的方式就会报错了,会提示缺少bas的定义
interface Foo { bar: number; bas: string; } var foo : Foo= { bar: 123 };
所以说,类型断言是不够严谨的,建议使用var foo : Foo
这种方式。
指定event类型
function handler (event: Event) { let mouseEvent = event as MouseEvent; }
function handler(event: Event) { let element = event as HTMLElement; // HTMLElement不是一个完全的event子类型,因此不能充分重叠,需要加一个unknown或者any }
二次断言编译提示取消:
function handler(event: Event) { let element = event as unknown as HTMLElement; // Okay! }
慎用as any和as unknown
通常情况是类型断言S和T的话,S为T的子类型,或者T为S的子类型,这种是相对安全的。
假如是用as any或者as unknown,是非常不安全的。慎用!慎用!
// 谨慎使用 as any as known
type与类型断言
type keys = 'foo' | 'bar' | 'baz',obj[key as keys]是什么意思?
与variable:type类似,这是另外一种类型约束。
如果不明白的花,看完下面这个demo就明白了。
type keys = 'foo' | 'bar' | 'baz' const obj = { foo: 'a', bar: 'b', baz: 'c' } const test = (key:any) => { return obj[key] ; // 提示错误 type 'any' can't be used to index type '{ foo: string; bar: string; baz: string; }'. }
如何解决这个报错呢?
第一种方式:类型约束
const test = (key:keys) => { return obj[key] ; }
第二种方式:类型断言(这种方式常用于第三方库的callback,返回值类型没有约束的情况)
const test = (key:any) => { return obj[key as keys] ; }
需要注意:obj[key as keys]中keys的类型可以少于obj的类型,反过来obj的属性不能少于keys的类型。
泛型函数与泛型接口
泛型函数
想想一个场景,我们希望函数的输入与输出类型一致。
你可能会这样做,但这并不能保障输入与输出类型一致。
function log(value: any):any { return value; }
通过泛型函数可以精准实现:函数名后加一个<T>
这里的T可以理解为泛型的名字。指定输入类型为T,返回值为T。
function log<T>(value: T):T { return value; }
这是一个泛型函数实例,如何定义一种泛型函数类型呢?
type Log = <T>(value: T) => T
使用泛型函数类型约束函数:
let log : Log = function <T>(value: T):T { return value; }
泛型接口
接口所有属性灵活,输入输出一致即可。
interface Log { <T>(value: T): T } let myLog: Log = log myLog("s")// "s" myLog(1)// 1
接口所有属性必须为同一类型。
interface Log<T> { (value: T): T } let myLog: Log<string> = log myLog("s")// "s" myLog(1)// Error
ts中的<>
在ts中,遇到<>的话,尖括号中间大多情况下都是类型。
- Array<string>
- <string>[]
- function <T>(value: T): T { ... }
- type MyType = <T>(value : T) => T
- interface MyInterface<T> { (value: T): T }
如何理解as const?
- 为了解决let赋值问题的,将一个mutable的变量改为readonly。
- 避免将类型推断为联合类型。
为了解决let赋值问题的,将一个mutable的变量改为readonly。
let x = "hello"; x = "world"; // 报错
第一种方式 const
const x = "hello"
第二种方式 "hello"类型
let x: "hello" = "hello"; x = "world"; //
第三种方式 discriminated unions
type Shape = | { kind: "circle", radius: number } | { kind: "square", sideLength: number } function getShapes(): readonly Shape[] { // to avoid widening in the first place. let result: readonly Shape[] = [ { kind: "circle", radius: 100, }, { kind: "square", sideLength: 50, }, ]; return result; }
避免将类型推断为联合类型。
避免将类型推断为 (boolean | typeof load)[],而是推断为[boolean, typeof load]。
export function useLoading() { const [isLoading, setState] = React.useState(false); const load = (aPromise: Promise<any>) => { setState(true); return aPromise.finally(() => setState(false)); }; return [isLoading, load] as const; // infers [boolean, typeof load] instead of (boolean | typeof load)[] }
declare global是什么意思?
是为了在全局命名空间做声明,比如为对象增加一个未定义的属性。
为Window增加csrf的定义
declare global { interface Window { csrf: string; } }
为String增加fancyFormat的定义
declare global { /*~ Here, declare things that go in the global namespace, or augment *~ existing declarations in the global namespace */ interface String { fancyFormat(opts: StringFormatOptions): string; } }
注意global作用域只能用于导出模块或者外部的模块声明
Augmentations for the global scope can only be directly nested in external modules or ambient module declarations.
如何在TypeScript环境增加一个全局变量?
比如我们想要实现下面的效果,但是会报错Property '__INITIAL_DATA__' does not exist
<script> window.__INITIAL_DATA__ = { "userID": "536891193569405430" }; </script> const initialData = window.__INITIAL_DATA__; // 报错
使用类型断言
const initialData = (window as any).__INITIAL_DATA__;
type InitialData = { userID: string; }; const initialData = (window as any).__INITIAL_DATA__ as InitialData; const userID = initialData.userID; // Type string
声明全局变量
declare var __INITIAL_DATA__: InitialData;
const initialData = __INITIAL_DATA__; const initialData = window.__INITIAL_DATA__;
在es模块中,有import,export的,需要这样做:
export function someExportedFunction() { // ... } declare global { var __INITIAL_DATA__: InitialData; } const initialData = window.__INITIAL_DATA__;
如果在很多文件都用到的话,可以用一个globals.d.ts文件。
利用interface合并
interface Window { __INITIAL_DATA__: InitialData; } const initialData = window.__INITIAL_DATA__;
在js模块中需要像下面这样:
export function someExportedFunction() { // ... } declare global { interface Window { __INITIAL_DATA__: InitialData; } } const initialData = window.__INITIAL_DATA__;
interface可以继承吗?
可以的。
interface Base { foo: string; } interface Props extends Base { bar: string baz?: string } const test = (props: Props) => { console.log(props); } test({ foo: 'hello' }) // Property 'bar' is missing in type '{ foo: string; }' but required in type 'Props' test({ foo: 'hello', bar: 'world' })
当Props继承了Base之后,实际上它最终变成了下面这样:
interface Props extends Base { foo: string; bar: string baz?: string }
Props可以覆盖Base吗?可以,但是只能是required覆盖optional,optional不能覆盖required。
// ✅ interface Base { foo?: string; } interface Props extends Base { foo: string; bar: string baz?: string }
// ❌ interface Base { foo: string; } interface Props extends Base { foo?: string; bar: string baz?: string }
typescript中的&是什么意思?
在react的dts文件中有这样一个定义。
type PropsWithChildren<P> = P & { children?: ReactNode };
typescript中的&指的是交叉类型。
interface ErrorHandling { success: boolean; error?: { message: string }; } interface ArtworksData { artworks: { title: string }[]; } interface ArtistsData { artists: { name: string }[]; } // These interfaces are composed to have // consistent error handling, and their own data. type ArtworksResponse = ArtworksData & ErrorHandling; type ArtistsResponse = ArtistsData & ErrorHandling; const handleArtistsResponse = (response: ArtistsResponse) => { if (response.error) { console.error(response.error.message); return; } console.log(response.artists); };
知道&是ts中的交叉类型以后,我们就明白PropsWithChildren的意思了,而且也明白为什么react的函数式组件会比普通函数组件多了children属性。
它的意思是PropsWithChildren类型是P和对象{children?: ReactNode}的交叉类型,也就是通过&连接两个对象之后,最终生成的对象是拥有children这个可选属性的。
interface与type的区别是什么?
An interface can be named in an extends or implements clause, but a type alias for an object type literal cannot.An interface can have multiple merged declarations, but a type alias for an object type literal cannot.
- interface可以继承(比如用extends),type不可以
- interface可以实现有多个合并声明,type不可以
enum作为一种类型是什么意思?
在阅读pixi.js的源码中,发现有将enum作为了一种类型。
enum也可以作为一种类型去约束。
// pixi/constants export enum BLEND_MODES { NORMAL = 0, ADD = 1, MULTIPLY = 2, SCREEN = 3, OVERLAY = 4, } export enum ANOTHER_ENUM { FOO = 5, BAR = 6 } import { BLEND_MODES } from '@pixi/constants'; export class Sprite extends Container { public blendMode: BLEND_MODES; constructor(){ this.blendMode = BLEND_MODES.NORMAL; // 最佳 // this.blendMode = 0 这样是可以的,次之 // this.blendMode = ANOTHER_ENUM.FOO 这样ts会报错 } }
项目中xxx.d.ts的declare module '*.scss'是什么意思?declare module还可以做什么?
项目中xxx.d.ts的declare module '*.scss'是什么意思?
// externals.d.ts declare module '*.scss'
默认情况下import style from 'style.scss'在ts的ide校验器里会报错,那就用d.ts假定定义所有scss结尾的文件是module。--社长
假设将declare module '*.scss'注释掉,ide会报错,但是可以通过lint。
declare module还可以做什么?
当我们引入了一个微软官方@types/*中不存在的自定义包时,ide会报错。
例如下面这样:
如何解决这个报红的错误呢?declare module
// typing.d.ts declare module 'visual-array'
这样报红就消失了。
typescript如何约束Promise的类型?
Promise泛型函数
interface Promise<T> { then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): Promise<TResult1 | TResult2>; catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): Promise<T | TResult>; }
interface foo { bar: ()=>Promise<string>, baz: ()=>Promise<number[]>, car: (id)=>Promise<boolean[]> }
typescript中的keyof如何使用?
最简
type Point = { x: number; y: number }; type P = keyof Point; // 'x' | 'y' let foo: P = 'x'; let bar: P = 'y'; let baz: P = 'z'; // ❌
常用
interface Person { name: string; age: number; location: string; } type K1 = keyof Person; // "name" | "age" | "location" type K2 = keyof Person[]; // "length" | "push" | "pop" | "concat" | ... type K3 = keyof { [x: string]: Person }; // string type P1 = Person["name"]; // string type P2 = Person["name" | "age"]; // string | number type P3 = string["charAt"]; // (pos: number) => string type P4 = string[]["push"]; // (...items: string[]) => number type P5 = string[][0]; // string
keyof使得函数类型安全(type-safe)
function getProperty<T, K extends keyof T>(obj: T, key: K) { return obj[key]; // Inferred type is T[K] } function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]) { obj[key] = value; } let x = { foo: 10, bar: "hello!" }; let foo = getProperty(x, "foo"); // number let bar = getProperty(x, "bar"); // string let oops = getProperty(x, "wargarbl"); // Error! "wargarbl" is not "foo" | "bar" setProperty(x, "foo", "string"); // Error!, string expected number
Partial,Required,Readonly,Pick 泛型工具类型的实现原理
type Partial<T> = { [P in keyof T]? : T[P]; }
type Required<T> = { [P in keyof T]?- : T[P]; }
type Readonly<T> = { readonly [P in keyof T] : T[P]; }
type Pick<T, K extends keyof T> = { [P in K]: T[P] }
typescript中的typeof如何使用?
js中的typeof主要用于表达式上下文,而ts中的typeof主要用于类型上下文。
let s = "hello"; let n: typeof s; // ^ = let n: string
type Predicate = (x: unknown) => boolean; type K = ReturnType<Predicate>; // ^ = type K = boolean
function f() { return { x: 10, y: 3 }; } type P = ReturnType<typeof f>; // ^ = type P = { // x: number; // y: number; // }
typescript中的non-null assert operator是什么?
非null断言操作符:当为null时,发生断言,抛出异常。
可选链:当为null/undefined时,返回undefined。
非空断言操作符和可选链操作符测试
// Non-Null Assertion Operator const obj = null; interface Entity { name?: string; } // 非空断言操作符 function nonNull(e?: Entity) { const s = e!.name; // 发生断言,抛出TypeError } try { nonNull(obj); } catch (e) { console.error("nonNull catch", e); // TypeError: Cannot read property 'name' of null } // 可选链 function optionalChaining(e?: Entity) { const s = e?.name; console.log(s); // undefined } optionalChaining(obj);
用于函数返回值空检测
function returnNullFunc() { return null; } try { returnNullFunc()!.age; } catch (e) { console.error("returnNullFunc", e); // TypeError: Cannot read property 'age' of null } function returnNonNullFunc() { return { age: "18" }; } returnNonNullFunc()!.age;