Skip to content

Commit

Permalink
refactor(typings): 改善和纠正各种 id 类型的性质
Browse files Browse the repository at this point in the history
各种 id 类型如:UserId, ProjectId, TaskId 等等,它们应该有下列性质:

 1.可以赋给 string 类型(允许如:`const x: string = id as UserId`)
 2.相互之间不可赋值(如遇 `const tid: TeamId = uid as UserId` 会报错,
   需要手动强转)
 3.string 不可以赋给它们(如遇 `const id: UserId = 'hello'` 会报错,需
   要手动强转)

原来 `interface X extends String { kind?: 'X' }` 的实现,满足了2,但没
有满足1、3。不满足1,导致当需要将 id 数据从带有场景上下文的业务代码传给
不关心业务逻辑而只是简单接受 string 的底层组件时,需要通过 `as string`
强转,如果该信息包在一个对象结构里,那这个对象结构要么需要 `as any`,
结果丢失所有类型信息,要么底层组件的对应对象结构类型声明就需要添加类型
参数(泛型声明),结果增加冗长而意义不大的泛型声明。

新的写法是:

  `type X = string & { kind: 'X' }`

它能同时满足1、2、3。

参考:

 - https://codemix.com/opaque-types-in-javascript/
 - microsoft/TypeScript#15807
 - microsoft/TypeScript#4895
 - microsoft/TypeScript#202
 - https://github.com/Microsoft/TypeScript/blob/d9b93903c035e48c8da1d731332787f83efc4619/src/compiler/types.ts#L54
  • Loading branch information
