Improved typescript experience #1563
Replies: 6 comments 9 replies
-
Update - progress is going well! New patterns are being defined in the WIP PR below: One of the next things on the radar (if anyone in the community is up for helping) is to create a set of TypeScript utilities that can take in an auto-generated collection / global type and "flatten" relationships into only an ID. Right now, our exported types are as safe as possible when it comes to relationship fields. For example, let's take the following configs: const categories: CollectionConfig = {
slug: 'categories',
fields: [
{
name: 'title',
type: 'text',
required: true,
}
]
}
const posts: CollectionConfig = {
slug: 'posts',
fields: [
{
name: 'title',
type: 'text',
required: 'true',
},
{
name: 'category',
type: 'relationship',
relationTo: 'categories',
required: true,
}
]
} Auto-generating types for these collections will result in the following: export interface Category {
title: string
}
export interface Post {
title: string
category: string | Category // could be populated, could not be
} You'll see that the The task at handIt would be great to have a TS utility that takes an auto-generated interface and flatten any unions into only the interfaces, or only the "basic" types of This would be handy for typing the Here's the intended API: type PostData = CreateData<Post>
// PostData would be transformed into:
// {
// title: string
// category: string
// } Additionally, we could create a utility that would dynamically specify population depth, as such: type PostData = WithDepth<Post, 2>
// PostData would be transformed into:
// {
// title: string
// category: WithDepth<Category, 1>
// } We are going to be looking into building this stuff over the next few weeks but would love a hand from anyone willing to contribute! |
Beta Was this translation helpful? Give feedback.
-
Don't know if it's useful for you, but since my project is using building blocks, I achieved some sort of type safety with the followings : Getting the type of a single block// Where Homepage is a generated payload interface from a global page containing a block
// field called `blocks` where every possible blocks are defined
export type BlockTypePicker<T> = Extract<Homepage['blocks'][number], { blockType: T }>; Force the relation of a field on that blockexport type RemoveStringType<T> = T extends string ? never : T;
// Remove type string of a relation property (MK) in an object (T)
export type ForceRelationObj<T extends Record<any, any>, MK extends keyof T> = {
[K in keyof T]: K extends MK
? T[MK] extends Array<unknown>
? RemoveStringType<T[MK][number]>[]
: RemoveStringType<T[MK]>
: T[K];
}; Which can be used like that type MyObject = {
media: Media | string;
title: string;
};
type BetterTypedObject = ForceRelationObj<MyObject , 'media'>;
// {
// media: Media;
// title: string;
// } And when combining the utilities together, you get a fully typed block type FullyTypedBlock = ForceRelationObj<BlockTypePicker<'hero-banner'>, 'thumbnail'> In my case I have to specify the field name |
Beta Was this translation helpful? Give feedback.
-
Super excited about this! thank you all so much! |
Beta Was this translation helpful? Give feedback.
-
Local API type inferences are GOAT! |
Beta Was this translation helpful? Give feedback.
-
Hello, just reposting here what I was posting on Discord: At the moment I am using the following workaround (it's kinda Regex ninja, but works in my case):
const fs = require('fs');
const path = require('path');
const file = fs.readFileSync(path.join(__dirname, '../src/payload-types.ts'), 'utf8');
const types = Array.from(file.matchAll(/export interface (.*) \{/g)).map((match) => match[1]);
const start = file.indexOf('export');
const withDecrement =
file.substring(0, start) +
'type Decrement = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];\n' +
file.substring(start);
const regex1 = RegExp(`(string|string\\[\\]) \\| (${types.join('|')})(\\[\\])?`, 'g');
const withDepth = withDecrement.replace(
regex1,
'D extends Decrement[number] ? D extends 0 ? $1 : $2<Decrement[D]>$3 : $1 | $2$3'
);
const regex2 = RegExp(`(export interface )(${types.join('|')})`, 'g');
const withTypes = withDepth.replace(regex2, '$1$2<D extends Decrement[number] | void = void>');
const regex3 = RegExp(`(: )(${types.join('|')})\;`, 'g');
const withConfig = withTypes.replace(regex3, '$1$2<D>;');
try {
console.log('Creating generic depth types...');
fs.writeFileSync(path.join(__dirname, '../src/payload-types.ts'), withConfig);
console.log('Done.');
} catch (err) {
console.error(err);
}
{
"scripts": "cros-env PAYLOAD_CONFIG_PATH=src/payload.config.js payload generate:types && node scripts/gen-generic-types.js"
} NB:
I would happily contribute to enhance this as a plugin or something and I am thinking of the following roadmap:
function decreaseDepth(object: Collection<D extends number>): Collection<Decrement<D>> What do you think? |
Beta Was this translation helpful? Give feedback.
-
Why is this closed? |
Beta Was this translation helpful? Give feedback.
-
Payload has been a typescript project since it launched and there have been large improvements over time. Now we want to address more typescript sticking points to make working with Payload even better.
Below are some of the items we are interested in:
create
andupdate
local operations do not validate incoming data according to the passed generic typecollection
slug is not typedstring | RelationshipType
is not ideal. There are cases where you know that the doc is populated, so we should enforceRelationshipType
and disregardstring
@echocrow offered up some great detail for some of these in #1319.
Related:
Beta Was this translation helpful? Give feedback.
All reactions