Skip to content

Commit aaa86ba

Browse files
committed
Rework ID
1 parent dd3ebbf commit aaa86ba

File tree

2 files changed

+63
-6
lines changed

2 files changed

+63
-6
lines changed

src/common/id-field.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/* eslint-disable @seedcompany/no-unused-vars */
2+
import { test } from '@jest/globals';
3+
import type { User } from '../components/user';
4+
import { ID } from './id-field';
5+
6+
test('only types here', () => undefined);
7+
8+
const NameFromSelf: ID<'User'> = '' as ID<'User'>;
9+
const NameFromDB: ID<'User'> = '' as ID<'default::User'>;
10+
const NameFromInstance: ID<'User'> = '' as ID<User>;
11+
const NameFromStatic: ID<'User'> = '' as ID<typeof User>;
12+
13+
const NameToSelf: ID<'User'> = '' as ID<'User'>;
14+
const NameToDB: ID<'default::User'> = '' as ID<'User'>;
15+
const NameToInstance: ID<User> = '' as ID<'User'>;
16+
const NameToStatic: ID<typeof User> = '' as ID<'User'>;
17+
18+
const NameFromAny: ID<'User'> = '' as ID;
19+
const NameToAny: ID = '' as ID<'User'>;
20+
21+
const AnyStringWorks: ID<'asdf'> = '' as ID;
22+
const AnyObjectWorks: ID<Date> = '' as ID;
23+
24+
// @ts-expect-error this should be blocked
25+
const UserIncompatibleDifferent: ID<'User'> = '' as ID<'Location'>;
26+
// @ts-expect-error this should be blocked
27+
const UserIncompatibleDifferent2: ID<'Location'> = '' as ID<'User'>;
28+
29+
const SubclassesAreCompatible: ID<'Engagement'> =
30+
'' as ID<'LanguageEngagement'>;
31+
// @ts-expect-error this should be blocked
32+
const InterfaceIsNotDirectlyCompatibleWithConcrete: ID<'LanguageEngagement'> =
33+
'' as ID<'Engagement'>;
34+
const ButCanBeTypeCastAsInterfaceOverlapsConcrete =
35+
'' as ID<'Engagement'> as ID<'LanguageEngagement'>;
36+
// @ts-expect-error this should be blocked
37+
const IndependentTypesCannotBeTypeCast = '' as ID<'Engagement'> as ID<'User'>;

src/common/id-field.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { applyDecorators } from '@nestjs/common';
22
import { Field, FieldOptions, ID as IDType } from '@nestjs/graphql';
33
import { ValidationOptions } from 'class-validator';
4-
import { Opaque } from 'type-fest';
4+
import { IsAny, IsNever, Tagged } from 'type-fest';
5+
import type {
6+
AllResourceDBNames,
7+
ResourceName,
8+
ResourceNameLike,
9+
} from '~/core';
510
import { IsId } from './validators';
611

712
export const IdField = ({
@@ -13,11 +18,26 @@ export const IdField = ({
1318
IsId(validation),
1419
);
1520

16-
export type ID = Opaque<string, 'ID'>;
17-
1821
export const isIdLike = (value: unknown): value is ID =>
1922
typeof value === 'string';
2023

21-
declare const ref: unique symbol;
22-
/** An ID for a specific thing */
23-
export type IdOf<T> = ID & { readonly [ref]: T };
24+
export type ID<Kind extends IDKindLike = any> = Tagged<string, 'ID'> &
25+
IDTo<IDTag<Kind>>;
26+
27+
/** @deprecated Use {@link ID} */
28+
export type IdOf<Kind extends IDKindLike> = ID<Kind>;
29+
30+
declare const IDTo: unique symbol;
31+
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
32+
type IDTo<T> = { readonly [IDTo]: T };
33+
34+
// Normalize resource name if possible, otherwise use input directly
35+
type IDTag<Kind> = IsAny<Kind> extends true
36+
? any // continue to allow ID<any> to be assignable to any ID
37+
: ResourceName<Kind, true> extends infer Normalized
38+
? IsNever<Normalized> extends false
39+
? Normalized
40+
: Kind
41+
: never;
42+
43+
type IDKindLike = ResourceNameLike | AllResourceDBNames | object;

0 commit comments

Comments
 (0)