javascript - TypeScript: Anahtarları açıkça tanımlamadan tipli Record oluşturma


Yapmak istediğim şey, belirli bir arayüzün bir Kaydı olan bir nesne oluşturmak ve TypeScript'in nesnemde ne olduğuna göre anahtarları çıkarabilmesini sağlamak. Birkaç şey denedim, hiçbiri tam olarak aradığımı yapmıyor.

interface Person {
  firstName:string
}

const peopleObj = {
  Jason: {
    firstName: "Jason"
  },
  Tim: {
    firstName: "Tim"
  }
} as const;

console.log(peopleObj);

Burada, eğer peopleObj'e bakarsanız, TypeScript as const sayesinde tam olarak hangi anahtarların olduğunu biliyor. Buradaki sorun, her nesnenin bir Person olmasını zorunlu kılmıyor olmam. Bu yüzden bunu denedim:

const peopleObj: Record<string, Person> = {
  Jason: {
    firstName: "Jason"
  },
  Tim: {
    firstName: "Tim"
  }
} as const;

Burada, tanımlanan Record nedeniyle her nesnenin bir Person olması gerekiyor, ancak TypeScript artık tüm anahtarları bilmiyor çünkü artık bunlar sabit 'Jason' | 'Tim' yerine sadece string. Ve sorun tam olarak burada yatıyor. string tipimin yerine açıkça 'Jason' | 'Tim' kullanabileceğimi biliyorum, ama gerçek hayatta bu oldukça büyük bir nesne ve her ekleme veya çıkarma yaptığımda bu tipi açıkça güncellemek oldukça zahmetli hale geliyor.

İki dünyanın en iyisine sahip olmanın bir yolu var mı, yani nesnedeki anahtarları sadece nesnede olanlara dayanarak TypeScript'in çıkarmasını sağlayabilir miyim? Bir yol buldum, ama çok temiz değil ve muhtemelen daha iyi bir yol olduğuna inanıyorum:

interface Person {
  firstName:string
}

type PeopleType<T extends string> = Record<T, Person>;

const peopleObj: Record<string, Person> = {
  Jason: {
    firstName: "Jason"
  },
  Tim: {
    firstName: "Tim"
  }
} as const;

const People:Record<keyof typeof peopleObj, Person> = peopleObj;

console.log(People.Jason);

Cevap #1

Üçüncü yöntemin aslında çalışmadığını düşünüyorum - çünkü People.foo’ya hata almadan erişebilirsiniz. Bunun nedeni, peopleObj’i Record<string, Person> olarak oluşturduğunuzda, türünün artık bu olmasıdır. Sonra keyof Record<string, Person> yapıyorsunuz, bu da string olarak değerlendirilir.

Bunun için bildiğim tek yol, jeneriklerle bir fonksiyon kullanmaktır. Bu, giriş parametresine bir kısıtlama uygulamanıza ve ardından orijinal türü döndürmenize olanak tanır.

const createPeople = <T extends Record<string, Person>>(people: T) => people;

const myPeople = createPeople({
  Jason: {
    firstName: "Jason"
  },
  Tim: {
    firstName: "Tim"
  }
});

console.log(myPeople.Jason);
console.log(myPeople.foo); // error

Aksi takdirde bir kısır döngü durumu var - yani - anahtarlarımın Person türünde olmasını istiyorum, ama anahtarlarımın ne olduğunu bilmiyorum.

Tercih edebileceğiniz bir diğer yol - temelde aynı şey ama fonksiyon olmadan:

interface Person {
  firstName:string
}

// Force evaluation of type to expanded form
type EvaluateType<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;

type PeopleType<T extends Record<string, Person>> = EvaluateType<{[key in keyof T]: Person }>;

const peopleLookup = {
  Jason: {
    firstName: "Jason"
  },
  Tim: {
    firstName: "Tim"
  }
};

const people: PeopleType<typeof peopleLookup> = peopleLookup;

console.log(people.Jason);
console.log(people.foo);

Cevap #2

@mbdavis'in cevabını genişletiyorum çünkü görünüşe göre ek bir kısıtlama var; o da kayıttaki her Person için anahtarın o Person'ın firstName özelliği olması. Bu, eşlenmiş türleri kullanmamıza olanak tanıyor.

Bir PeopleRecord, her name anahtarı için değeri ya {firstName: name} ile bir Person nesnesi ya da undefined olan bir nesne olmalıdır;

type PeopleRecord = {
    [K in string]?: Person & {firstName: K} 
}

Ne yazık ki, bu türü sadece peopleObj üzerinde uygularsak istediğimiz şekilde çalışmıyor, çünkü isimleri stringden daha spesifik bir şey olarak görmüyor. Belki başka bir kullanıcı bunu nasıl çalıştıracağını bulabilir, ama ben bulamıyorum.

@mbdavis gibi, nesnenin daha spesifik bir kısıtlamayla eşleşmesini sağlamak için yeniden atanmaya ihtiyacım var.

Herhangi bir T tipini alan ve o tip üzerindeki her anahtarın o ilk ada sahip bir Person olmasını zorunlu kılan eşlenmiş tip ValidateRecord<T>'i kullanıyorum.

type ValidateRecord<T> = {
    [P in keyof T]: Person & {firstName: P}
}

Artık peopleObj öğenizi yeniden atayabiliriz, bu yalnızca nesne geçerliyse çalışmalıdır, aksi takdirde hata verecektir.

const validatedObj: ValidateRecord<typeof peopleObj> = peopleObj;

Ama bu doğrulamayı bir fonksiyon aracılığıyla yapmak çok daha temizdir. Fonksiyonun kendisinin sadece girdiyi döndürdüğünü unutmayın. Burada JS tarafında başka herhangi bir doğrulama kontrolü de yapabilirsiniz.

const asValid = <T extends {}>( record: T & ValidateRecord<T> ): ValidateRecord<T> => record;
const goodObj = {
  Jason: {
    firstName: "Jason"
  },
  Tim: {
    firstName: "Tim"
  }
} as const;

// should be ok
const myRecordGood = asValid(goodObj);

// should be a person with {firstName: "Tim"}
const A = myRecordGood["Tim"]

// should be a problem
const B = myRecordGood["Missing"]

const badObj = {
  Jason: {
    firstName: "Bob"
  },
  Tim: {
    firstName: "John"
  }
} as const;

// should be a problem
const myRecordBad = asValid(badObj);

Oyun Alanı Bağlantısı

İlgili makaleler