TypeScript Basic Notes
TypeScript Toolchain
TypeScript Installation
npm i -D typescript
npm i -D react react-dom @types/node @types/react @types/react-dom
TypeScript Configuration
npx tsconfig.json
npx tsc --init
npm i -D @tsconfig/create-react-app
Basic tsconfig
:
extends
:@tsconfig/recommended/tsconfig.json
.@tsconfig/create-react-app/tsconfig.json
.@tsconfig/node16/tsconfig.json
.@tsconfig/deno/tsconfig.json
.
include
.exclude
.buildOptions
.compilerOptions
.watchOptions
.tsNode
.
{
"include": ["./src/**/*"],
"exclude": ["node_modules", "build", "dist", "coverage"],
"compilerOptions": {
/* 基本选项 */
"target": "es2022", // 'ES3', 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
"module": "NodeNext", // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
"lib": ["es2022"], // 指定要包含在编译中的库文件
"allowJs": true, // 允许编译 javascript 文件
"checkJs": true, // 报告 javascript 文件中的错误
"jsx": "react", // 'preserve', 'react-native', or 'react'
"declaration": true, // 生成相应的 '.d.ts' 文件
"outFile": "./", // 将输出文件合并为一个文件
"outDir": "./dist/", // 指定输出目录
"rootDir": "./", // 用来控制输出目录结构 --outDir.
"removeComments": true, // 删除编译后的所有的注释
"noEmit": true, // 不生成输出文件
"importHelpers": true, // 从 tslib 导入辅助工具函数
"isolatedModules": true, // 将每个文件做为单独的模块 (与 'ts.transpileModule' 类似)
"resolveJsonModule": true,
/* 严格的类型检查选项 */
"strict": true, // 启用所有严格类型检查选项
"noImplicitAny": true, // 在表达式和声明上有隐含的 any类型时报错
"strictNullChecks": true, // 启用严格的 null 检查
"noImplicitThis": true, // 当 this 表达式值为 any 类型的时候,生成一个错误
"alwaysStrict": true, // 以严格模式检查每个模块,并在每个文件里加入 'use strict'
"skipLibCheck": true,
/* 额外的检查 */
"noUnusedLocals": true, // 有未使用的变量时,抛出错误
"noUnusedParameters": true, // 有未使用的参数时,抛出错误
"noImplicitReturns": true, // 并不是所有函数里的代码都有返回值时,抛出错误
"noFallthroughCasesInSwitch": true, // 报告 switch 语句的 fallthrough 错误
"noUncheckedIndexedAccess": true,
/* 模块解析选项 */
"moduleResolution": "NodeNext", // 选择模块解析策略: 'node' (Node.js) or 'classic'
"moduleDetection": "force",
"esModuleInterop": true,
"baseUrl": "./", // 用于解析非相对模块名称的基目录
"paths": {
"@components": ["src/components"],
"@components/*": ["src/components/*"],
"@config": ["src/config"],
"@config/*": ["src/config/*"],
"@hooks": ["src/hooks"],
"@hooks/*": ["src/hooks/*"],
"@images": ["src/images"],
"@images/*": ["src/images/*"],
"@layouts": ["src/layouts"],
"@layouts/*": ["src/layouts/*"],
"@pages": ["src/pages"],
"@pages/*": ["src/pages/*"],
"@styles": ["src/styles"],
"@styles/*": ["src/styles/*"],
"@templates": ["src/templates"],
"@templates/*": ["src/templates/*"],
"@types": ["src/types"],
"@types/*": ["src/types/*"]
}, // 模块名到基于 baseUrl 的路径映射的列表
"rootDirs": [], // 根文件夹列表,其组合内容表示项目运行时的结构内容
"typeRoots": [], // 包含类型声明的文件列表
"types": [], // 需要包含的类型声明文件名列表
"allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入。
/* Source Map Options */
"sourceMap": true,
"sourceRoot": "./", // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
"mapRoot": "./", // 指定调试器应该找到映射文件而不是生成文件的位置
"inlineSourceMap": true, // 生成单个 source map 文件,而不是将 source maps 生成不同的文件
"inlineSources": true // 将代码与 source map 生成到一个文件中
}
}
Webpack Configuration
npm i -D typescript ts-loader source-map-loader
const path = require('node:path')
module.exports = {
entry: './src/index.tsx',
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
devtool: 'source-map',
resolve: {
extensions: ['.js', '.json', '.ts', '.tsx'],
},
module: {
rules: [
{
test: /\.(ts|tsx)$/,
loader: 'ts-loader',
},
{ enforce: 'pre', test: /\.js$/, loader: 'source-map-loader' },
],
},
externals: {
'react': 'React',
'react-dom': 'ReactDOM',
},
}
ESLint Configuration
npx eslint --init
Jest Configuration
npm i -D jest typescript ts-jest @types/jest
npx ts-jest config:init
npx jest
TypeScript DefinitelyTyped
- Library (
npm
package):@types/*
should bedependencies
, consumers can bring in type definitions used within. - Application:
@types/*
should bedevDependencies
, type definitions are just development-time tool.
TypeScript Compiler Performance
- Faster tools:
swc
/rome
. - Multithread:
ts-loader
+fork-ts-checker-plugin
. - Project references (
tsc -b
build mode):- Find
tsconfig
referenced projects. - Detect if they are up-to-date.
- Build out-of-date projects in correct order.
- Build provided
tsconfig
if itself or any dependencies have changed.
- Find
- Skip type checking (sometimes).
- Load
@types/
by need (include
/exclude
/compilerOptions.types
). tsc --listFiles
列出编译时包含文件列表,tsc --traceResolution
列出编译时包含文件原因.
TypeScript Project Reference
Project Reference
for TypeScript
compile and build Speed.
TypeScript Monorepo Configuration
- NPM workspaces.
TypeScript
references.
Modules
Declaration Type | Namespace | Type | Value |
---|---|---|---|
Namespace | X | X | |
Class | X | X | |
Enum | X | X | |
Interface | X | ||
Type Alias | X | ||
Function | X | ||
Variable | X |
Value means truly output JavaScript.
Globals Definition
{
"include": ["./src/**/*", "globals.d.ts", "index.d.ts"]
}
declare module '*.css'
// => import * as foo from './some/file.css';
declare module '*.png' {
const value: unknown
export = value
}
// => import logo from './logo.png';
// <img src={logo as string} />
declare module '*.jpg' {
const value: unknown
export = value
}
globals.d.ts
:
// npm i -D @types/react @types/react-dom
declare global {
namespace JSX {
interface Element extends React.ReactElement<any, any> {}
interface ElementClass extends React.Component<any> {
render: () => React.ReactNode
}
}
}
Library Definition
lib.d.ts
:
{
"compilerOptions": {
"target": "es5",
"lib": ["es6", "dom"]
}
}
Namespace
Namespace aliases:
import polygons = Shapes.Polygons
namespace Shapes {
export namespace Polygons {
export class Triangle {}
export class Square {}
}
}
const sq = new polygons.Square() // Same as 'new Shapes.Polygons.Square()'
Library namespace declaration with declaration merging:
export = React
export as namespace React
declare namespace React {
type ElementType<P = any> =
| {
[K in keyof JSX.IntrinsicElements]: P extends JSX.IntrinsicElements[K]
? K
: never
}[keyof JSX.IntrinsicElements]
| ComponentType<P>
}
namespace
compiles to IIFE
pattern:
namespace Utility {
export function log(msg) {
console.log(msg)
}
export function error(msg) {
console.log(msg)
}
}
;(function (Utility) {
Utility.log = log
Utility.log = error
})(Utility || (Utility = {}))
Unless authoring DefinitelyTyped type definitions for existing package, do not use namespaces.
Namespaces do not match up to modern JavaScript module semantics, their automatic member assignments can make code confusing to read.
Module Resolution
Classic Module Resolution
import { a } from './module'
:
/root/src/folder/module.ts
./root/src/folder/module.d.ts
.
import { a } from 'module'
:
/root/src/folder/module.ts
./root/src/folder/module.d.ts
./root/src/module.ts
./root/src/module.d.ts
./root/module.ts
./root/module.d.ts
./module.ts
./module.d.ts
.
Node Module Resolution
const x = require('./module')
:
/root/src/module.ts
./root/src/module.tsx
./root/src/module.d.ts
./root/src/module/package.json
+{ "types": "lib/mainModule.ts" }
=/root/src/module/lib/mainModule.ts
./root/src/module/index.ts
./root/src/module/index.tsx
./root/src/module/index.d.ts
.
const x = require('module')
:
/root/src/node_modules/module.ts
./root/src/node_modules/module.tsx
./root/src/node_modules/module.d.ts
./root/src/node_modules/module/package.json
(if it specifies atypes
property)./root/src/node_modules/@types/module.d.ts
./root/src/node_modules/module/index.ts
./root/src/node_modules/module/index.tsx
./root/src/node_modules/module/index.d.ts
./root/node_modules/module.ts
./root/node_modules/module.tsx
./root/node_modules/module.d.ts
./root/node_modules/module/package.json
(if it specifies atypes
property)./root/node_modules/@types/module.d.ts
./root/node_modules/module/index.ts
./root/node_modules/module/index.tsx
./root/node_modules/module/index.d.ts
./node_modules/module.ts
./node_modules/module.tsx
./node_modules/module.d.ts
./node_modules/module/package.json
(if it specifies atypes
property)./node_modules/@types/module.d.ts
./node_modules/module/index.ts
./node_modules/module/index.tsx
./node_modules/module/index.d.ts
.
Basic Types
boolean
.number
.string
.array
.tuple
:- Fixed number of elements whose types are known.
- Variable length
array
types aren’t assignable totuple
types.
enum
.null
.undefined
.void
.any
.unknown
: 任何类型都能分配给unknown
, 但unknown
不能分配给其他基本类型.never
:switch
default case guard (exhaustiveness check).- Reduce
never
intersection type. - Ignored in union type:
- mapped conditional type.
- distributive conditional type.
let num: number
let str: string
let bool: boolean
let boolArray: boolean[]
let tuple: [string, number]
let power: any
// 赋值任意类型
power = '123'
power = 123
// 它也兼容任何类型
power = num
num = power
function log(message: string): void {
console.log(message)
}
function unknownColor(x: never): never {
throw new Error('unknown color')
}
type Color = 'red' | 'green' | 'blue'
function getColorName(c: Color): string {
switch (c) {
case 'red':
return 'is red'
case 'green':
return 'is green'
default:
return unknownColor(c)
}
}
Enum Types
Number Enum
enum CardSuit {
Clubs = 1,
Diamonds, // 2
Hearts, // 3
Spades, // 4
}
// 简单的使用枚举类型
let Card = CardSuit.Clubs
// 类型安全
Card = 'not a member of card suit' // Error: string 不能赋值给 `CardSuit` 类型
String Enum
enum EvidenceTypeEnum {
UNKNOWN = '',
PASSPORT_VISA = 'passport_visa',
PASSPORT = 'passport',
SIGHTED_STUDENT_CARD = 'sighted_tertiary_edu_id',
SIGHTED_KEYPASS_CARD = 'sighted_keypass_card',
SIGHTED_PROOF_OF_AGE_CARD = 'sighted_proof_of_age_card',
}
Enum Parameters
enum Weekday {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday,
}
namespace Weekday {
export function isBusinessDay(day: Weekday) {
switch (day) {
case Weekday.Saturday:
case Weekday.Sunday:
return false
default:
return true
}
}
}
const mon = Weekday.Monday
const sun = Weekday.Sunday
console.log(Weekday.isBusinessDay(mon)) // true
console.log(Weekday.isBusinessDay(sun))
Enum Flags
enum AnimalFlags {
None = 0,
HasClaws = 1 << 0,
CanFly = 1 << 1,
EatsFish = 1 << 2,
Endangered = 1 << 3,
// eslint-disable-next-line ts/prefer-literal-enum-member
EndangeredFlyingClawedFishEating = HasClaws | CanFly | EatsFish | Endangered,
}
interface Animal {
flags: AnimalFlags
[key: string]: any
}
function printAnimalAbilities(animal: Animal) {
const animalFlags = animal.flags
if (animalFlags & AnimalFlags.HasClaws)
console.log('animal has claws')
if (animalFlags & AnimalFlags.CanFly)
console.log('animal can fly')
if (animalFlags === AnimalFlags.None)
console.log('nothing')
}
const animal = { flags: AnimalFlags.None }
printAnimalAbilities(animal) // nothing
animal.flags |= AnimalFlags.HasClaws
printAnimalAbilities(animal) // animal has claws
animal.flags &= ~AnimalFlags.HasClaws
printAnimalAbilities(animal) // nothing
animal.flags |= AnimalFlags.HasClaws | AnimalFlags.CanFly
printAnimalAbilities(animal) // animal has claws, animal can fly
Enum Index Signature
keyof typeof EnumType
:
enum ColorPalette {
red = '#f03e3e',
pink = '#d7336c',
grape = '#ae3ec9',
violet = '#7048e8',
indigo = '#4263eb',
blue = '#1890ff',
cyan = '#1098ad',
teal = '#0ca678',
green = '#37b24d',
lime = '#74b816',
yellow = '#f59f00',
orange = '#f76707',
}
function hashString(name = '') {
return name.length
}
function getColorByName(name = ''): string {
const palette = Object.keys(ColorPalette)
const colorIdx = hashString(name) % palette.length
const paletteIdx = palette[colorIdx] as keyof typeof ColorPalette
return ColorPalette[paletteIdx]
}
Enum Internals
const
enums don’t have representation at runtime,
its member values are used directly.
// Source code:
const enum NoYes {
No,
Yes,
}
function toGerman(value: NoYes) {
switch (value) {
case NoYes.No:
return 'Neither'
case NoYes.Yes:
return 'Ja'
}
}
// Compiles to:
function toGerman(value) {
switch (value) {
case 'No' /* No */:
return 'Neither'
case 'Yes' /* Yes */:
return 'Ja'
}
}
Non-const enums are objects:
// Source code:
enum Tristate {
False,
True,
Unknown,
}
// Compiles to:
let Tristate
;(function (Tristate) {
Tristate[(Tristate.False = 0)] = 'False'
Tristate[(Tristate.True = 1)] = 'True'
Tristate[(Tristate.Unknown = 2)] = 'Unknown'
})(Tristate || (Tristate = {}))
console.log(Tristate[0]) // 'False'
console.log(Tristate.False) // 0
console.log(Tristate[Tristate.False]) // 'False' because `Tristate.False == 0`
enum NoYes {
No = 'NO!',
Yes = 'YES!',
}
let NoYes
;(function (NoYes) {
NoYes.No = 'NO!'
NoYes.Yes = 'YES!'
})(NoYes || (NoYes = {}))
Function
Function Interface
interface ReturnString {
(): string
}
declare const foo: ReturnString
const bar = foo() // bar 被推断为一个字符串
interface Complex {
(foo: string, bar?: number, ...others: boolean[]): number
}
interface Overloaded {
(foo: string): string
(foo: number): number
}
// 实现接口的一个例子:
function stringOrNumber(foo: number): number
function stringOrNumber(foo: string): string
function stringOrNumber(foo: any): any {
if (typeof foo === 'number')
return foo * foo
else if (typeof foo === 'string')
return `hello ${foo}`
}
const overloaded: Overloaded = stringOrNumber
// 使用
const str = overloaded('') // str 被推断为 'string'
const num = overloaded(123) // num 被推断为 'number'
WangCai extends
Dog extends
Animal.
Animal => WangCai 是 Dog => Dog 的子类型:
- 函数参数的类型兼容是反向的, 称之为逆变.
- 返回值的类型兼容是正向的, 称之为协变.
Arrow Function
在一个以 number 类型为参数,以 string 类型为返回值的函数中:
const simple: (foo: number) => string = foo => foo.toString()
Function Overload
函数签名的类型重载:
- 多个重载签名和一个实现签名.
- 定义了重载签名, 则实现签名对外不可见.
- 实现签名必须兼容重载签名.
// 重载
function padding(all: number)
function padding(topAndBottom: number, leftAndRight: number)
function padding(top: number, right: number, bottom: number, left: number)
function padding(a: number, b?: number, c?: number, d?: number) {
if (b === undefined && c === undefined && d === undefined) {
b = c = d = a
} else if (c === undefined && d === undefined) {
c = a
d = b
}
return {
top: a,
right: b,
bottom: c,
left: d,
}
}
padding(1) // Okay: all
padding(1, 1) // Okay: topAndBottom, leftAndRight
padding(1, 1, 1, 1) // Okay: top, right, bottom, left
padding(1, 1, 1) // Error: Not a part of the available overloads
TypeScript
中的函数重载没有任何运行时开销.
它只允许你记录希望调用函数的方式,
并且编译器会检查其余代码.
Rest Parameters
type Arr = readonly unknown[]
function partialCall<T extends Arr, U extends Arr, R>(
f: (...args: [...T, ...U]) => R,
...headArgs: T
) {
return (...tailArgs: U) => f(...headArgs, ...tailArgs)
}
function foo(x: string, y: number, z: boolean) {}
const f1 = partialCall(foo, 100)
const f2 = partialCall(foo, 'hello', 100, true, 'oops')
const f3 = partialCall(foo, 'hello')
f3(123, true)
f3()
f3(123, 'hello')
Function Types Design
- Input types tend to be broader than output types.
- Optional properties and union types are more common in parameter types.
- To reuse types between parameters and return types, introduce a canonical form (for return types) and a looser form (for parameters).
interface LngLat {
lng: number
lat: number
}
type LngLatLike = LngLat | { lon: number, lat: number } | [number, number]
interface Camera {
center: LngLat
zoom: number
bearing: number
pitch: number
}
interface CameraOptions extends Omit<Partial<Camera>, 'center'> {
center?: LngLatLike
}
function createCamera(options: CameraOptions): Camera {
return CameraFactory.create(options)
}
Interface
interface Name {
first: string
second: string
}
let name: Name
name = {
first: 'John',
second: 'Doe',
}
name = {
// Error: 'Second is missing'
first: 'John',
}
name = {
// Error: 'Second is the wrong type'
first: 'John',
second: 1337,
}
Interface Function
- Use a method function for class instances (
this
binding to function). - Use a property function otherwise.
interface HasBothFunctionTypes {
method: () => string
property: () => string
}
Interface Implementation
Implementing interface is purely safety check, does not copy any interface members onto class definition:
interface Crazy {
new (): {
hello: number
}
}
class CrazyClass implements Crazy {
constructor() {
return { hello: 123 }
}
}
// Because
const crazy = new CrazyClass() // crazy would be { hello:123 }
Interface Extension
Overridden Properties
Overridden property must be assignable to its base property (ensure derived interface assignable to base interface):
interface WithNullableName {
name: string | null
}
interface WithNonNullableName extends WithNullableName {
name: string
}
interface WithNumericName extends WithNullableName {
name: number | string
}
// Error: Interface 'WithNumericName' incorrectly
// extends interface 'WithNullableName'.
// Types of property 'name' are incompatible.
// Type 'string | number' is not assignable to type 'string | null'.
// Type 'number' is not assignable to type 'string'.