網站搜尋

如何向 TypeScript 解釋泛型類中的變量總是字串


我有以下的類別和介面定義:

import { Component } from 'react';

type KeysMatching<T, V> = {
    [K in keyof T]-?: T[K] extends V ? K : never
}[keyof T];

interface SearchListProps<T extends object, K extends KeysMatching<T, string>> {
    data: T[];
    sort_and_filter_key: K;
}

class SearchList<ItemDataT extends object, K extends KeysMatching<ItemDataT, string>> extends Component<SearchListProps<ItemDataT, K>> {

    constructor(props: SearchListProps<ItemDataT, K>) {
        super(props);

        const value = props.data[0][props.sort_and_filter_key]; // ItemDataT[K] -> should be string
    }
}

// exmple case: 

interface DataExample {
    name: string, 
    city: string,
    id: number,
    content: object
    // etc
}

const example_data: DataExample[] = [
    {
        name: "asd",
        city: "bcn",
        id: 1,
        content: {}
    },
    {
        name: "dkg",
        city: "nld",
        id: 2,
        content: {}
    }
];

<SearchList data={example_data} sort_and_filter_key={"city"}></SearchList>

如你在程式碼中看到的註解,TypeScript 認為 value 的類型是 ItemDataT[K],但它應該是 string。

為什麼?因為屬性 sort_and_filter_key 應該只允許值為字串的鍵,這是根據以下泛型表達式:K extends KeysMatching<T, string>。但是 TypeScript 似乎不理解這一點。

自動完成功能似乎能理解,請見下圖:

由於這張圖清楚地只允許兩個保證為字串的鍵。

我該如何讓 tsc 也理解這個?
如果有任何不清楚的地方,請告訴我,這樣我可以修改問題。

答案#1

看起來這個問題是 Typescript 在泛型類型推斷能力上的限制——Typescript 不會追蹤所有可推斷的約束條件。確實這樣做是有一些取捨的。

對於具體類型,推斷如預期般運作:

const props: SearchListProps<DataExample> = {
  data: example_data,
  sort_and_filter_key: 'city', 
};

const ok_string = props.data[0][props.sort_and_filter_key];

在遇到泛型類型的情況下,我們需要手動給 TypeScript 引擎提示我們究竟感興趣的是什麼:

interface SearchListProps<T> {
  data: (T &  Record<KeysMatching<T, string>, string>)[];
  sort_and_filter_key: KeysMatching<T, string>;
}

有趣的是,我們可以將 T 與其他任何東西做交集——例如 Record<KeysMatching<T, string>, number>,但這樣一來,我們就無法用具體類型初始化 SearchListProps。

一個固定和簡化的版本:https://stackblitz.com/edit/react-ts-x4jdez

順便提一下,props.data[0][props.sort_and_filter_key] 不安全(noUncheckedIndexedAccess)。

答案#2

我們也可以給 ItemDataT 添加約束 Record<K, string>,其中 K 只是被約束為 string。

class SearchList<
  ItemDataT extends Record<K, string>, 
  K extends string
> extends Component<SearchListProps<ItemDataT, K>> {
    constructor(props: {
        data: ItemDataT[],
        sort_and_filter_key: K
    }) {
        /* ... */
    }
}

當我們現在存取 props.data[0][props.sort_and_filter_key] 時,TypeScript 仍然將類型顯示為 ItemDataT[K]。但是 TypeScript 其實知道這次這一定是 string。所有字串方法都可以使用。類型只是顯示為 「錯誤」,但行為是正確的。

const value = props.data[0][props.sort_and_filter_key]; 

value.charAt(0)
// this was compile time error before

遊樂場