const 斷言 & infer 關鍵字


Posted by TempuraEngineer on 2023-11-17

目錄


const斷言是什麼

(2024/4/30更新)

const斷言(assertion)是一種型別斷言,它可以告訴Typescript限縮變數的型別、把變數變成readonly,寫immutable code時會用到

使用as const會把型別變成literal type

literal
adjective
The literal meaning of a word is its original, basic meaning.

literal type是一種鎖死的型別,簡單說就是你詳細定義「這個字串(數值)是什麼」、「這個陣列內的元素是什麼」、「這個物件內有什麼屬性、屬性值是什麼」

const斷言和const宣告的差別在於並不是用於宣告變數,或者限縮型別,兩者使用的場景並不完全一樣

 // 型別是string;
let fruit = 'apple';

 // 型別是apple;
let fruit2 = 'apple' as const;
const fruit3 = 'apple';
let arr = [10, 20]; // number[]
const  arr2 = [10, 20] as const; // readonly [10, 20]
const  arr3 = arr2; // readonly [10, 20]

arr.push(30);

// Property 'push' does not exist on type 'readonly [10, 20]'
arr2.push(30);

// Property 'push' does not exist on type 'readonly [10, 20]'
// 傳址,且其reference已經被as const鎖死型別,所以不能push
arr3.push(30);
const obj = {
    a:1,
};
// {
//     a: number;
// }

// 使用了as const以後物件的型別完全被鎖死,所有屬性變readonly(immutable)
const obj2 = {
    a:1,
} as const;
// {
//     readonly a: 1;
// }

// Property 'b' does not exist on type '{ a: number; }'.
obj.b = 2;
// 型別沒有被完全鎖死,所以可以修改屬性值
obj.a = 20;

Property 'b' does not exist on type '{ readonly a: 1; }'
obj2.b = 2;
Cannot assign to 'a' because it is a read-only property.
obj2.a = 20;

在一些需要enum的場景也可以使用const斷言代替

const mainDishes = {
    beef:'steak',
    pork:'pepper pork',
    lamb:'roasted lamb',
    fish:'sashimi',
    shrimp:'curry shirmp',
    carb:'butter crab',
} as const;
// {
//     readonly beef: "steak";
//     readonly pork: "pepper pork";
//     readonly lamb: "roasted lamb";
//     readonly fish: "sashimi";
//     readonly shrimp: "curry shirmp";
//     readonly carb: "butter crab";
// }

// Cannot assign to 'beef' because it is a read-only property.
mainDishes.beef = 'roasted beef';


const斷言限制

1.只能用於直接賦值宣告、簡單的表達式

```js
// bad
// A 'const' assertion can only be applied to a to a string, number, boolean, array, or object literal.
const num = 1 + 2 as const;
const day = new Date('2023-11-13') as const;
const set = new Set([1,2,3]) as const;
const maxNum = Math.max(...[10,20,30]) as const;

// ok
const num = 1 as const;
const weekends = ['Sunday', 'Saturday'] as const;
```

2.如果物件的屬性有reference,那const斷言不會對那個屬性生效

```js
let arr = ['chocolate']; // string[]
let arr2 = ['coffe'] as const; // let arr2 = ['coffe'] as const;

let person = {
  name: "Derek",
  favorite: arr,
  hate: arr2,
  hobby: ['gardening'],

} as const;
// {
//     readonly name: "Derek";
//     readonly favorite: string[];
//     readonly hobby: readonly ["gardening"];
// }

// Property 'push' does not exist on type 'readonly ["gardening"]'.
person.hobby.push('swimming');

// 可以push的原因是arr並沒有被用as const鎖死型別,所以可以修改
person.favorite.push('strawberry');

// Cannot assign to 'favorite' because it is a read-only property.
// 不可以重新將favorite屬性賦值,因為person被用as const鎖死型別
person.favorite = ['apple'];

// Property 'push' does not exist on type 'readonly ["coffe"]'.
// 不可以push是因為arr2被用as const鎖死型別
person.hate.push('milk');
```


infer關鍵字是什麼

使用infer關鍵字(keyword)可以讓TS自動從推斷型別參數推斷出一個型別,並傳給變數,可以在限縮型別的同時讓型別變得更彈性

用於條件型別(conditional type),能解決深度巢狀的條件型別(a? b : c? d : e? f : g)

enum Desserts {
    Cake = 'chocolate cake',
    Jelly = 'organge jelly',
    IceCream = 'macha ice cream'
}

// 攤平陣列取得element的型別
type Flatten<Type> = Type extends (infer Item)[] ? Item : Type;

// Desserts[]是型別參數
// Type被extends限制必須要是陣列,如果Type是陣列,TS會從它推斷出其中的元素的型別,即Desserts,並將Desserts賦予給Item變數
type Sweets = Flatten<Desserts[]>; // Desserts

用於推斷return value型別、parameter型別

type GetReturnType<Type> = Type extends (...args: never[]) => infer Return ? Return : never;

type Num = GetReturnType<() => number>;

// 相當於
type Num2 = ReturnType<() => number>;
type GetParameterType<Type> = Type extends (arg: infer Args) => any ? Args : Type;

type Args = GetParameterType<(a:number, b: string) => void>

// 相當於
type Params = Parameters<(a:number, b: string) => void>;

ComponentProps的實作

React也提供了util type,只要傳component給它就可以取得其props型別

import {ComponentProps} from 'react';

type PropsOfMyButton = ComponentProps<typeof MyButton>; // MyButtonProps

type PropsOfButton = ComponentProps<"button">; // ClassAttributes<HTMLButtonElement> & ButtonHTMLAttributes<HTMLButtonElement>

type Try = ComponentProps<123>; // {}
// 原始碼
type ComponentProps<
  T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>
> = T extends JSXElementConstructor<infer P> ? P : 
      T extends keyof JSX.IntrinsicElements ? JSX.IntrinsicElements[T] : {};

JSX.IntrinsicElements是在 JSX 中可以使用的HTML 元素 (ex: button、span)
JSXElementConstructor則是在 JSX 中可以使用的 React 組件 (ex: function component、class component)

以上的程式碼的意思是,如果傳入的型別參數是組件,那就回傳其props。如果不是的話,檢查是否為HTML元素,是的話就回傳該HTML元素的所有屬性 (ex: style、type、name、value...),如果不是的話回傳{}


infer關鍵字限制

不是conditional type時,infer不能寫在extend後(型別參數的限制子句)

// bad
// 'infer' declarations are only permitted in the 'extends' clause of a conditional type.
type Item<T extends (infer U)[]> = U;

參考資料

TS - const assertions
TS - Inferring Within Conditional Types
TS - Literal Types
TS - readonly Tuple Types


#TypeScript #const assertion #infer keyword #conditional type







Related Posts

Day 136

Day 136

Web開發學習筆記10 — 預設參數、Spread Operator、Rest Operator、解構賦值、陣列方法的練習筆記

Web開發學習筆記10 — 預設參數、Spread Operator、Rest Operator、解構賦值、陣列方法的練習筆記

[Vue 學習筆記(三)] vue iteration

[Vue 學習筆記(三)] vue iteration


Comments