網站搜尋

typescript:從介面建立 [key,value 類型] 的元組


我正在嘗試創建一個 TypeScript 元組類型(一對),它只能包含第一個值是某個接口的鍵(該接口以有效操作作為鍵,對應值為預期參數的類型)的值

我想下面的程式碼會更好地說明我想要達成的目標:

interface IEventsAndArguments {
  '+': number,
  '-': number,
  'change_name': string,
}

// Now I want to produce tuples,
// where the first value is in keyof IEventsAndArguments
// and the second value is of the corresponding type
// (but a type with 2 properties name and value would do as well)

// it works for function arguments
function myFunction<K extends keyof IEventsAndArguments>(x:K, y:IEventsAndArguments[K]) {}
myFunction('+', 'a string'); // invalid as expected, '+' needs a number
myFunction('+', 4);
myFunction('change_name', 'John');
function myFunction2<K extends keyof IEventsAndArguments>({x, y}:{x:K, y:IEventsAndArguments[K]}) {}
myFunction2({x: '+', y: 'another string'}); // invalid as expected
myFunction2({x: '-', y: 3});

// it doesn't work when I try to define a tuple type
type TTuple<K extends keyof IEventsAndArguments> = [K, IEventsAndArguments[K]];
// I would like the next tuple to be INVALID
// but it is valid since the second element is of type number | string
const tuple1:TTuple<keyof IEventsAndArguments> = [ '+', 'hello' ]


// What I ultimately would want is something similar to this 
type TTuple2<K extends keyof IEventsAndArguments> = [
  K,
  (v:IEventsAndArguments[K]) => void
];
// I would like the next tuple to be VALID but it is invalid
// since the function argument must be of type number | string
const tuple2:TTuple2<keyof IEventsAndArguments> = [
  '+',
  (t:number) => {},
];

這裡有一個包含相同程式碼的遊樂場連結。

有沒有辦法讓這個運作?有人能解釋為什麼它在函數上表現如我所願,但在定義新類型時卻不行嗎?

謝謝。

編輯:我想我找到了一个“足夠好”的機制:既然它對函數有效,我們可以用函數來產生一個有效的元組!

// if we use a function to produce the tuple, we can produce 'valid'
// tuples, even if the tuple type itself is not as strict as we'd like...
function tuple<K extends keyof IEventsAndArguments>(
    a:K, f:(v:IEventsAndArguments[K]) => void
):TTuple2<K>
{
  return [a, f];
}

const tuple3 = tuple('+', (v:number) => v + 1); // valid, as expected
const tuple4 = tuple('+', (v:string) => v + 1); // invalid, also as expected

但挫折感依然存在:為什麼它對函數有效(參數都『綁定』到介面的同一個條目),但在創建新類型時卻無效?如果我們也能以某種方式將類型設置得盡可能嚴格,那會很好,因為現在你仍然可以通過不使用函數來生成『無效』的元組。

哦:另外很重要的一點:我希望這是通用的,所以無論人們之後在介面中『添加』什麼條目,它都應該能繼續運作,因此應假設 IEventsAndArguments 的定義是未知的,並且可以在其他地方被擴展。

答案#1

你希望你的 TTuple 類型要麼是形式為 ["+", (v: number) => void] 的元組,要麼是形式為 ["-", (v: number) => void] 的元組,要麼是形式為 ["change_name", (v: string) => void] 的元組。你可以不用泛型,只需將 TTuple 定義為這些類型的聯合即可達到這個目的:

type TTuple = 
  ["+", (v: number) => void] | 
  ["-", (v: number) => void] | 
  ["change_name", (v: string) => void];

你可以確認這個有效:

let tuple: TTuple;
tuple = ['+', (t: number) => console.log(t.toFixed())]; // okay
tuple = ['change_name', (t: number) => console.log(t.toFixed())]; // error
tuple = ['change_name', (t: string) => console.log(t.toUpperCase())]; // okay

當然,你不想手動定義TTuple;最好讓編譯器將TTuple作為IEventsAndArguments的函數來計算。

其中一種方法是創建所謂的「分配物件類型」(術語來自 microsoft/TypeScript#47109),你首先對 IEventsAndArguments 類型進行映射,以形成具有相同鍵的新物件類型,但其中屬性值類型是對應的元組類型,然後你立即使用 IEventsAndArguments 鍵的聯合類型來索引這個映射類型,這會產生元組的聯合類型。看起來如下:

type TTuple = { [K in keyof IEventsAndArguments]: [
    K,
    (v: IEventsAndArguments[K]) => void
] }[keyof IEventsAndArguments];

通過那個,映射類型 {[K in keyof]... } 會遍歷 "+"、"-" 和 "change_name" 鍵以生成類似的東西

{ 
   "+": ["+", (v: IEventsAndArguments["+"] )=> void],
   "-": ["-", (v: IEventsAndArguments["-"] )=> void],
   "change_name": ["change_name", (v: IEventsAndArguments["change_name"] )=> void]
}

化簡為

{
   "+": ["+", (v: number) => void],
   "-": ["-", (v: number) => void],
   "change_name": ["change_name", (v: string) => void]
}

然後我們用 "+" | "-" | "change_name" 來索引它,這會產生所需的聯合型 ["+", (v: number) => void] | ["-", (v: number) => void] | ["change_name", (v: string) => void]。

現在,如果你在 IEventsAndArguments 類型上添加或更改屬性,TTuple 類型將會自動做出相應的更改以適應它。

程式碼的 Playground 連結