chuan6 committed Jun 6, 2018
1 parent 2c50839 commit 889b82a
Show file tree
Hide file tree
Showing 20 changed files with 126 additions and 125 deletions.
2 changes: 1 addition & 1 deletion src/apis/event/utils.ts
Expand Up @@ -191,5 +191,5 @@ export function timeToDate(date: string, returnValue?: boolean) {
* 使用该函数根据实例上的 _id 获得原 _id。
*/
export const originEventId = (id: EventId): EventId => {
return id.split('_', 1)[0]
return id.split('_', 1)[0] as EventId
}
2 changes: 1 addition & 1 deletion src/schemas/Member.ts
Expand Up @@ -20,7 +20,7 @@ export interface MemberProfileSchema {

export interface MemberSchema {
_boundToObjectId: ProjectId | OrganizationId
_id: String // 兼容新(MemberId)和旧(UserId),当完成迁移,换为更准确的 MemberId
_id: string // 兼容新(MemberId)和旧(UserId),当完成迁移,换为更准确的 MemberId
_memberId: MemberId
_roleId: RoleId
_userId: UserId
Expand Down
4 changes: 1 addition & 3 deletions src/schemas/TapDashboard.ts
Expand Up @@ -29,9 +29,7 @@ export interface TapCoordination {
size?: TapCoordSize
}

export interface TapDashboardSectionId extends String {
kind?: 'TapDashboardSectionId'
}
export type TapDashboardSectionId = string & { kind: 'TapDashboardSectionId' }

export interface TapDashboardSection {
_id: TapDashboardSectionId
Expand Down
4 changes: 1 addition & 3 deletions src/schemas/TapQuestion.ts
@@ -1,9 +1,7 @@
import { TapChartType } from './TapChart'
import { TapGenericFilterResponse } from 'teambition-types'

export interface TapQuestionId extends String {
kind?: 'QuestionId'
}
export type TapQuestionId = string & { kind: 'TapQuestionId' }

export interface TapQuestion {
_id: TapQuestionId
Expand Down
8 changes: 2 additions & 6 deletions src/schemas/UserMe.ts
Expand Up @@ -19,13 +19,9 @@ export interface UserPaymentPlan {
status: string
}

export interface StrikerToken extends String {
kind?: 'StrikerToken'
}
export type StrikerToken = string & { kind: 'StrikerToken' }

export interface TcmToken extends String {
kind?: 'TcmToken'
}
export type TcmToken = string & { kind: 'TcmToken' }

export interface UserMe {
_id: UserId
Expand Down
92 changes: 46 additions & 46 deletions src/teambition.ts
@@ -1,52 +1,52 @@
// id

declare module 'teambition-types' {
export interface ActivenessId extends String { kind?: 'ActivenessId' }
export interface ActivityId extends String { kind?: 'ActivityId' }
export interface ApplicationId extends String { kind?: 'ApplicationId' }
export interface CollectionId extends String { kind?: 'CollectionId' }
export interface CustomFieldCategoryId extends String { kind?: 'CustomFieldCategoryId' }
export interface CustomFieldChoiceId extends String { kind?: 'CustomFieldChoiceId' }
export interface CustomFieldId extends String { kind?: 'CustomFieldId' }
export interface CustomFieldLinkId extends String { kind?: 'CustomFieldLinkId' }
export interface CustomFieldValueId extends String { kind?: 'CustomFieldValueId' }
export interface CustomRoleId extends String { kind?: 'CustomRoleId' }
export interface EntryCategoryId extends String { kind?: 'EntryCategoryId' }
export interface EntryId extends String { kind?: 'EntryId' }
export interface EventId extends String { kind?: 'EventId' }
export interface FeedbackId extends String { kind?: 'FeedbackId' }
export interface FileId extends String { kind?: 'FileId' }
export interface GroupId extends String { kind?: 'GroupId' }
export interface HomeActivityId extends String { kind?: 'HomeActivityId' }
export interface MemberId extends String { kind?: 'MemberId' }
export interface MessageId extends String { kind?: 'MessageId' }
export interface ObjectLinkId extends String { kind?: 'ObjectLinkId' }
export interface OrganizationId extends String { kind?: 'OrganizationId' }
export interface PostId extends String { kind?: 'PostId' }
export interface PreferenceId extends String { kind?: 'PreferenceId' }
export interface ProjectBoardId extends String { kind?: 'ProjectBoardId' }
export interface ProjectId extends String { kind?: 'ProjectId' }
export interface ProjectTagId extends String { kind?: 'ProjectTagId' }
export interface RoomId extends String { kind?: 'RoomId' }
export interface ScenarioFieldId extends String { kind?: 'ScenarioFieldId' }
export interface ScenarioFieldConfigId extends String { kind?: 'ScenarioFieldConfigId' }
export interface SmartGroupId extends String { kind?: 'SmartGroupId' }
export interface SprintId extends String { kind?: 'SprintId' }
export interface StageId extends String { kind?: 'StageId' }
export interface SubscribeId extends String { kind?: 'SubscribeId' }
export interface SubtaskId extends String { kind?: 'SubtaskId' }
export interface TagCategoryId extends String { kind?: 'TagCategoryId' }
export interface TagId extends String { kind?: 'TagId' }
export interface TapChartId extends String { kind?: 'TapChartId' }
export interface TapDashboardId extends String { kind?: 'TapDashboardId' }
export interface TaskflowId extends String { kind?: 'TaskflowId' }
export interface TaskflowStatusId extends String { kind?: 'TaskflowStatusId' }
export interface TaskId extends String { kind?: 'TaskId' }
export interface TasklistId extends String { kind?: 'TasklistId' }
export interface TeamId extends String { kind?: 'TeamId' }
export interface UserId extends String { kind?: 'UserId' }
export interface VersionId extends String { kind?: 'VersionId' }
export interface WorkId extends String { kind?: 'WorkId' }
export type ActivenessId = string & { kind: 'ActivenessId' }
export type ActivityId = string & { kind: 'ActivityId' }
export type ApplicationId = string & { kind: 'ApplicationId' }
export type CollectionId = string & { kind: 'CollectionId' }
export type CustomFieldCategoryId = string & { kind: 'CustomFieldCategoryId' }
export type CustomFieldChoiceId = string & { kind: 'CustomFieldChoiceId' }
export type CustomFieldId = string & { kind: 'CustomFieldId' }
export type CustomFieldLinkId = string & { kind: 'CustomFieldLinkId' }
export type CustomFieldValueId = string & { kind: 'CustomFieldValueId' }
export type CustomRoleId = string & { kind: 'CustomRoleId' }
export type EntryCategoryId = string & { kind: 'EntryCategoryId' }
export type EntryId = string & { kind: 'EntryId' }
export type EventId = string & { kind: 'EventId' }
export type FeedbackId = string & { kind: 'FeedbackId' }
export type FileId = string & { kind: 'FileId' }
export type GroupId = string & { kind: 'GroupId' }
export type HomeActivityId = string & { kind: 'HomeActivityId' }
export type MemberId = string & { kind: 'MemberId' }
export type MessageId = string & { kind: 'MessageId' }
export type ObjectLinkId = string & { kind: 'ObjectLinkId' }
export type OrganizationId = string & { kind: 'OrganizationId' }
export type PostId = string & { kind: 'PostId' }
export type PreferenceId = string & { kind: 'PreferenceId' }
export type ProjectBoardId = string & { kind: 'ProjectBoardId' }
export type ProjectId = string & { kind: 'ProjectId' }
export type ProjectTagId = string & { kind: 'ProjectTagId' }
export type RoomId = string & { kind: 'RoomId' }
export type ScenarioFieldId = string & { kind: 'ScenarioFieldId' }
export type ScenarioFieldConfigId = string & { kind: 'ScenarioFieldConfigId' }
export type SmartGroupId = string & { kind: 'SmartGroupId' }
export type SprintId = string & { kind: 'SprintId' }
export type StageId = string & { kind: 'StageId' }
export type SubscribeId = string & { kind: 'SubscribeId' }
export type SubtaskId = string & { kind: 'SubtaskId' }
export type TagCategoryId = string & { kind: 'TagCategoryId' }
export type TagId = string & { kind: 'TagId' }
export type TapChartId = string & { kind: 'TapChartId' }
export type TapDashboardId = string & { kind: 'TapDashboardId' }
export type TaskflowId = string & { kind: 'TaskflowId' }
export type TaskflowStatusId = string & { kind: 'TaskflowStatusId' }
export type TaskId = string & { kind: 'TaskId' }
export type TasklistId = string & { kind: 'TasklistId' }
export type TeamId = string & { kind: 'TeamId' }
export type UserId = string & { kind: 'UserId' }
export type VersionId = string & { kind: 'VersionId' }
export type WorkId = string & { kind: 'WorkId' }
}

// computed id
Expand Down
5 changes: 3 additions & 2 deletions test/apis/customfieldlink.spec.ts
Expand Up @@ -5,6 +5,7 @@ import { expect } from 'chai'
import { SDKFetch, createSdk, SDK } from '../'
import { customFieldLink } from '../fixtures/customfieldlinks.fixture'
import { mock, expectToDeepEqualForFieldsOfTheExpected } from '../utils'
import { ProjectId } from 'teambition-types'

const fetchMock = require('fetch-mock')

Expand All @@ -29,7 +30,7 @@ describe('CustomFieldLinkApi request spec: ', () => {
})

it('should return a CustomFieldLink array', function* () {
const projectId = customFieldLink._projectId
const projectId = customFieldLink._projectId as ProjectId
const customFieldLinks = [customFieldLink]
const url = `/projects/${projectId}/customfieldlinks?boundType=application&_=666`

Expand All @@ -51,7 +52,7 @@ describe('CustomFieldLinkApi spec: ', () => {
})

it('should return a CustomFieldLink array', function* () {
const projectId = customFieldLink._projectId
const projectId = customFieldLink._projectId as ProjectId
const customFieldLinks = [customFieldLink]
mockResponse(customFieldLinks)

Expand Down
13 changes: 7 additions & 6 deletions test/apis/event/EventGenerator.spec.ts
Expand Up @@ -10,6 +10,7 @@ import {
} from '../../fixtures/events.fixture'
import { EventGenerator } from '../../../src/apis/event/EventGenerator'
import { clone } from '../../index'
import { EventId } from 'teambition-types'

describe('EventGenerator spec', () => {
let eventGenerator: EventGenerator
Expand Down Expand Up @@ -297,31 +298,31 @@ describe('EventGenerator spec', () => {

it('findByEventId() should work on a normal event', () => {
const _eventGenerator = new EventGenerator(normalEvent as any)
const targetId = normalEvent._id
const targetId = normalEvent._id as EventId
const invalidId = normalEvent._id + 'asdf'

expect(_eventGenerator.findByEventId(targetId)).to.deep.equal(normalEvent)
expect(_eventGenerator.findByEventId(invalidId)).to.be.null
expect(_eventGenerator.findByEventId(invalidId as EventId)).to.be.null
})

it('findByEventId() should return null for a recurrent event with an un-timestamped id', () => {
const targetId = recurrenceByMonth._id
const targetId = recurrenceByMonth._id as EventId

expect(eventGenerator.findByEventId(targetId)).to.be.null
})

it('findByEventId() should work on a recurrent event', () => {
let timestamp = new Date(recurrenceByMonth.startDate).valueOf()
let targetId = recurrenceByMonth._id + '_' + timestamp
let targetId = recurrenceByMonth._id + '_' + timestamp as EventId
expect(eventGenerator.findByEventId(targetId)).to.deep.equal(eventGenerator.next().value)

timestamp = Moment(recurrenceByMonth.startDate).add(2, 'months').valueOf()
targetId = recurrenceByMonth._id + '_' + timestamp
targetId = (recurrenceByMonth._id + '_' + timestamp) as EventId
eventGenerator.next()
expect(eventGenerator.findByEventId(targetId)).to.deep.equal(eventGenerator.next().value)

const timestampExDate = new Date(recurrenceStartAtAnExcludedDate.startDate).valueOf()
const targetIdExDate = recurrenceStartAtAnExcludedDate._id + '_' + timestampExDate
const targetIdExDate = recurrenceStartAtAnExcludedDate._id + '_' + timestampExDate as EventId
const eventGeneratorExDate = new EventGenerator(recurrenceStartAtAnExcludedDate as any)
expect(eventGeneratorExDate.findByEventId(targetIdExDate)).to.be.null
})
Expand Down
11 changes: 6 additions & 5 deletions test/apis/event/event.spec.ts
Expand Up @@ -4,6 +4,7 @@ import { expect } from 'chai'
import { createSdk, SDK, SocketMock, EventSchema } from '../../index'
import * as Fixture from '../../fixtures/events.fixture'
import { mock, restore, equals, looseDeepEqual, clone } from '../../utils'
import { EventId } from 'teambition-types'

describe('EventApi request spec', () => {
let sdk: SDK
Expand All @@ -22,7 +23,7 @@ describe('EventApi request spec', () => {
const fixture = Fixture.normalEvent
mockResponse(fixture)

yield sdk.getEvent(fixture._id)
yield sdk.getEvent(fixture._id as EventId)
.values()
.do(([r]) => {
const result = r.next().value
Expand All @@ -34,7 +35,7 @@ describe('EventApi request spec', () => {
const fixture = Fixture.recurrenceByMonth
mockResponse(fixture)

yield sdk.getEvent(fixture._id)
yield sdk.getEvent(fixture._id as EventId)
.values()
.do(([r]) => {
const result = r.next().value
Expand All @@ -58,7 +59,7 @@ describe('EventApi request spec', () => {
const fixture = Fixture.recurrenceByMonth
mockResponse(fixture)

const signal = sdk.getEvent(fixture._id)
const signal = sdk.getEvent(fixture._id as EventId)
.changes()

signal.subscribe()
Expand All @@ -83,13 +84,13 @@ describe('EventApi request spec', () => {
const fixture = Fixture.recurrenceByMonth
mockResponse(fixture)

const token1 = sdk.getEvent(fixture._id)
const token1 = sdk.getEvent(fixture._id as EventId)

const f2 = clone(fixture)
f2._id = 'mockF2Id'
mockResponse(f2)

const token2 = sdk.getEvent(f2._id)
const token2 = sdk.getEvent(f2._id as EventId)

yield token1.combine(token2)
.values()
Expand Down
3 changes: 2 additions & 1 deletion test/apis/file.spec.ts
Expand Up @@ -3,6 +3,7 @@ import { expect } from 'chai'
import { createSdk, SDK, SocketMock, FileSchema } from '../index'
import * as Fixture from '../fixtures/files.fixture'
import { mock, restore, looseDeepEqual, expectToDeepEqualForFieldsOfTheExpected } from '../utils'
import { FileId } from 'teambition-types'

describe('FileApi request spec', () => {
let sdk: SDK
Expand All @@ -21,7 +22,7 @@ describe('FileApi request spec', () => {
const [ fixture ] = Fixture.projectFiles
mockResponse(fixture)

yield sdk.getFile(fixture._id)
yield sdk.getFile(fixture._id as FileId)
.values()
.do(([r]) => {
expectToDeepEqualForFieldsOfTheExpected(r, fixture)
Expand Down
15 changes: 9 additions & 6 deletions test/apis/like.spec.ts
Expand Up @@ -4,10 +4,13 @@ import { describe, it, beforeEach, afterEach } from 'tman'
import { createSdk, SDK, LikeSchema } from '../index'
import like from '../fixtures/like.fixture'
import { mock, restore } from '../utils'
import { DetailObjectId } from 'teambition-types'

describe('LikeApi request spec: ', () => {
let sdk: SDK
let mockResponse: <T>(m: T, delay?: number | Promise<any>) => void
const mockTaskId = 'mocktask' as DetailObjectId
const mockTaskLikeId = 'mocktask:like'

beforeEach(() => {
sdk = createSdk()
Expand All @@ -21,7 +24,7 @@ describe('LikeApi request spec: ', () => {
it('get like should pass', function* () {
mockResponse(like)

yield sdk.getLike('task', 'mocktask')
yield sdk.getLike('task', mockTaskId)
.values()
.do(([r]) => {
delete r._id
Expand All @@ -32,15 +35,15 @@ describe('LikeApi request spec: ', () => {
it('toggle like should pass', function* () {
yield sdk.database.insert('Like', {
...like,
_id: 'mocktask:like'
_id: mockTaskLikeId
})

mockResponse({ ...like, isLike: false })

yield sdk.toggleLike('task', 'mocktask', true)
yield sdk.toggleLike('task', mockTaskId, true)

yield sdk.database.get<LikeSchema>('Like', {
where: { _id: 'mocktask:like' }
where: { _id: mockTaskLikeId }
})
.values()
.do(([r]) => {
Expand All @@ -49,10 +52,10 @@ describe('LikeApi request spec: ', () => {

mockResponse({ ...like, isLike: true })

yield sdk.toggleLike('task', 'mocktask', false)
yield sdk.toggleLike('task', mockTaskId, false)

yield sdk.database.get<LikeSchema>('Like', {
where: { _id: 'mocktask:like' }
where: { _id: mockTaskLikeId }
})
.values()
.do(([r]) => {
Expand Down
3 changes: 2 additions & 1 deletion test/apis/my.spec.ts
Expand Up @@ -4,9 +4,10 @@ import { createSdk, SDK, TaskSchema } from '../index'
import { EventGenerator } from '../../src/apis/event/EventGenerator'
import * as Fixture from '../fixtures/my.fixture'
import { mock, restore, expectToDeepEqualForFieldsOfTheExpected } from '../utils'
import { UserId } from 'teambition-types'

describe('MyApi request spec', () => {
const userId = Fixture.myRecent[0]['_executorId']
const userId = Fixture.myRecent[0]['_executorId'] as UserId
let sdk: SDK
let mockResponse: <T>(m: T, delay?: number | Promise<any>) => void

Expand Down
5 changes: 3 additions & 2 deletions test/apis/organization.spec.ts
Expand Up @@ -9,6 +9,7 @@ import {
getStarredOrganizationProjects,
getUngroupedOrganizationProjects
} from '../../src/apis/organization/projects'
import { OrganizationId, TagId } from 'teambition-types'

const fetchMock = require('fetch-mock')

Expand All @@ -17,7 +18,7 @@ describe('get organization projects', () => {

let sdkFetch: SDKFetch
let projects: any[]
const sampleOrgId = '56f0d51e3cd13a5b537c3a12'
const sampleOrgId = '56f0d51e3cd13a5b537c3a12' as OrganizationId
const getOrganizationProjectsFns = () => [
{ fn: getAllOrganizationProjects, namespace: 'all' },
{ fn: getJoinedOrganizationProjects, namespace: 'joined' },
Expand Down Expand Up @@ -71,7 +72,7 @@ describe('get organization projects', () => {
})

it('getOrganizationProjectByTagId should make correctly formatted request to target url and return response as it is', function* () {
const sampleTagId = '3'
const sampleTagId = '3' as TagId
const expectedUrl = `/organizations/${sampleOrgId}/projecttags/${sampleTagId}/projects?_=666`
const expectedResponse = projects.filter(({ tagId }) => tagId === sampleTagId)

Expand Down

0 comments on commit 889b82a

Please sign in to comment.