Batasi satu parameter generik Typecript berdasarkan properti yang lain?

Oct 20 2020

Saya mencoba menulis fungsi yang mengambil objek dan kunci (string), lalu beroperasi pada properti objek. Ini mudah:

function f<T extends any, K extends keyof T>(obj: T, key: K) {
  const prop = obj[key]; // prop is typed as T[K]
}

Saya ingin membatasi kunci yang diteruskan ke panggilan tersebut , pada waktu kompilasi, berdasarkan jenis T[K]. Saya mencoba ini:

function f<T extends any, K extends keyof T>(obj: T, key: T[K] extends number ? K : never) {
  obj[key] = 5; // error, "is not assignable to" etc
}

propdiketik sebagai T[T[K] extends number ? K : never]yang berbunyi bagi saya seperti seharusnya runtuh menjadi adil number, tetapi tidak.

Tujuan saya adalah untuk memastikan bahwa obj[key]diketik sebagai number, di dalam fungsi, dan juga memiliki panggilan seperti yang f({a: true}, "a")ditandai sebagai kesalahan. Apakah ini mungkin? Saya pikir saya mungkin perlu memindahkan batasan dari deklarasi parameter fungsi, ke deklarasi parameter generik, tetapi saya tidak dapat memahami sintaksnya.


ETA lagi: Contoh Playground - diperbarui untuk mencoba pendekatan yang disarankan oleh @reactgular dalam komentar:

type AssignableKeys<T, ValueType> = {
  [Key in keyof T]-?: ValueType extends T[Key] | undefined ? Key : never
}[keyof T];

type PickAssignable<T, ValueType> = Pick<T, AssignableKeys<T, ValueType>>;

type OnlyAssignable<T, ValueType> = {
  [Key in AssignableKeys<T, ValueType>]: ValueType
};

interface Foo {
  a: number;
  b: string;
  nine: 9;
  whatevs: any;
}

type FooNumberKeys = AssignableKeys<Foo, number>; // "a" | "whatevs"
type CanAssignNumber = PickAssignable<Foo, number>; //  { a: number; whatevs: any; }
type DefinitelyJustNumbers = OnlyAssignable<Foo, number>; //  { a: number; whatevs: number; }

function f1<T>(obj: OnlyAssignable<T, number>, key: keyof OnlyAssignable<T, number>) {
  obj[key] = Math.random(); // Assignment is typed correctly, good
}

function f2<T extends object, K extends keyof PickAssignable<T, number>>(obj: T, key: K) {
  obj[key] = Math.random(); // Uh oh, Type 'number' is not assignable to type 'T[K]'.(2322)
}

declare const foo: Foo;
f1(foo, "a"); // Allowed, good
f1(foo, "whatevs"); // Allowed, good
f1(foo, "nine"); // Uh oh, should error, but doesn't!
f1(foo, "b"); // Error, good

f2(foo, "a"); // Allowed, good
f2(foo, "whatevs"); // Allowed, good
f2(foo, "nine"); // Error, good
f2(foo, "b"); // Error, good

Di Playground, DefinitelyJustNumbersmenunjukkan tooltip dari {a: number; whatevs: number}- apa pun yang dapat saya tetapkan numbersecara eksplisit diketik sebagai number. Ini memperbaiki penetapan di dalam badan fungsi, tetapi gagal mendeteksi fakta bahwa ninehanya sebagian dari angka dan karenanya tidak boleh diizinkan.

CanAssignNumbermenunjukkan tooltip dari {a: number; whatevs: any}, mengecualikan dengan benar ninekarena tidak dapat ditetapkan ke number. Ini terlihat bagus, tetapi masih tidak memperbaiki tugas di dalam fungsi f2.

Jawaban

2 TitianCernicova-Dragomir Oct 21 2020 at 12:11

Cukup gunakan pernyataan tipe, perhatian Anda harus situs panggilan, yang diketik dengan benar dan memberi Anda kesalahan di tempat yang seharusnya. Implementasinya tidak dapat diketik dengan benar jika Anda ingin menetapkan nilai tertentu di dalam fungsi.

Anda dapat membuat hasil T[K]memperpanjang angka, misalnya tetapi menambahkan kendala untuk Tdari Record<K, number>, tapi kami masih belum dapat menetapkan nilai-nilai konkret untukobj[key]

type KeyOfType<T, ValueType> = 
  { [Key in keyof T]-?: T[Key] extends ValueType | undefined ? Key : never }[keyof T]

function f<T extends Record<K, number>, K extends KeyOfType<T, number>>(obj: T, key: K, value: T[K]) {
    let r = obj[key]; 
    r.toExponential(); // seems numberish, but it `T[K]` which does extend number, but might not be number
    obj[key] = obj[key] // T[K] is assignable to T[K]
    obj[key] = value; // even if it is a parameter 
    obj[key] = 5; // still an error
}

declare const foo: Foo;
f(foo, "a", 1); // Allowed, good
f(foo, "b", 2); // Error, good

const other: { 
  a: 1
} = {
  a: 1
}
f(other, "a", 1) // this will break the type of other because of obj[key] = 5

Tautan Playground

Alasannya demikian, adalah contoh terakhir f(other, "a", 1). Di sini, adi othermemiliki tipe 1, yang memperpanjang number, begitu f(other, "a", 1)juga panggilan yang valid ke f, tetapi di dalamnya kami ingin menetapkan other[key] = 5. Ini akan merusak jenis other. Masalahnya di sini adalah bahwa tidak ada cara yang menentukan yang T[K]memiliki batasan atas numberhanya batasan yang lebih rendah.

2 Mahdi Oct 20 2020 at 21:35

Anda harus memperluas dari kunci yang menghasilkan nilai tipe number.

export type PickByValue<T, ValueType> = Pick<
  T,
  { [Key in keyof T]-?: T[Key] extends ValueType ? Key : never }[keyof T]
>;

function f<T extends object, K extends keyof PickByValue<T, number>>(obj: T, key: K) : T[K] {
  return obj[key]
}

Sunting: Apa yang Anda coba lakukan tidak mungkin dilakukan di TS AFAIK, dan terkadang ada alasan bagus untuk itu. misalkan Anda memiliki kode di bawah ini:

function f<T extends object, K extends keyof PickByValue<T, number>>(obj: T, key: K) {
    obj[key] = 5; // Type 'number' is not assignable to type 'T[K]'.
} 

const obj = {a: 9} as const;

f(obj, "a")

Misalnya dalam skenario di atas, nilai properti aadalah angka namun bukan dari tipe numbertapi tipenya 9. tidak ada cara bagi ketikan untuk mengetahui ini sebelumnya. dalam skenario lain, satu-satunya hal yang terlintas di benak saya adalah menggunakan Type Assertions .