數棧是—站式大數據開發平臺,咱們在github和gitee上有一個有趣的開源項目:FlinkX,FlinkX是一個基於Flink的批流統一的數據同步工具,既能夠採集靜態的數據,也能夠採集實時變化的數據,是全域、異構、批流一體的數據同步引擎。你們喜歡的話請給咱們點個star!star!star!
github開源項目:https://github.com/DTStack/flinkx
gitee開源項目:https://gitee.com/dtstack_dev_0/flinkx
有興趣的話,歡迎你們加入咱們的交流社羣:30537511(釘釘羣)
javascript
寫在前面
本文難度偏中下,涉及到的點大多爲如何在項目中合理應用TS,小部分會涉及一些原理,受衆面較廣,有無TS基礎都可放心食用
閱讀完本文,您可能會收穫到:
一、若您還不熟悉 TS,那本文可幫助您完成 TS 應用部分的學習,伴隨衆多 Demo 例來引導業務應用。
二、若您比較熟悉 TS,那本文可看成複習文,帶您回顧知識,但願能在某些點引起您新發現和思考。
三、針對於 class 組件的 IState 和 IProps,類比 Hook 組件的部分寫法和思考。
TIPS:超好用的在線 TS 編輯器(諸多配置項可手動配置)
傳送門:https://www.typescriptlang.org/html
不扯晦澀的概念,通俗來講 TypeScript 就是 JavaScript 的超集,它具備可選的類型,並能夠編譯爲純 JavaScript 運行。(筆者一直就把 TypeScript 看做 JavaScript 的 Lint)
那麼問題來了,爲何 TS 必定要設計成靜態的?或者換句話說,咱們爲何須要向 JavaScript 添加類型規範呢 ?
經典自問自答環節——由於它能夠解決一些 JS 還沒有解決的痛點:一、JS 是動態類型的語言,這也意味着在實例化以前咱們都不知道變量的類型,可是使用 TS 能夠在運行前就避免經典低級錯誤。
例:Uncaught TypeError:'xxx' is not a function⚠️ 典中典級別的錯誤 :
JS 就是這樣,只有在運行時發生了錯誤才告訴我有錯,可是當 TS 介入後:
好傢伙!直接把問題在編輯器階段拋出,nice!
二、懶人狂歡! 規範方便,又不容易出錯,對於 VS Code,它能作的最多隻是標示出有沒有這個屬性,但並不能精確的代表這個屬性是什麼類型,但 TS 能夠經過類型推導/反推導(說白話:若是您未明確編寫類型,則將使用類型推斷來推斷您正在使用的類型),從而完美優化了代碼補全這一項:
1)第一個 Q&A——思考 :提問:那麼咱們還能想到在業務開發中 TS 解決了哪些 JS 的痛點呢?回答,總結,補充:
對函數參數的類型限制;
對數組和對象的類型限制,避免定義出錯 例如數據解構複雜或較多時,可能會出現數組定義錯誤 a = { }, if (a.length){ // xxxxx }
let functionA = 'jiawen' // 實際上 let functionA: string = 'jiawen'
三、使咱們的應用代碼更易閱讀和維護,若是定義完善,能夠經過類型大體明白參數的做用。相信經過上述簡單的bug-demo,各位已對TS有了一個初步的從新認識 接下來的章節便正式介紹咱們在業務開發過程當中如何用好TS。java
在業務中如何用TS/如何用好TS?這個問題其實和 " 在業務中怎麼用好一個API " 是同樣的。首先要知道這個東西在幹嗎,參數是什麼,規則是什麼,可以接受有哪些擴展......等等。簡而言之,擼它!哪些擴展......等等。 簡而言之,擼它!
一、TS 經常使用類型概括
經過對業務中常見的 TS 錯誤作出的一個綜合性總結概括,但願 Demos 會對您有收穫
1)元語(primitives)之 string number boolean
筆者把基本類型拆開的緣由是: 無論是中文仍是英文文檔,primitives/元語/元組 這幾個名詞都頻繁出鏡,筆者理解的白話:但願在類型約束定義時,使用的是字面量而不是內置對象類型,官方文檔:
let a: string = 'jiawen';let flag: boolean = false;let num: number = 150interface IState: { flag: boolean; name: string; num: number;}
2)元組
// 元組類型表示已知元素數量和類型的數組,各元素的類型沒必要相同,可是對應位置的類型須要相同。
let x: [string, number];
x = ['jiawen', 18]; // ok
x = [18, 'jiawen']; // Erro
console.log(x[0]); // jiawen
3)undefined null
let special: string = undefined
// 值得一提的是 undefined/null 是全部基本類型的子類,
// 因此它們能夠任意賦值給其餘已定義的類型,這也是爲何上述代碼不報錯的緣由
4)object 和 { }
// object 表示的是常規的 Javascript對象類型,非基礎數據類型
const offDuty = (value: object) => {
console.log("value is ", value);
}
offDuty({ prop: 0}) // ok
offDuty(null) offDuty(undefined) // Error
offDuty(18) offDuty('offDuty') offDuty(false) // Error
// {} 表示的是 非null / 非undefined 的任意類型
const offDuty = (value: {}) => {
console.log("value is ", value);
}
offDuty({ prop: 0}) // ok
offDuty(null) offDuty(undefined) // Error
offDuty(18) offDuty('offDuty') offDuty(false) // ok
offDuty({ toString(){ return 333 } }) // ok
// {} 和Object幾乎一致,區別是Object會對Object內置的 toString/hasOwnPreperty 進行校驗
const offDuty = (value: Object) => {
console.log("value is ", value);
}
offDuty({ prop: 0}) // ok
offDuty(null) offDuty(undefined) // Error
offDuty(18) offDuty('offDuty') offDuty(false) // ok
offDuty({ toString(){ return 333 } }) // Error
若是須要一個對象類型,但對屬性沒有要求,建議使用 object
{} 和 Object 表示的範圍太大,建議儘可能不要使用
5)object of params
// 咱們一般在業務中可多采用點狀對象函數(規定參數對象類型)
const offDuty = (value: { x: number; y: string }) => {
console.log("x is ", value.x);
console.log("y is ", value.y);
}
// 業務中必定會涉及到"可選屬性";先簡單介紹下方便快捷的「可選屬性」
const offDuty = (value: { x: number; y?: string }) => {
console.log("必選屬性x ", value.x);
console.log("可選屬性y ", value.y);
console.log("可選屬性y的方法 ", value.y.toLocaleLowerCase());
}
offDuty({ x: 123, y: 'jiawen' })
offDuty({ x: 123 })
// 提問:上述代碼有問題嗎?
答案:
// offDuty({ x: 123 }) 會致使結果報錯value.y.toLocaleLowerCase()
// Cannot read property 'toLocaleLowerCase' of undefined
方案1: 手動類型檢查
const offDuty = (value: { x: number; y?: string }) => {
if (value.y !== undefined) {
console.log("可能不存在的 ", value.y.toUpperCase());
}
}
方案2:使用可選屬性 (推薦)
const offDuty = (value: { x: number; y?: string }) => {
console.log("可能不存在的 ", value.y?.toLocaleLowerCase());
}
6)unknown 與 any
// unknown 能夠表示任意類型,但它同時也告訴TS, 開發者對類型也是沒法肯定,作任何操做時須要慎重
let Jiaven: unknown
Jiaven.toFixed(1) // Error
if (typeof Jiaven=== 'number') {
Jiaven.toFixed(1) // OK
}
當咱們使用any類型的時候,any會逃離類型檢查,而且any類型的變量能夠執行任意操做,編譯時不會報錯
anyscript === javascript
注意:any 會增長了運行時出錯的風險,不到萬不得已不要使用;
若是遇到想要表示【不知道什麼類型】的場景,推薦優先考慮 unknown
7)union 聯合類型
union也叫聯合類型,由兩個或多個其餘類型組成,表示可能爲任何一個的值,類型之間用 ' | '隔開
type dayOff = string | number | boolean
聯合類型的隱式推導可能會致使錯誤,遇到相關問題請參考語雀 code and tips —— 《TS的隱式推導》
.值得注意的是,若是訪問不共有的屬性的時候,會報錯,訪問共有屬性時不會.上個最直觀的demo
function dayOff (value: string | number): number {
return value.length;
}
// number並不具有length,會報錯,解決方法:typeof value === 'string'
function dayOff (value: string | number): number {
return value.toString();
}
// number和string都具有toString(),不會報錯
8)never
// never是其它類型(包括 null 和 undefined)的子類型,表明從不會出現的值。
// 那never在實際開發中到底有什麼做用?這裏筆者原汁原味照搬尤雨溪的經典解釋來作第一個例子
第一個例子,當你有一個 union type:
interface Foo {
type: 'foo'
}
interface Bar {
type: 'bar'
}
type All = Foo | Bar
在 switch 當中判斷 type,TS是能夠收窄類型的 (discriminated union):
function handleValue(val: All) {
switch (val.type) {
case 'foo':
// 這裏 val 被收窄爲 Foo
break
case 'bar':
// val 在這裏是 Bar
break
default:
// val 在這裏是 never
const exhaustiveCheck: never = val
break
}
}
注意在 default 裏面咱們把被收窄爲 never 的 val 賦值給一個顯式聲明爲 never 的變量。
若是一切邏輯正確,那麼這裏應該可以編譯經過。可是假如後來有一天你的同事改了 All 的類型:
type All = Foo | Bar | Baz
然而他忘記了在 handleValue 裏面加上針對 Baz 的處理邏輯,
這個時候在 default branch 裏面 val 會被收窄爲 Baz,致使沒法賦值給 never,產生一個編譯錯誤。
因此經過這個辦法,你能夠確保 handleValue 老是窮盡 (exhaust) 了全部 All 的可能類型
第二個用法 返回值爲 never 的函數能夠是拋出異常的狀況
function error(message: string): never {
throw new Error(message);
}
第三個用法 返回值爲 never 的函數能夠是沒法被執行到的終止點的狀況
function loop(): never {
while (true) {}
}
9)Void
interface IProps {
onOK: () => void
}
void 和 undefined 功能高度相似,但void表示對函數的返回值並不在乎或該方法並沒有返回值
10)enum
筆者認爲ts中的enum是一個頗有趣的枚舉類型,它的底層就是number的實現
1.普通枚舉
enum Color {
Red,
Green,
Blue
};
let c: Color = Color.Blue;
console.log(c); // 2
2.字符串枚舉
enum Color {
Red = 'red',
Green = 'not red',
};
3.異構枚舉 / 有時也叫混合枚舉
enum Color {
Red = 'red',
Num = 2,
};
<第一個坑>
enum Color {
A, // 0
B, // 1
C = 20, // 20
D, // 21
E = 100, // 100
F, // 101
}
若初始化有部分賦值,那麼後續成員的值爲上一個成員的值加1
<第二個坑> 這個坑是第一個坑的延展,稍不仔細就會上當!
const getValue = () => {
return 23
}
enum List {
A = getValue(),
B = 24, // 此處必需要初始化值,否則編譯不經過
C
}
console.log(List.A) // 23
console.log(List.B) // 24
console.log(List.C) // 25
若是某個屬性的值是計算出來的,那麼它後面一位的成員必需要初始化值。
不然將會 Enum member must have initializer.
11)泛型
筆者理解的泛型很白話:先不指定具體類型,經過傳入的參數類型來獲得具體類型 咱們從下述的 filter-demo 入手,探索一下爲何必定須要泛型
泛型的基礎樣式
function fun<T>(args: T): T {
return args
}
若是沒接觸過,是否是會以爲有點懵?不要緊!咱們直接從業務角度深刻。
1.剛開始的需求:過濾數字類型的數組
declare function filter(
array: number[],
fn: (item: unknown) => boolean
) : number[];
2.產品改了需求:還要過濾一些字符串 string[]
彳亍,那就利用函數的重載, 加一個聲明, 雖然笨了點,可是很好理解
declare function filter(
array: string[],
fn: (item: unknown) => boolean
): string[];
declare function filter(
array: number[],
fn: (item: unknown) => boolean
): number[];
3.產品又來了! 此次還要過濾 boolean[]、object[] ..........
這個時候若是仍是選擇重載,將會大大提高工做量,代碼也會變得愈來愈累贅,這個時候泛型就出場了,
它從實現上來講更像是一種方法,經過你的傳參來定義類型,改造以下:
declare function filter<T>(
array: T[],
fn: (item: unknown) => boolean
): T[];
當咱們把泛型理解爲一種方法實現後,那麼咱們便很天然的聯想到:方法有多個參數、默認值,泛型也能夠。
type Foo<T, U = string> = { // 多參數、默認值
foo: Array<T> // 能夠傳遞
bar: U
}
type A = Foo<number> // type A = { foo: number[]; bar: string; }
type B = Foo<number, number> // type B = { foo: number[]; bar: number; }
既然是「函數」,那也會有「限制」,下文列舉一些稍微常見的約束。
1. extends: 限制 T 必須至少是一個 XXX 的類型
type dayOff<T extends HTMLElement = HTMLElement> = {
where: T,
name: string
}
2. Readonly<T>: 構造一個全部屬性爲readonly,這意味着沒法從新分配所構造類型的屬性。
interface Eat {
food: string;
}
const todo: Readonly<Eat> = {
food: "meat beef milk",
};
todo.food = "no food"; // Cannot assign to 'title' because it is a read-only property.
3. Pick<T,K>: 從T中挑選出一些K屬性
interface Todo {
name: string;
job: string;
work: boolean;
type TodoPreview = Pick<Todo, "name" | "work">;
const todo: TodoPreview = {
name: "jiawen",
work: true,
};
todo;
4. Omit<T, K>: 結合了 T 和 K 並忽略對象類型中 K 來構造類型。
interface Todo {
name: string;
job: string;
work: boolean;
}
type TodoPreview = Omit<Todo, "work">;
const todo: TodoPreview = {
name: "jiawen",
job: 'job',
};
5.Record: 約束 定義鍵類型爲 Keys、值類型爲 Values 的對象類型。
enum Num {
A = 10001,
B = 10002,
C = 10003
}
const NumMap: Record<Num, string> = {
[Num.A]: 'this is A',
[Num.B]: 'this is B'
}
// 類型 "{ 10001: string; 10002: string; }" 中缺乏屬性 "10003",
// 但類型 "Record<ErrorCodes, string>" 中須要該屬性,因此咱們還能夠經過Record來作全面性檢查
keyof 關鍵字能夠用來獲取一個對象類型的全部 key 類型
type User = {
id: string;
name: string;
};
type UserKeys = keyof User; // "id" | "name"
改造以下
type Record<K extends keyof any, T> = {
[P in K]: T;
};
此時的 T 爲 any;
還有一些不經常使用,可是很易懂的:
6. Extract<T, U> 從T,U中提取相同的類型
7. Partial<T> 全部屬性可選
type User = {
id?: string,
gender: 'male' | 'female'
}
type PartialUser = Partial<User> // { id?: string, gender?: 'male' | 'female'}
type Partial<T> = { [U in keyof T]?: T[U] }
8. Required<T> 全部屬性必須 << === >> 與Partial相反
type User = {
id?: string,
sex: 'male' | 'female'
}
type RequiredUser = Required<User> // { readonly id: string, readonly gender: 'male' | 'female'}
function showUserProfile (user: RequiredUser) {
console.log(user.id) // 這時候就不須要再加?了
console.log(user.sex)
}
type Required<T> = { [U in keyof T]-?: T[U] }; -? : 表明去掉?
TS的一些須知
一、TS 的 type 和 interface
1)interface(接口) 只能聲明對象類型,支持聲明合併(可擴展)。
interface User { id: string}interface User { name: string}const user = {} as Userconsole.log(user.id);console.log(user.name);
2)type(類型別名)不支持聲明合併 -- l類型
type User = {
id: string,
}
if (true) {
type User = {
name: string,
}
const user = {} as User;
console.log(user.name);
console.log(user.id) // 類型「User」上不存在屬性「id」。
}
3)type 和 interface 異同點總結:
a、一般來說 type 更爲通用,右側能夠是任意類型,包括表達式運算,以及映射等;
b、凡是可用 interface 來定義的,type 也可;
c、擴展方式也不一樣,interface 能夠用 extends 關鍵字進行擴展,或用來 implements 實現某個接口;
d、均可以用來描述一個對象或者函數;
e、type 能夠聲明基本類型別名、聯合類型、元組類型,interface 不行;
f、⚠️ 但若是你是在開發一個包,模塊,容許別人進行擴展就用 interface,若是須要定義基礎數據類型或者須要類型運算,使用 type;
g、interface 能夠被屢次定義,並會被視做合併聲明,而 type 不支持;
h、導出方式不一樣,interface 支持同時聲明並默認導出,而 typetype 必須先聲明後導出;r/>
二、TS 的腳本模式和模塊模式
Typescript 存在兩種模式,區分的邏輯是,文件內容包不包含 import 或者 export 關鍵字 。
1)腳本模式(Script), 一個文件對應一個 html 的 script 標籤 。
2)模塊模式(Module),一個文件對應一個 Typescript 的模塊。
腳本模式下,全部變量定義,類型聲明都是全局的,多個文件定義同一個變量會報錯,同名 interface 會進行合併;而模塊模式下,全部變量定義,類型聲明都是模塊內有效的。
兩種模式在編寫類型聲明時也有區別,例如腳本模式下直接 declare var GlobalStore 便可爲全局對象編寫聲明。
例子:
腳本模式下直接 declare var GlobalStore 便可爲全局對象編寫聲明。
GlobalStore.foo = "foo";
GlobalStore.bar = "bar"; // Error
declare var GlobalStore: {
foo: string;
};
模塊模式下,要爲全局對象編寫聲明須要 declare global
GlobalStore.foo = "foo";
GlobalStore.bar = "bar";
declare global {
var GlobalStore: {
foo: string;
bar: string;
};
}
export {}; // export 關鍵字改變文件的模式
三、TS 的索引簽名
索引簽名能夠用來定義對象內的屬性、值的類型,例如定義一個 React 組件,容許 Props 能夠傳任意 key 爲 string,value 爲 number 的 props
interface Props {
[key: string]: number
}
<Component count={1} /> // OK
<Component count={true} /> // Error
<Component count={'1'} /> // Error
四、TS 的類型鍵入
Typescript 容許像對象取屬性值同樣使用類型
type User = {
userId: string
friendList: {
fristName: string
lastName: string
}[]
}
type UserIdType = User['userId'] // string
type FriendList = User['friendList'] // { fristName: string; lastName: string; }[]
type Friend = FriendList[number] // { fristName: string; lastName: string; }
在上面的例子中,咱們利用類型鍵入的功能從 User 類型中計算出了其餘的幾種類型。FriendList[number]這裏的 number 是關鍵字,用來取數組子項的類型。在元組中也可使用字面量數字獲得數組元素的類型。
type group = [number, string]
type First = group[0] // number
type Second = group[1] // string
五、TS 的斷言
1)類型斷言不是類型轉換,斷言成一個聯合類型中不存在的類型是不容許的。
function getLength(value: string | number): number {
if (value.length) {
return value.length;
} else {
return value.toString().length;
}
// 這個問題在object of parmas已經說起,再也不贅述
修改後:
if ((<string>value).length) {
return (<string>value).length;
} else {
return something.toString().length;
}
}
斷言的兩種寫法
1. <類型>值: <string>value
2. 或者 value as string
特別注意!!!斷言成一個聯合類型中不存在的類型是不容許的
function toBoolean(something: string | number): boolean {
return <boolean>something;
}
2)非空斷言符
TypeScript 還具備一種特殊的語法,用於從類型中刪除 null 和 undefined 不進行任何顯式檢查。
在任何表達式以後寫入其實是一個類型斷言,代表該值不是 null 或 undefined
function liveDangerously(x?: number | undefined | null) {
// 推薦寫法
console.log(x!.toFixed());
}git
一、usestate
useState 若是初始值不是 null/undefined 的話,是具有類型推導能力的,根據傳入的初始值推斷出類型;初始值是 null/undefined 的話則須要傳遞類型定義才能進行約束。通常狀況下,仍是推薦傳入類型(經過 useState 的第一個泛型參數)。
// 這裏ts能夠推斷 value的類型而且能對setValue函數調用進行約束
const [value, setValue] = useState(0);
interface MyObject {
name: string;
age?: number;
}
// 這裏須要傳遞MyObject才能約束 value, setValue
// 因此咱們通常狀況下推薦傳入類型
const [value, setValue] = useState<MyObject>(null);
2)useEffect useLayoutEffect
沒有返回值,無需類型傳遞和約束
3)useMemo useCallback
useMemo無需傳遞類型, 根據函數的返回值就能推斷出類型。
useCallback無需傳遞類型,根據函數的返回值就能推斷出類型。
可是注意函數的入參須要定義類型,否則將會推斷爲any!
const value = 10;
const result = useMemo(() => value * 2, [value]); // 推斷出result是number類型
const multiplier = 2;
// 推斷出 (value: number) => number
// 注意函數入參value須要定義類型
const multiply = useCallback((value: number) => value * multiplier, [multiplier]);
4)useRef
useRef傳非空初始值的時候能夠推斷類型,一樣也能夠經過傳入第一個泛型參數來定義類型,約束ref.current的類型。
1. 若是傳值爲null
const MyInput = () => {
const inputRef = useRef<HTMLInputElement>(null); // 這裏約束inputRef是一個html元素
return <input ref={inputRef} />
}
2. 若是不爲null
const myNumberRef = useRef(0); // 自動推斷出 myNumberRef.current 是number類型
myNumberRef.current += 1;
5)useContext
useContext通常根據傳入的Context的值就能夠推斷出返回值。通常無需顯示傳遞類型。
type Theme = 'light' | 'dark';// 咱們在createContext就傳了類型了const ThemeContext = createContext<Theme>('dark');const App = () => ( <ThemeContext.Provider value="dark"> <MyComponent /> </ThemeContext.Provider>)const MyComponent = () => { // useContext根據ThemeContext推斷出類型,這裏不須要顯示傳 const theme = useContext(ThemeContext); return <div>The theme is {theme}</div>github
在本文中筆者對TS的基礎應用和Hook中的TS作了一些思考,但關於關於TSC如何把TS代碼轉換爲JS代碼的內容,這個部分比較冗長,後續能夠單獨出一篇文章(2)來專門探索。關於TS泛型的底層實現,這個部分比較複雜,筆者還需沉澱,歡迎各位直接留言或在文章中補充!!!
typescript